2025-05-10 02:09:06 +02:00
|
|
|
/*!
|
|
|
|
* Copyright (c) 2024 PLANKA Software GmbH
|
|
|
|
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
|
|
|
*/
|
|
|
|
|
2022-12-26 21:10:50 +01:00
|
|
|
import { attr, fk, many } from 'redux-orm';
|
2019-08-31 04:07:25 +05:00
|
|
|
|
2022-12-26 21:10:50 +01:00
|
|
|
import BaseModel from './BaseModel';
|
2025-05-10 02:09:06 +02:00
|
|
|
import buildSearchParts from '../utils/build-search-parts';
|
|
|
|
import { isListFinite } from '../utils/record-helpers';
|
2019-08-31 04:07:25 +05:00
|
|
|
import ActionTypes from '../constants/ActionTypes';
|
2025-05-22 23:14:46 +02:00
|
|
|
import Config from '../constants/Config';
|
2025-05-10 02:09:06 +02:00
|
|
|
import { BoardContexts, BoardViews } from '../constants/Enums';
|
2019-08-31 04:07:25 +05:00
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
const prepareFetchedBoard = (board) => ({
|
|
|
|
...board,
|
|
|
|
isFetching: false,
|
|
|
|
context: BoardContexts.BOARD,
|
|
|
|
view: board.defaultView,
|
|
|
|
search: '',
|
|
|
|
});
|
2024-04-22 23:15:31 +02:00
|
|
|
|
2022-12-26 21:10:50 +01:00
|
|
|
export default class extends BaseModel {
|
2019-08-31 04:07:25 +05:00
|
|
|
static modelName = 'Board';
|
|
|
|
|
|
|
|
static fields = {
|
|
|
|
id: attr(),
|
|
|
|
position: attr(),
|
|
|
|
name: attr(),
|
2025-05-10 02:09:06 +02:00
|
|
|
defaultView: attr(),
|
|
|
|
defaultCardType: attr(),
|
|
|
|
limitCardTypesToDefaultOne: attr(),
|
|
|
|
alwaysDisplayCardCreator: attr(),
|
|
|
|
context: attr(),
|
|
|
|
view: attr(),
|
|
|
|
search: attr(),
|
|
|
|
isSubscribed: attr({
|
|
|
|
getDefault: () => false,
|
|
|
|
}),
|
2019-08-31 04:07:25 +05:00
|
|
|
isFetching: attr({
|
|
|
|
getDefault: () => null,
|
|
|
|
}),
|
2025-05-22 23:14:46 +02:00
|
|
|
lastActivityId: attr({
|
|
|
|
getDefault: () => null,
|
|
|
|
}),
|
|
|
|
isActivitiesFetching: attr({
|
|
|
|
getDefault: () => false,
|
|
|
|
}),
|
|
|
|
isAllActivitiesFetched: attr({
|
|
|
|
getDefault: () => null,
|
|
|
|
}),
|
2019-08-31 04:07:25 +05:00
|
|
|
projectId: fk({
|
|
|
|
to: 'Project',
|
|
|
|
as: 'project',
|
|
|
|
relatedName: 'boards',
|
|
|
|
}),
|
2021-06-24 01:05:22 +05:00
|
|
|
memberUsers: many({
|
|
|
|
to: 'User',
|
|
|
|
through: 'BoardMembership',
|
|
|
|
relatedName: 'boards',
|
|
|
|
}),
|
2019-08-31 04:07:25 +05:00
|
|
|
filterUsers: many('User', 'filterBoards'),
|
|
|
|
filterLabels: many('Label', 'filterBoards'),
|
|
|
|
};
|
|
|
|
|
|
|
|
static reducer({ type, payload }, Board) {
|
|
|
|
switch (type) {
|
2021-06-24 01:05:22 +05:00
|
|
|
case ActionTypes.LOCATION_CHANGE_HANDLE:
|
2022-12-16 17:05:03 +01:00
|
|
|
if (payload.board) {
|
2025-05-10 02:09:06 +02:00
|
|
|
Board.upsert(prepareFetchedBoard(payload.board));
|
2022-12-16 17:05:03 +01:00
|
|
|
}
|
2021-06-24 01:05:22 +05:00
|
|
|
|
|
|
|
break;
|
|
|
|
case ActionTypes.LOCATION_CHANGE_HANDLE__BOARD_FETCH:
|
|
|
|
case ActionTypes.BOARD_FETCH:
|
|
|
|
Board.withId(payload.id).update({
|
|
|
|
isFetching: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
break;
|
2025-05-10 02:09:06 +02:00
|
|
|
case ActionTypes.SOCKET_RECONNECT_HANDLE: {
|
|
|
|
const boardIds = payload.boards.map(({ id }) => id);
|
2021-06-24 01:05:22 +05:00
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
Board.all()
|
|
|
|
.toModelArray()
|
|
|
|
.forEach((boardModel) => {
|
|
|
|
if (boardModel.isFetching === null || !boardIds.includes(boardModel.id)) {
|
|
|
|
boardModel.deleteWithClearable();
|
|
|
|
}
|
2021-06-24 01:05:22 +05:00
|
|
|
});
|
2025-05-10 02:09:06 +02:00
|
|
|
|
|
|
|
if (payload.board) {
|
|
|
|
const boardModel = Board.withId(payload.board.id);
|
|
|
|
|
|
|
|
if (boardModel) {
|
|
|
|
boardModel.update(payload.board);
|
|
|
|
} else {
|
|
|
|
Board.upsert(prepareFetchedBoard(payload.board));
|
|
|
|
}
|
2021-06-24 01:05:22 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
payload.boards.forEach((board) => {
|
|
|
|
Board.upsert(board);
|
|
|
|
});
|
|
|
|
|
|
|
|
break;
|
2025-05-10 02:09:06 +02:00
|
|
|
}
|
2021-06-24 01:05:22 +05:00
|
|
|
case ActionTypes.SOCKET_RECONNECT_HANDLE__CORE_FETCH:
|
|
|
|
Board.all()
|
|
|
|
.toModelArray()
|
|
|
|
.forEach((boardModel) => {
|
|
|
|
if (boardModel.id !== payload.currentBoardId) {
|
|
|
|
boardModel.update({
|
|
|
|
isFetching: null,
|
|
|
|
});
|
|
|
|
|
|
|
|
boardModel.deleteRelated(payload.currentUserId);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
break;
|
|
|
|
case ActionTypes.CORE_INITIALIZE:
|
|
|
|
if (payload.board) {
|
2025-05-10 02:09:06 +02:00
|
|
|
Board.upsert(prepareFetchedBoard(payload.board));
|
2021-06-24 01:05:22 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
payload.boards.forEach((board) => {
|
|
|
|
Board.upsert(board);
|
|
|
|
});
|
|
|
|
|
|
|
|
break;
|
2025-05-10 02:09:06 +02:00
|
|
|
case ActionTypes.USER_UPDATE_HANDLE:
|
|
|
|
Board.all()
|
|
|
|
.toModelArray()
|
|
|
|
.forEach((boardModel) => {
|
|
|
|
if (!payload.boardIds.includes(boardModel.id)) {
|
|
|
|
boardModel.deleteWithRelated();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (payload.board) {
|
|
|
|
Board.upsert(prepareFetchedBoard(payload.board));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (payload.boards) {
|
|
|
|
payload.boards.forEach((board) => {
|
|
|
|
Board.upsert(board);
|
|
|
|
});
|
|
|
|
}
|
2019-08-31 04:07:25 +05:00
|
|
|
|
|
|
|
break;
|
2025-05-10 02:09:06 +02:00
|
|
|
case ActionTypes.USER_TO_BOARD_FILTER_ADD: {
|
|
|
|
const boardModel = Board.withId(payload.boardId);
|
|
|
|
|
|
|
|
if (payload.replace) {
|
|
|
|
boardModel.filterUsers.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
boardModel.filterUsers.add(payload.id);
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
2019-08-31 04:07:25 +05:00
|
|
|
case ActionTypes.USER_FROM_BOARD_FILTER_REMOVE:
|
|
|
|
Board.withId(payload.boardId).filterUsers.remove(payload.id);
|
|
|
|
|
|
|
|
break;
|
2021-06-24 01:05:22 +05:00
|
|
|
case ActionTypes.PROJECT_CREATE_HANDLE:
|
2020-03-25 00:15:47 +05:00
|
|
|
payload.boards.forEach((board) => {
|
2019-08-31 04:07:25 +05:00
|
|
|
Board.upsert(board);
|
|
|
|
});
|
|
|
|
|
|
|
|
break;
|
2025-05-10 02:09:06 +02:00
|
|
|
case ActionTypes.PROJECT_UPDATE_HANDLE:
|
2021-06-24 01:05:22 +05:00
|
|
|
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
|
|
|
|
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
|
2025-05-10 02:09:06 +02:00
|
|
|
if (payload.board) {
|
|
|
|
Board.upsert(prepareFetchedBoard(payload.board));
|
|
|
|
}
|
|
|
|
|
2021-06-24 01:05:22 +05:00
|
|
|
if (payload.boards) {
|
|
|
|
payload.boards.forEach((board) => {
|
2025-05-10 02:09:06 +02:00
|
|
|
Board.upsert(board);
|
2021-06-24 01:05:22 +05:00
|
|
|
});
|
|
|
|
}
|
2019-08-31 04:07:25 +05:00
|
|
|
|
|
|
|
break;
|
2021-06-24 01:05:22 +05:00
|
|
|
case ActionTypes.BOARD_CREATE:
|
|
|
|
case ActionTypes.BOARD_CREATE_HANDLE:
|
|
|
|
case ActionTypes.BOARD_UPDATE__SUCCESS:
|
|
|
|
case ActionTypes.BOARD_UPDATE_HANDLE:
|
|
|
|
Board.upsert(payload.board);
|
2019-08-31 04:07:25 +05:00
|
|
|
|
|
|
|
break;
|
2021-06-24 01:05:22 +05:00
|
|
|
case ActionTypes.BOARD_CREATE__SUCCESS:
|
2019-08-31 04:07:25 +05:00
|
|
|
Board.withId(payload.localId).delete();
|
2022-12-16 23:48:06 +01:00
|
|
|
Board.upsert(payload.board);
|
2019-08-31 04:07:25 +05:00
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
break;
|
|
|
|
case ActionTypes.BOARD_CREATE__FAILURE:
|
|
|
|
Board.withId(payload.localId).delete();
|
|
|
|
|
2022-12-16 17:05:03 +01:00
|
|
|
break;
|
|
|
|
case ActionTypes.BOARD_FETCH__SUCCESS:
|
2025-05-10 02:09:06 +02:00
|
|
|
Board.upsert(prepareFetchedBoard(payload.board));
|
2022-12-16 17:05:03 +01:00
|
|
|
|
2019-08-31 04:07:25 +05:00
|
|
|
break;
|
2021-06-24 01:05:22 +05:00
|
|
|
case ActionTypes.BOARD_FETCH__FAILURE:
|
2019-08-31 04:07:25 +05:00
|
|
|
Board.withId(payload.id).update({
|
2021-06-24 01:05:22 +05:00
|
|
|
isFetching: null,
|
2019-08-31 04:07:25 +05:00
|
|
|
});
|
|
|
|
|
|
|
|
break;
|
2021-06-24 01:05:22 +05:00
|
|
|
case ActionTypes.BOARD_UPDATE:
|
|
|
|
Board.withId(payload.id).update(payload.data);
|
2019-08-31 04:07:25 +05:00
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
break;
|
|
|
|
case ActionTypes.BOARD_CONTEXT_UPDATE: {
|
|
|
|
const boardModel = Board.withId(payload.id);
|
|
|
|
|
|
|
|
boardModel.update({
|
|
|
|
context: payload.value,
|
|
|
|
view: payload.value === BoardContexts.BOARD ? boardModel.defaultView : BoardViews.LIST,
|
|
|
|
});
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case ActionTypes.IN_BOARD_SEARCH:
|
|
|
|
Board.withId(payload.id).update({
|
|
|
|
search: payload.value,
|
|
|
|
});
|
|
|
|
|
2019-08-31 04:07:25 +05:00
|
|
|
break;
|
2021-06-24 01:05:22 +05:00
|
|
|
case ActionTypes.BOARD_DELETE:
|
|
|
|
Board.withId(payload.id).deleteWithRelated();
|
2019-08-31 04:07:25 +05:00
|
|
|
|
|
|
|
break;
|
2021-06-24 01:05:22 +05:00
|
|
|
case ActionTypes.BOARD_DELETE__SUCCESS:
|
|
|
|
case ActionTypes.BOARD_DELETE_HANDLE: {
|
|
|
|
const boardModel = Board.withId(payload.board.id);
|
|
|
|
|
|
|
|
if (boardModel) {
|
|
|
|
boardModel.deleteWithRelated();
|
|
|
|
}
|
2019-08-31 04:07:25 +05:00
|
|
|
|
|
|
|
break;
|
2021-06-24 01:05:22 +05:00
|
|
|
}
|
2019-08-31 04:07:25 +05:00
|
|
|
case ActionTypes.LABEL_TO_BOARD_FILTER_ADD:
|
|
|
|
Board.withId(payload.boardId).filterLabels.add(payload.id);
|
|
|
|
|
|
|
|
break;
|
|
|
|
case ActionTypes.LABEL_FROM_BOARD_FILTER_REMOVE:
|
|
|
|
Board.withId(payload.boardId).filterLabels.remove(payload.id);
|
|
|
|
|
2025-05-22 23:14:46 +02:00
|
|
|
break;
|
|
|
|
case ActionTypes.ACTIVITIES_IN_BOARD_FETCH:
|
|
|
|
Board.withId(payload.boardId).update({
|
|
|
|
isActivitiesFetching: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
break;
|
|
|
|
case ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS:
|
|
|
|
Board.withId(payload.boardId).update({
|
|
|
|
isActivitiesFetching: false,
|
|
|
|
isAllActivitiesFetched: payload.activities.length < Config.ACTIVITIES_LIMIT,
|
|
|
|
...(payload.activities.length > 0 && {
|
|
|
|
lastActivityId: payload.activities[payload.activities.length - 1].id,
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
|
2019-08-31 04:07:25 +05:00
|
|
|
break;
|
2025-05-10 02:09:06 +02:00
|
|
|
default:
|
|
|
|
}
|
|
|
|
}
|
2024-04-22 23:15:31 +02:00
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
getMembershipsQuerySet() {
|
|
|
|
return this.memberships.orderBy(['id.length', 'id']);
|
|
|
|
}
|
2024-04-22 23:15:31 +02:00
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
getLabelsQuerySet() {
|
|
|
|
return this.labels.orderBy(['position', 'id.length', 'id']);
|
|
|
|
}
|
2024-04-22 23:15:31 +02:00
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
getListsQuerySet() {
|
|
|
|
return this.lists.orderBy(['position', 'id.length', 'id']);
|
2019-08-31 04:07:25 +05:00
|
|
|
}
|
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
getFiniteListsQuerySet() {
|
|
|
|
return this.getListsQuerySet().filter((list) => isListFinite(list));
|
2023-01-09 12:17:06 +01:00
|
|
|
}
|
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
getCustomFieldGroupsQuerySet() {
|
|
|
|
return this.customFieldGroups.orderBy(['position', 'id.length', 'id']);
|
2019-08-31 04:07:25 +05:00
|
|
|
}
|
|
|
|
|
2025-05-22 23:14:46 +02:00
|
|
|
getActivitiesQuerySet() {
|
|
|
|
return this.activities.orderBy(['id.length', 'id'], ['desc', 'desc']);
|
|
|
|
}
|
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
getUnreadNotificationsQuerySet() {
|
|
|
|
return this.notifications.filter({
|
|
|
|
isRead: false,
|
|
|
|
});
|
2024-07-15 15:26:26 +02:00
|
|
|
}
|
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
getNotificationServicesQuerySet() {
|
|
|
|
return this.notificationServices.orderBy(['id.length', 'id']);
|
|
|
|
}
|
|
|
|
|
|
|
|
getMembershipModelByUserId(userId) {
|
2022-08-19 14:00:40 +02:00
|
|
|
return this.memberships
|
|
|
|
.filter({
|
|
|
|
userId,
|
|
|
|
})
|
|
|
|
.first();
|
|
|
|
}
|
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
getCardsModelArray() {
|
|
|
|
return this.getFiniteListsQuerySet()
|
|
|
|
.toModelArray()
|
|
|
|
.flatMap((listModel) => listModel.getCardsModelArray());
|
|
|
|
}
|
|
|
|
|
|
|
|
getFilteredCardsModelArray() {
|
|
|
|
let cardModels = this.getCardsModelArray();
|
|
|
|
|
|
|
|
if (cardModels.length === 0) {
|
|
|
|
return cardModels;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.search) {
|
|
|
|
if (this.search.startsWith('/')) {
|
|
|
|
let searchRegex;
|
|
|
|
try {
|
|
|
|
searchRegex = new RegExp(this.search.substring(1), 'i');
|
|
|
|
} catch {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
cardModels = cardModels.filter(
|
|
|
|
(cardModel) =>
|
|
|
|
searchRegex.test(cardModel.name) ||
|
|
|
|
(cardModel.description && searchRegex.test(cardModel.description)),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
const searchParts = buildSearchParts(this.search);
|
|
|
|
|
|
|
|
cardModels = cardModels.filter((cardModel) => {
|
|
|
|
const name = cardModel.name.toLowerCase();
|
|
|
|
const description = cardModel.description && cardModel.description.toLowerCase();
|
|
|
|
|
|
|
|
return searchParts.every(
|
|
|
|
(searchPart) =>
|
|
|
|
name.includes(searchPart) || (description && description.includes(searchPart)),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const filterUserIds = this.filterUsers.toRefArray().map((user) => user.id);
|
|
|
|
|
|
|
|
if (filterUserIds.length > 0) {
|
|
|
|
cardModels = cardModels.filter((cardModel) => {
|
|
|
|
const users = cardModel.users.toRefArray();
|
|
|
|
return users.some((user) => filterUserIds.includes(user.id));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const filterLabelIds = this.filterLabels.toRefArray().map((label) => label.id);
|
|
|
|
|
|
|
|
if (filterLabelIds.length > 0) {
|
|
|
|
cardModels = cardModels.filter((cardModel) => {
|
|
|
|
const labels = cardModel.labels.toRefArray();
|
|
|
|
return labels.some((label) => filterLabelIds.includes(label.id));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return cardModels;
|
|
|
|
}
|
|
|
|
|
2025-05-22 23:14:46 +02:00
|
|
|
getActivitiesModelArray() {
|
|
|
|
if (this.isAllActivitiesFetched === null) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
const activityModels = this.getActivitiesQuerySet().toModelArray();
|
|
|
|
|
|
|
|
if (this.lastActivityId && this.isAllActivitiesFetched === false) {
|
|
|
|
return activityModels.filter((activityModel) => {
|
|
|
|
if (activityModel.id.length > this.lastActivityId.length) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (activityModel.id.length < this.lastActivityId.length) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return activityModel.id >= this.lastActivityId;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return activityModels;
|
|
|
|
}
|
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
hasMembershipWithUserId(userId) {
|
2021-06-24 01:05:22 +05:00
|
|
|
return this.memberships
|
|
|
|
.filter({
|
|
|
|
userId,
|
|
|
|
})
|
|
|
|
.exists();
|
|
|
|
}
|
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
isAvailableForUser(userModel) {
|
|
|
|
if (!this.project) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-12-26 21:10:50 +01:00
|
|
|
return (
|
2025-05-10 02:09:06 +02:00
|
|
|
this.project.isExternalAccessibleForUser(userModel) ||
|
|
|
|
this.hasMembershipWithUserId(userModel.id)
|
2022-12-26 21:10:50 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
deleteListsWithRelated() {
|
|
|
|
this.lists.toModelArray().forEach((listModel) => {
|
|
|
|
listModel.deleteWithRelated();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
deleteClearable() {
|
|
|
|
this.filterUsers.clear();
|
|
|
|
this.filterLabels.clear();
|
|
|
|
}
|
|
|
|
|
2021-06-24 01:05:22 +05:00
|
|
|
deleteRelated(exceptMemberUserId) {
|
2025-05-10 02:09:06 +02:00
|
|
|
this.deleteClearable();
|
|
|
|
|
2021-06-24 01:05:22 +05:00
|
|
|
this.memberships.toModelArray().forEach((boardMembershipModel) => {
|
|
|
|
if (boardMembershipModel.userId !== exceptMemberUserId) {
|
|
|
|
boardMembershipModel.deleteWithRelated();
|
|
|
|
}
|
2019-08-31 04:07:25 +05:00
|
|
|
});
|
|
|
|
|
2025-05-10 02:09:06 +02:00
|
|
|
this.labels.toModelArray().forEach((labelModel) => {
|
|
|
|
labelModel.deleteWithRelated();
|
2021-06-24 01:05:22 +05:00
|
|
|
});
|
2025-05-10 02:09:06 +02:00
|
|
|
|
|
|
|
this.deleteListsWithRelated();
|
|
|
|
this.notificationServices.delete();
|
|
|
|
}
|
|
|
|
|
|
|
|
deleteWithClearable() {
|
|
|
|
this.deleteClearable();
|
|
|
|
this.delete();
|
2021-06-24 01:05:22 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
deleteWithRelated() {
|
|
|
|
this.deleteRelated();
|
2019-08-31 04:07:25 +05:00
|
|
|
this.delete();
|
|
|
|
}
|
|
|
|
}
|