feat: Version 2

Closes #627, closes #1047
This commit is contained in:
Maksim Eltyshev
2025-05-10 02:09:06 +02:00
parent ad7fb51cfa
commit 2ee1166747
1557 changed files with 76832 additions and 47042 deletions

View File

@@ -1,3 +1,8 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
@@ -13,9 +18,6 @@ export default class extends BaseModel {
createdAt: attr({
getDefault: () => new Date(),
}),
isInCard: attr({
getDefault: () => true,
}),
cardId: fk({
to: 'Card',
as: 'card',
@@ -33,62 +35,25 @@ export default class extends BaseModel {
case ActionTypes.SOCKET_RECONNECT_HANDLE:
Activity.all().delete();
payload.activities.forEach((activity) => {
Activity.upsert({
...activity,
isInCard: false,
});
});
break;
case ActionTypes.CORE_INITIALIZE:
payload.activities.forEach((activity) => {
Activity.upsert({
...activity,
isInCard: false,
});
});
break;
case ActionTypes.LIST_CARDS_MOVE__SUCCESS:
case ActionTypes.ACTIVITIES_FETCH__SUCCESS:
case ActionTypes.ACTIVITIES_DETAILS_TOGGLE__SUCCESS:
case ActionTypes.NOTIFICATION_CREATE_HANDLE:
payload.activities.forEach((activity) => {
Activity.upsert(activity);
});
break;
case ActionTypes.ACTIVITY_CREATE_HANDLE:
case ActionTypes.ACTIVITY_UPDATE_HANDLE:
case ActionTypes.COMMENT_ACTIVITY_CREATE:
case ActionTypes.COMMENT_ACTIVITY_UPDATE__SUCCESS:
Activity.upsert(payload.activity);
break;
case ActionTypes.ACTIVITY_DELETE_HANDLE:
case ActionTypes.COMMENT_ACTIVITY_DELETE__SUCCESS: {
const activityModel = Activity.withId(payload.activity.id);
if (activityModel) {
activityModel.delete();
case ActionTypes.CARDS_UPDATE_HANDLE:
if (payload.activities) {
payload.activities.forEach((activity) => {
Activity.upsert(activity);
});
}
break;
}
case ActionTypes.COMMENT_ACTIVITY_CREATE__SUCCESS:
Activity.withId(payload.localId).delete();
case ActionTypes.ACTIVITY_CREATE_HANDLE:
Activity.upsert(payload.activity);
break;
case ActionTypes.COMMENT_ACTIVITY_UPDATE:
Activity.withId(payload.id).update({
data: payload.data,
});
break;
case ActionTypes.COMMENT_ACTIVITY_DELETE:
Activity.withId(payload.id).delete();
break;
default:
}

View File

@@ -1,16 +1,41 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';
import { AttachmentTypes } from '../constants/Enums';
const prepareAttachment = (attachment) => {
if (attachment.type !== AttachmentTypes.FILE || !attachment.data) {
return attachment;
}
const filename = attachment.data.url.split('/').pop().toLowerCase();
let extension = filename.slice((Math.max(0, filename.lastIndexOf('.')) || Infinity) + 1);
extension = extension ? extension.toLowerCase() : null;
return {
...attachment,
data: {
...attachment.data,
filename,
extension,
},
};
};
export default class extends BaseModel {
static modelName = 'Attachment';
static fields = {
id: attr(),
url: attr(),
coverUrl: attr(),
image: attr(),
type: attr(),
data: attr(),
name: attr(),
createdAt: attr({
getDefault: () => new Date(),
@@ -20,46 +45,45 @@ export default class extends BaseModel {
as: 'card',
relatedName: 'attachments',
}),
creatorUserId: fk({
to: 'User',
as: 'creatorUser',
relatedName: 'createdAttachments',
}),
};
static reducer({ type, payload }, Attachment) {
switch (type) {
case ActionTypes.LOCATION_CHANGE_HANDLE:
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.USER_UPDATE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
if (payload.attachments) {
payload.attachments.forEach((attachment) => {
Attachment.upsert(attachment);
Attachment.upsert(prepareAttachment(attachment));
});
}
break;
case ActionTypes.SOCKET_RECONNECT_HANDLE:
Attachment.all().delete();
if (payload.attachments) {
// FIXME: bug with oneToOne relation in Redux-ORM
const attachmentIds = payload.attachments.map((attachment) => attachment.id);
Attachment.all()
.toModelArray()
.forEach((attachmentModel) => {
if (!attachmentIds.includes(attachmentModel.id)) {
attachmentModel.delete();
}
});
payload.attachments.forEach((attachment) => {
Attachment.upsert(attachment);
Attachment.upsert(prepareAttachment(attachment));
});
} else {
Attachment.all().delete();
}
break;
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.attachments.forEach((attachment) => {
Attachment.upsert(attachment);
Attachment.upsert(prepareAttachment(attachment));
});
break;
@@ -67,12 +91,16 @@ export default class extends BaseModel {
case ActionTypes.ATTACHMENT_CREATE_HANDLE:
case ActionTypes.ATTACHMENT_UPDATE__SUCCESS:
case ActionTypes.ATTACHMENT_UPDATE_HANDLE:
Attachment.upsert(payload.attachment);
Attachment.upsert(prepareAttachment(payload.attachment));
break;
case ActionTypes.ATTACHMENT_CREATE__SUCCESS:
Attachment.withId(payload.localId).delete();
Attachment.upsert(payload.attachment);
Attachment.upsert(prepareAttachment(payload.attachment));
break;
case ActionTypes.ATTACHMENT_CREATE__FAILURE:
Attachment.withId(payload.localId).delete();
break;
case ActionTypes.ATTACHMENT_UPDATE:
@@ -96,4 +124,16 @@ export default class extends BaseModel {
default:
}
}
duplicate(id, data) {
return this.getClass().create({
id,
cardId: this.cardId,
creatorUserId: this.creatorUserId,
type: this.type,
data: this.data,
name: this.name,
...data,
});
}
}

View File

@@ -0,0 +1,98 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';
export default class extends BaseModel {
static modelName = 'BackgroundImage';
static fields = {
id: attr(),
url: attr(),
thumbnailUrls: attr(),
projectId: fk({
to: 'Project',
as: 'project',
relatedName: 'backgroundImages',
}),
};
static reducer({ type, payload }, BackgroundImage) {
switch (type) {
case ActionTypes.SOCKET_RECONNECT_HANDLE:
BackgroundImage.all().delete();
payload.backgroundImages.forEach((backgroundImage) => {
BackgroundImage.upsert(backgroundImage);
});
break;
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.PROJECT_CREATE_HANDLE:
payload.backgroundImages.forEach((backgroundImage) => {
BackgroundImage.upsert(backgroundImage);
});
break;
case ActionTypes.USER_UPDATE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
if (payload.backgroundImages) {
payload.backgroundImages.forEach((backgroundImage) => {
BackgroundImage.upsert(backgroundImage);
});
}
break;
case ActionTypes.BACKGROUND_IMAGE_CREATE:
case ActionTypes.BACKGROUND_IMAGE_CREATE_HANDLE:
BackgroundImage.upsert(payload.backgroundImage);
break;
case ActionTypes.BACKGROUND_IMAGE_CREATE__SUCCESS:
BackgroundImage.withId(payload.localId).delete();
BackgroundImage.upsert(payload.backgroundImage);
break;
case ActionTypes.BACKGROUND_IMAGE_CREATE__FAILURE:
BackgroundImage.withId(payload.localId).delete();
break;
case ActionTypes.BACKGROUND_IMAGE_DELETE:
BackgroundImage.withId(payload.id).deleteWithRelated();
break;
case ActionTypes.BACKGROUND_IMAGE_DELETE__SUCCESS:
case ActionTypes.BACKGROUND_IMAGE_DELETE_HANDLE: {
const backgroundImageModel = BackgroundImage.withId(payload.backgroundImage.id);
if (backgroundImageModel) {
backgroundImageModel.deleteWithRelated();
}
break;
}
default:
}
}
deleteRelated() {
if (this.backgroundedProject) {
this.backgroundedProject.update({
backgroundType: null,
backgroundImageId: null,
});
}
}
deleteWithRelated() {
this.deleteRelated();
this.delete();
}
}

View File

@@ -0,0 +1,108 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';
export default class extends BaseModel {
static modelName = 'BaseCustomFieldGroup';
static fields = {
id: attr(),
name: attr(),
projectId: fk({
to: 'Project',
as: 'project',
relatedName: 'baseCustomFieldGroups',
}),
};
static reducer({ type, payload }, BaseCustomFieldGroup) {
switch (type) {
case ActionTypes.SOCKET_RECONNECT_HANDLE:
BaseCustomFieldGroup.all().delete();
payload.baseCustomFieldGroups.forEach((baseCustomFieldGroup) => {
BaseCustomFieldGroup.upsert(baseCustomFieldGroup);
});
break;
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.PROJECT_CREATE_HANDLE:
payload.baseCustomFieldGroups.forEach((baseCustomFieldGroup) => {
BaseCustomFieldGroup.upsert(baseCustomFieldGroup);
});
break;
case ActionTypes.USER_UPDATE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
if (payload.baseCustomFieldGroups) {
payload.baseCustomFieldGroups.forEach((baseCustomFieldGroup) => {
BaseCustomFieldGroup.upsert(baseCustomFieldGroup);
});
}
break;
case ActionTypes.BASE_CUSTOM_FIELD_GROUP_CREATE:
case ActionTypes.BASE_CUSTOM_FIELD_GROUP_CREATE_HANDLE:
case ActionTypes.BASE_CUSTOM_FIELD_GROUP_UPDATE__SUCCESS:
case ActionTypes.BASE_CUSTOM_FIELD_GROUP_UPDATE_HANDLE:
BaseCustomFieldGroup.upsert(payload.baseCustomFieldGroup);
break;
case ActionTypes.BASE_CUSTOM_FIELD_GROUP_CREATE__SUCCESS:
BaseCustomFieldGroup.withId(payload.localId).delete();
BaseCustomFieldGroup.upsert(payload.baseCustomFieldGroup);
break;
case ActionTypes.BASE_CUSTOM_FIELD_GROUP_CREATE__FAILURE:
BaseCustomFieldGroup.withId(payload.localId).delete();
break;
case ActionTypes.BASE_CUSTOM_FIELD_GROUP_UPDATE:
BaseCustomFieldGroup.withId(payload.id).update(payload.data);
break;
case ActionTypes.BASE_CUSTOM_FIELD_GROUP_DELETE:
BaseCustomFieldGroup.withId(payload.id).deleteWithRelated();
break;
case ActionTypes.BASE_CUSTOM_FIELD_GROUP_DELETE__SUCCESS:
case ActionTypes.BASE_CUSTOM_FIELD_GROUP_DELETE_HANDLE: {
const baseCustomFieldGroupModel = BaseCustomFieldGroup.withId(
payload.baseCustomFieldGroup.id,
);
if (baseCustomFieldGroupModel) {
baseCustomFieldGroupModel.deleteWithRelated();
}
break;
}
default:
}
}
getCustomFieldsQuerySet() {
return this.customFields.orderBy(['position', 'id.length', 'id']);
}
deleteRelated() {
this.customFields.delete();
this.customFieldGroups.toModelArray().forEach((customFieldGroupModel) => {
customFieldGroupModel.deleteWithRelated();
});
}
deleteWithRelated() {
this.deleteRelated();
this.delete();
}
}

View File

@@ -1,3 +1,8 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { Model } from 'redux-orm';
export default class BaseModel extends Model {

View File

@@ -1,11 +1,23 @@
import orderBy from 'lodash/orderBy';
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk, many } from 'redux-orm';
import BaseModel from './BaseModel';
import buildSearchParts from '../utils/build-search-parts';
import { isListFinite } from '../utils/record-helpers';
import ActionTypes from '../constants/ActionTypes';
import { BoardContexts, BoardViews } from '../constants/Enums';
import User from './User';
import Label from './Label';
const prepareFetchedBoard = (board) => ({
...board,
isFetching: false,
context: BoardContexts.BOARD,
view: board.defaultView,
search: '',
});
export default class extends BaseModel {
static modelName = 'Board';
@@ -14,6 +26,16 @@ export default class extends BaseModel {
id: attr(),
position: attr(),
name: attr(),
defaultView: attr(),
defaultCardType: attr(),
limitCardTypesToDefaultOne: attr(),
alwaysDisplayCardCreator: attr(),
context: attr(),
view: attr(),
search: attr(),
isSubscribed: attr({
getDefault: () => false,
}),
isFetching: attr({
getDefault: () => null,
}),
@@ -29,19 +51,13 @@ export default class extends BaseModel {
}),
filterUsers: many('User', 'filterBoards'),
filterLabels: many('Label', 'filterBoards'),
filterText: attr({
getDefault: () => '',
}),
};
static reducer({ type, payload }, Board) {
switch (type) {
case ActionTypes.LOCATION_CHANGE_HANDLE:
if (payload.board) {
Board.upsert({
...payload.board,
isFetching: false,
});
Board.upsert(prepareFetchedBoard(payload.board));
}
break;
@@ -52,14 +68,25 @@ export default class extends BaseModel {
});
break;
case ActionTypes.SOCKET_RECONNECT_HANDLE:
Board.all().delete();
case ActionTypes.SOCKET_RECONNECT_HANDLE: {
const boardIds = payload.boards.map(({ id }) => id);
Board.all()
.toModelArray()
.forEach((boardModel) => {
if (boardModel.isFetching === null || !boardIds.includes(boardModel.id)) {
boardModel.deleteWithClearable();
}
});
if (payload.board) {
Board.upsert({
...payload.board,
isFetching: false,
});
const boardModel = Board.withId(payload.board.id);
if (boardModel) {
boardModel.update(payload.board);
} else {
Board.upsert(prepareFetchedBoard(payload.board));
}
}
payload.boards.forEach((board) => {
@@ -67,6 +94,7 @@ export default class extends BaseModel {
});
break;
}
case ActionTypes.SOCKET_RECONNECT_HANDLE__CORE_FETCH:
Board.all()
.toModelArray()
@@ -83,10 +111,7 @@ export default class extends BaseModel {
break;
case ActionTypes.CORE_INITIALIZE:
if (payload.board) {
Board.upsert({
...payload.board,
isFetching: false,
});
Board.upsert(prepareFetchedBoard(payload.board));
}
payload.boards.forEach((board) => {
@@ -94,10 +119,37 @@ export default class extends BaseModel {
});
break;
case ActionTypes.USER_TO_BOARD_FILTER_ADD:
Board.withId(payload.boardId).filterUsers.add(payload.id);
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);
});
}
break;
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;
}
case ActionTypes.USER_FROM_BOARD_FILTER_REMOVE:
Board.withId(payload.boardId).filterUsers.remove(payload.id);
@@ -108,17 +160,16 @@ export default class extends BaseModel {
});
break;
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
if (payload.board) {
Board.upsert(prepareFetchedBoard(payload.board));
}
if (payload.boards) {
payload.boards.forEach((board) => {
Board.upsert({
...board,
...(payload.board &&
payload.board.id === board.id && {
isFetching: false,
}),
});
Board.upsert(board);
});
}
@@ -134,12 +185,13 @@ export default class extends BaseModel {
Board.withId(payload.localId).delete();
Board.upsert(payload.board);
break;
case ActionTypes.BOARD_CREATE__FAILURE:
Board.withId(payload.localId).delete();
break;
case ActionTypes.BOARD_FETCH__SUCCESS:
Board.upsert({
...payload.board,
isFetching: false,
});
Board.upsert(prepareFetchedBoard(payload.board));
break;
case ActionTypes.BOARD_FETCH__FAILURE:
@@ -151,6 +203,22 @@ export default class extends BaseModel {
case ActionTypes.BOARD_UPDATE:
Board.withId(payload.id).update(payload.data);
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,
});
break;
case ActionTypes.BOARD_DELETE:
Board.withId(payload.id).deleteWithRelated();
@@ -174,66 +242,41 @@ export default class extends BaseModel {
Board.withId(payload.boardId).filterLabels.remove(payload.id);
break;
case ActionTypes.TEXT_FILTER_IN_CURRENT_BOARD: {
const board = Board.withId(payload.boardId);
let filterText = payload.text;
const posSpace = filterText.indexOf(' ');
// Shortcut to user filters
const posAT = filterText.indexOf('@');
if (posAT >= 0 && posSpace > 0 && posAT < posSpace) {
const userId = User.findUsersFromText(
filterText.substring(posAT + 1, posSpace),
board.memberships.toModelArray().map((membership) => membership.user),
);
if (
userId &&
board.filterUsers.toModelArray().filter((user) => user.id === userId).length === 0
) {
board.filterUsers.add(userId);
filterText = filterText.substring(0, posAT);
}
}
// Shortcut to label filters
const posSharp = filterText.indexOf('#');
if (posSharp >= 0 && posSpace > 0 && posSharp < posSpace) {
const labelId = Label.findLabelsFromText(
filterText.substring(posSharp + 1, posSpace),
board.labels.toModelArray(),
);
if (
labelId &&
board.filterLabels.toModelArray().filter((label) => label.id === labelId).length === 0
) {
board.filterLabels.add(labelId);
filterText = filterText.substring(0, posSharp);
}
}
board.update({ filterText });
break;
}
default:
}
}
getOrderedLabelsQuerySet() {
return this.labels.orderBy('position');
getMembershipsQuerySet() {
return this.memberships.orderBy(['id.length', 'id']);
}
getOrderedListsQuerySet() {
return this.lists.orderBy('position');
getLabelsQuerySet() {
return this.labels.orderBy(['position', 'id.length', 'id']);
}
getOrderedMembershipsModelArray() {
return orderBy(this.memberships.toModelArray(), (boardMembershipModel) =>
boardMembershipModel.user.name.toLocaleLowerCase(),
);
getListsQuerySet() {
return this.lists.orderBy(['position', 'id.length', 'id']);
}
getMembershipModelForUser(userId) {
getFiniteListsQuerySet() {
return this.getListsQuerySet().filter((list) => isListFinite(list));
}
getCustomFieldGroupsQuerySet() {
return this.customFieldGroups.orderBy(['position', 'id.length', 'id']);
}
getUnreadNotificationsQuerySet() {
return this.notifications.filter({
isRead: false,
});
}
getNotificationServicesQuerySet() {
return this.notificationServices.orderBy(['id.length', 'id']);
}
getMembershipModelByUserId(userId) {
return this.memberships
.filter({
userId,
@@ -241,7 +284,70 @@ export default class extends BaseModel {
.first();
}
hasMembershipForUser(userId) {
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;
}
hasMembershipWithUserId(userId) {
return this.memberships
.filter({
userId,
@@ -249,24 +355,48 @@ export default class extends BaseModel {
.exists();
}
isAvailableForUser(userId) {
isAvailableForUser(userModel) {
if (!this.project) {
return false;
}
return (
this.project && (this.project.hasManagerForUser(userId) || this.hasMembershipForUser(userId))
this.project.isExternalAccessibleForUser(userModel) ||
this.hasMembershipWithUserId(userModel.id)
);
}
deleteListsWithRelated() {
this.lists.toModelArray().forEach((listModel) => {
listModel.deleteWithRelated();
});
}
deleteClearable() {
this.filterUsers.clear();
this.filterLabels.clear();
}
deleteRelated(exceptMemberUserId) {
this.deleteClearable();
this.memberships.toModelArray().forEach((boardMembershipModel) => {
if (boardMembershipModel.userId !== exceptMemberUserId) {
boardMembershipModel.deleteWithRelated();
}
});
this.labels.delete();
this.lists.toModelArray().forEach((listModel) => {
listModel.deleteWithRelated();
this.labels.toModelArray().forEach((labelModel) => {
labelModel.deleteWithRelated();
});
this.deleteListsWithRelated();
this.notificationServices.delete();
}
deleteWithClearable() {
this.deleteClearable();
this.delete();
}
deleteWithRelated() {

View File

@@ -1,3 +1,8 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
@@ -10,9 +15,6 @@ export default class extends BaseModel {
id: attr(),
role: attr(),
canComment: attr(),
createdAt: attr({
getDefault: () => new Date(),
}),
boardId: fk({
to: 'Board',
as: 'board',
@@ -28,6 +30,8 @@ export default class extends BaseModel {
static reducer({ type, payload }, BoardMembership) {
switch (type) {
case ActionTypes.LOCATION_CHANGE_HANDLE:
case ActionTypes.USER_UPDATE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
if (payload.boardMemberships) {
payload.boardMemberships.forEach((boardMembership) => {
@@ -47,6 +51,7 @@ export default class extends BaseModel {
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.PROJECT_CREATE_HANDLE:
case ActionTypes.BOARD_CREATE__SUCCESS:
case ActionTypes.BOARD_CREATE_HANDLE:
case ActionTypes.BOARD_FETCH__SUCCESS:
payload.boardMemberships.forEach((boardMembership) => {
BoardMembership.upsert(boardMembership);
@@ -54,6 +59,8 @@ export default class extends BaseModel {
break;
case ActionTypes.BOARD_MEMBERSHIP_CREATE:
case ActionTypes.BOARD_MEMBERSHIP_UPDATE__SUCCESS:
case ActionTypes.BOARD_MEMBERSHIP_UPDATE_HANDLE:
BoardMembership.upsert(payload.boardMembership);
break;
@@ -61,6 +68,10 @@ export default class extends BaseModel {
BoardMembership.withId(payload.localId).delete();
BoardMembership.upsert(payload.boardMembership);
break;
case ActionTypes.BOARD_MEMBERSHIP_CREATE__FAILURE:
BoardMembership.withId(payload.localId).delete();
break;
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
BoardMembership.upsert(payload.boardMembership);
@@ -75,14 +86,9 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_UPDATE:
BoardMembership.withId(payload.id).update(payload.data);
break;
case ActionTypes.BOARD_MEMBERSHIP_UPDATE__SUCCESS:
case ActionTypes.BOARD_MEMBERSHIP_UPDATE_HANDLE:
BoardMembership.upsert(payload.boardMembership);
break;
case ActionTypes.BOARD_MEMBERSHIP_DELETE:
BoardMembership.withId(payload.id).deleteWithRelated();
BoardMembership.withId(payload.id).deleteWithRelated(payload.isCurrentUser);
break;
case ActionTypes.BOARD_MEMBERSHIP_DELETE__SUCCESS:
@@ -90,7 +96,7 @@ export default class extends BaseModel {
const boardMembershipModel = BoardMembership.withId(payload.boardMembership.id);
if (boardMembershipModel) {
boardMembershipModel.deleteWithRelated();
boardMembershipModel.deleteWithRelated(payload.isCurrentUser);
}
break;
@@ -99,20 +105,44 @@ export default class extends BaseModel {
}
}
deleteRelated() {
deleteRelated(isCurrentUser = false) {
if (isCurrentUser) {
this.board.isSubscribed = false;
}
this.board.cards.toModelArray().forEach((cardModel) => {
if (isCurrentUser) {
cardModel.update({
isSubscribed: false,
});
}
try {
cardModel.users.remove(this.userId);
} catch {} // eslint-disable-line no-empty
} catch {
/* empty */
}
cardModel.taskLists.toModelArray().forEach((taskListModel) => {
taskListModel.tasks.toModelArray().forEach((taskModel) => {
if (taskModel.assigneeUserId === this.userId) {
taskModel.update({
assigneeUserId: null,
});
}
});
});
});
try {
this.board.filterUsers.remove(this.userId);
} catch {} // eslint-disable-line no-empty
} catch {
/* empty */
}
}
deleteWithRelated() {
this.deleteRelated();
deleteWithRelated(isCurrentUser) {
this.deleteRelated(isCurrentUser);
this.delete();
}
}

View File

@@ -1,41 +1,51 @@
import pick from 'lodash/pick';
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk, many, oneToOne } from 'redux-orm';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';
import Config from '../constants/Config';
import { ActivityTypes } from '../constants/Enums';
export default class extends BaseModel {
static modelName = 'Card';
static fields = {
id: attr(),
type: attr(),
position: attr(),
name: attr(),
description: attr(),
creatorUserId: oneToOne({
to: 'User',
as: 'creatorUser',
relatedName: 'ownCards',
}),
dueDate: attr(),
isDueDateCompleted: attr(),
stopwatch: attr(),
createdAt: attr({
getDefault: () => new Date(),
}),
listChangedAt: attr({
getDefault: () => new Date(),
}),
isSubscribed: attr({
getDefault: () => false,
}),
lastCommentId: attr({
getDefault: () => null,
}),
isCommentsFetching: attr({
getDefault: () => false,
}),
isAllCommentsFetched: attr({
getDefault: () => null,
}),
lastActivityId: attr({
getDefault: () => null,
}),
isActivitiesFetching: attr({
getDefault: () => false,
}),
isAllActivitiesFetched: attr({
getDefault: () => false,
}),
isActivitiesDetailsVisible: attr({
getDefault: () => false,
}),
isActivitiesDetailsFetching: attr({
getDefault: () => false,
getDefault: () => null,
}),
boardId: fk({
to: 'Board',
@@ -47,6 +57,16 @@ export default class extends BaseModel {
as: 'list',
relatedName: 'cards',
}),
creatorUserId: fk({
to: 'User',
as: 'creatorUser',
relatedName: 'createdCards',
}),
prevListId: fk({
to: 'List',
as: 'prevList',
relatedName: 'prevCards',
}),
coverAttachmentId: oneToOne({
to: 'Attachment',
as: 'coverAttachment',
@@ -60,6 +80,8 @@ export default class extends BaseModel {
switch (type) {
case ActionTypes.LOCATION_CHANGE_HANDLE:
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.USER_UPDATE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
if (payload.cards) {
@@ -121,7 +143,9 @@ export default class extends BaseModel {
case ActionTypes.USER_TO_CARD_ADD_HANDLE:
try {
Card.withId(payload.cardMembership.cardId).users.add(payload.cardMembership.userId);
} catch {} // eslint-disable-line no-empty
} catch {
/* empty */
}
break;
case ActionTypes.USER_FROM_CARD_REMOVE:
@@ -132,7 +156,9 @@ export default class extends BaseModel {
case ActionTypes.USER_FROM_CARD_REMOVE_HANDLE:
try {
Card.withId(payload.cardMembership.cardId).users.remove(payload.cardMembership.userId);
} catch {} // eslint-disable-line no-empty
} catch {
/* empty */
}
break;
case ActionTypes.BOARD_FETCH__SUCCESS:
@@ -148,6 +174,22 @@ export default class extends BaseModel {
Card.withId(cardId).labels.add(labelId);
});
break;
case ActionTypes.LABEL_FROM_CARD_CREATE:
Card.withId(payload.cardId).labels.add(payload.label.id);
break;
case ActionTypes.LABEL_FROM_CARD_CREATE__SUCCESS: {
const cardModel = Card.withId(payload.cardLabel.cardId);
cardModel.labels.remove(payload.localId);
cardModel.labels.add(payload.label.id);
break;
}
case ActionTypes.LABEL_FROM_CARD_CREATE__FAILURE:
Card.withId(payload.cardId).labels.remove(payload.localId);
break;
case ActionTypes.LABEL_TO_CARD_ADD:
Card.withId(payload.cardId).labels.add(payload.id);
@@ -157,7 +199,9 @@ export default class extends BaseModel {
case ActionTypes.LABEL_TO_CARD_ADD_HANDLE:
try {
Card.withId(payload.cardLabel.cardId).labels.add(payload.cardLabel.labelId);
} catch {} // eslint-disable-line no-empty
} catch {
/* empty */
}
break;
case ActionTypes.LABEL_FROM_CARD_REMOVE:
@@ -168,16 +212,74 @@ export default class extends BaseModel {
case ActionTypes.LABEL_FROM_CARD_REMOVE_HANDLE:
try {
Card.withId(payload.cardLabel.cardId).labels.remove(payload.cardLabel.labelId);
} catch {} // eslint-disable-line no-empty
} catch {
/* empty */
}
break;
case ActionTypes.LIST_SORT__SUCCESS:
case ActionTypes.LIST_SORT_HANDLE:
case ActionTypes.NOTIFICATION_CREATE_HANDLE:
case ActionTypes.LIST_CARDS_MOVE__SUCCESS:
case ActionTypes.LIST_DELETE__SUCCESS:
case ActionTypes.CARDS_UPDATE_HANDLE:
payload.cards.forEach((card) => {
Card.upsert(card);
});
break;
case ActionTypes.LIST_CARDS_MOVE: {
const listChangedAt = new Date();
payload.cardIds.forEach((cardId) => {
const cardModel = Card.withId(cardId);
cardModel.update({
listChangedAt,
listId: payload.nextId,
prevListId: cardModel.listId,
});
});
break;
}
case ActionTypes.LIST_DELETE: {
const listChangedAt = new Date();
payload.cardIds.forEach((cardId) => {
Card.withId(cardId).update({
listChangedAt,
listId: payload.trashId,
});
});
break;
}
case ActionTypes.LIST_DELETE_HANDLE:
if (payload.cards) {
payload.cards.forEach((card) => {
Card.upsert(card);
});
}
break;
case ActionTypes.CARDS_FETCH__SUCCESS:
payload.cards.forEach((card) => {
const cardModel = Card.withId(card.id);
if (cardModel) {
cardModel.deleteWithRelated();
}
Card.upsert(card);
});
payload.cardMemberships.forEach(({ cardId, userId }) => {
Card.withId(cardId).users.add(userId);
});
payload.cardLabels.forEach(({ cardId, labelId }) => {
Card.withId(cardId).labels.add(labelId);
});
break;
case ActionTypes.CARD_CREATE:
case ActionTypes.CARD_UPDATE__SUCCESS:
@@ -185,23 +287,26 @@ export default class extends BaseModel {
break;
case ActionTypes.CARD_CREATE__SUCCESS:
Card.withId(payload.localId).delete();
Card.withId(payload.localId).deleteWithClearable();
Card.upsert(payload.card);
break;
case ActionTypes.CARD_CREATE_HANDLE: {
const cardModel = Card.upsert(payload.card);
case ActionTypes.CARD_CREATE__FAILURE:
Card.withId(payload.localId).deleteWithClearable();
payload.cardMemberships.forEach(({ userId }) => {
cardModel.users.add(userId);
break;
case ActionTypes.CARD_CREATE_HANDLE:
Card.upsert(payload.card);
payload.cardMemberships.forEach(({ cardId, userId }) => {
Card.withId(cardId).users.add(userId);
});
payload.cardLabels.forEach(({ labelId }) => {
cardModel.labels.add(labelId);
payload.cardLabels.forEach(({ cardId, labelId }) => {
Card.withId(cardId).labels.add(labelId);
});
break;
}
case ActionTypes.CARD_UPDATE: {
const cardModel = Card.withId(payload.id);
@@ -209,22 +314,17 @@ export default class extends BaseModel {
if (payload.data.boardId && payload.data.boardId !== cardModel.boardId) {
cardModel.deleteWithRelated();
} else {
cardModel.update({
...payload.data,
...(payload.data.dueDate === null && {
isDueDateCompleted: null,
}),
...(payload.data.dueDate &&
!cardModel.dueDate && {
isDueDateCompleted: false,
}),
});
if (payload.data.listId && payload.data.listId !== cardModel.listId) {
payload.data.listChangedAt = new Date(); // eslint-disable-line no-param-reassign
}
cardModel.update(payload.data);
}
break;
}
case ActionTypes.CARD_UPDATE_HANDLE:
if (payload.isFetched) {
if (payload.card.boardId === null || payload.isFetched) {
const cardModel = Card.withId(payload.card.id);
if (cardModel) {
@@ -232,7 +332,9 @@ export default class extends BaseModel {
}
}
Card.upsert(payload.card);
if (payload.card.boardId !== null) {
Card.upsert(payload.card);
}
if (payload.cardMemberships) {
payload.cardMemberships.forEach(({ cardId, userId }) => {
@@ -247,35 +349,13 @@ export default class extends BaseModel {
}
break;
case ActionTypes.CARD_DUPLICATE: {
const cardModel = Card.withId(payload.id);
const nextCardModel = Card.upsert({
...pick(cardModel.ref, [
'boardId',
'listId',
'position',
'name',
'description',
'dueDate',
'isDueDateCompleted',
'stopwatch',
]),
...payload.card,
});
cardModel.users.toRefArray().forEach(({ id }) => {
nextCardModel.users.add(id);
});
cardModel.labels.toRefArray().forEach(({ id }) => {
nextCardModel.labels.add(id);
});
case ActionTypes.CARD_DUPLICATE:
Card.withId(payload.id).duplicate(payload.localId, payload.data);
break;
}
case ActionTypes.CARD_DUPLICATE__SUCCESS: {
Card.withId(payload.localId).deleteWithRelated();
const cardModel = Card.upsert(payload.card);
payload.cardMemberships.forEach(({ userId }) => {
@@ -288,6 +368,10 @@ export default class extends BaseModel {
break;
}
case ActionTypes.CARD_DUPLICATE__FAILURE:
Card.withId(payload.localId).deleteWithRelated();
break;
case ActionTypes.CARD_DELETE:
Card.withId(payload.id).deleteWithRelated();
@@ -302,6 +386,22 @@ export default class extends BaseModel {
break;
}
case ActionTypes.COMMENTS_FETCH:
Card.withId(payload.cardId).update({
isCommentsFetching: true,
});
break;
case ActionTypes.COMMENTS_FETCH__SUCCESS:
Card.withId(payload.cardId).update({
isCommentsFetching: false,
isAllCommentsFetched: payload.comments.length < Config.COMMENTS_LIMIT,
...(payload.comments.length > 0 && {
lastCommentId: payload.comments[payload.comments.length - 1].id,
}),
});
break;
case ActionTypes.ACTIVITIES_FETCH:
Card.withId(payload.cardId).update({
isActivitiesFetching: true,
@@ -312,53 +412,34 @@ export default class extends BaseModel {
Card.withId(payload.cardId).update({
isActivitiesFetching: false,
isAllActivitiesFetched: payload.activities.length < Config.ACTIVITIES_LIMIT,
...(payload.activities.length > 0 && {
lastActivityId: payload.activities[payload.activities.length - 1].id,
}),
});
break;
case ActionTypes.ACTIVITIES_DETAILS_TOGGLE: {
const cardModel = Card.withId(payload.cardId);
cardModel.isActivitiesDetailsVisible = payload.isVisible;
if (payload.isVisible) {
cardModel.isActivitiesDetailsFetching = true;
}
break;
}
case ActionTypes.ACTIVITIES_DETAILS_TOGGLE__SUCCESS: {
const cardModel = Card.withId(payload.cardId);
cardModel.update({
isAllActivitiesFetched: payload.activities.length < Config.ACTIVITIES_LIMIT,
isActivitiesDetailsFetching: false,
});
cardModel.deleteActivities();
break;
}
default:
}
}
getOrderedTasksQuerySet() {
return this.tasks.orderBy('position');
getTaskListsQuerySet() {
return this.taskLists.orderBy(['position', 'id.length', 'id']);
}
getOrderedAttachmentsQuerySet() {
return this.attachments.orderBy('createdAt', false);
getAttachmentsQuerySet() {
return this.attachments.orderBy(['id.length', 'id'], ['desc', 'desc']);
}
getFilteredOrderedInCardActivitiesQuerySet() {
const filter = {
isInCard: true,
};
getCustomFieldGroupsQuerySet() {
return this.customFieldGroups.orderBy(['position', 'id.length', 'id']);
}
if (!this.isActivitiesDetailsVisible) {
filter.type = ActivityTypes.COMMENT_CARD;
}
getCommentsQuerySet() {
return this.comments.orderBy(['id.length', 'id'], ['desc', 'desc']);
}
return this.activities.filter(filter).orderBy('createdAt', false);
getActivitiesQuerySet() {
return this.activities.orderBy(['id.length', 'id'], ['desc', 'desc']);
}
getUnreadNotificationsQuerySet() {
@@ -367,8 +448,143 @@ export default class extends BaseModel {
});
}
isAvailableForUser(userId) {
return this.board && this.board.isAvailableForUser(userId);
getShownOnFrontOfCardTaskListsModelArray() {
return this.getTaskListsQuerySet()
.toModelArray()
.filter((taskListModel) => taskListModel.showOnFrontOfCard);
}
getCommentsModelArray() {
if (this.isAllCommentsFetched === null) {
return [];
}
const commentModels = this.getCommentsQuerySet().toModelArray();
if (this.lastCommentId && this.isAllCommentsFetched === false) {
return commentModels.filter((commentModel) => {
if (commentModel.id.length > this.lastCommentId.length) {
return true;
}
if (commentModel.id.length < this.lastCommentId.length) {
return false;
}
return commentModel.id >= this.lastCommentId;
});
}
return commentModels;
}
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;
}
hasUserWithId(userId) {
return this.cardusersSet
.filter({
toUserId: userId,
})
.exists();
}
isAvailableForUser(userModel) {
return !!this.list && this.list.isAvailableForUser(userModel);
}
duplicate(id, data, rootId) {
if (rootId === undefined) {
rootId = id; // eslint-disable-line no-param-reassign
}
const cardModel = this.getClass().create({
id,
boardId: this.boardId,
listId: this.listId,
creatorUserId: this.creatorUserId,
prevListId: this.prevListId,
coverAttachmentId: this.coverAttachmentId && `${this.coverAttachmentId}-${rootId}`,
type: this.type,
position: this.position,
name: this.name,
description: this.description,
dueDate: this.dueDate,
stopwatch: this.stopwatch,
...data,
});
this.users.toRefArray().forEach((user) => {
cardModel.users.add(user.id);
});
this.labels.toRefArray().forEach((label) => {
cardModel.labels.add(label.id);
});
this.taskLists.toModelArray().forEach((taskListModel) => {
taskListModel.duplicate(`${taskListModel.id}-${rootId}`, {
cardId: cardModel.id,
});
});
this.attachments.toModelArray().forEach((attachmentModel) => {
attachmentModel.duplicate(`${attachmentModel.id}-${rootId}`, {
cardId: cardModel.id,
...(data.creatorUserId && {
creatorUserId: data.creatorUserId,
}),
});
});
this.customFieldGroups.toModelArray().forEach((customFieldGroupModel) => {
customFieldGroupModel.duplicate(
`${customFieldGroupModel.id}-${rootId}`,
{
cardId: cardModel.id,
},
rootId,
);
});
this.customFieldValues.toModelArray().forEach((customFieldValueModel) => {
const customFieldValueData = {
cardId: cardModel.id,
};
if (customFieldValueModel.customFieldGroup.cardId) {
customFieldValueData.customFieldGroupId = `${customFieldValueModel.customFieldGroupId}-${rootId}`;
if (customFieldValueModel.customField.customFieldGroupId) {
customFieldValueData.customFieldId = `${customFieldValueModel.customFieldId}-${rootId}`;
}
}
customFieldValueModel.duplicate(customFieldValueData);
});
return cardModel;
}
deleteClearable() {
@@ -376,23 +592,22 @@ export default class extends BaseModel {
this.labels.clear();
}
deleteActivities() {
this.activities.toModelArray().forEach((activityModel) => {
if (activityModel.notification) {
activityModel.update({
isInCard: false,
});
} else {
activityModel.delete();
}
});
}
deleteRelated() {
this.deleteClearable();
this.tasks.delete();
this.taskLists.toModelArray().forEach((taskListModel) => {
taskListModel.deleteWithRelated();
});
this.attachments.delete();
this.deleteActivities();
this.customFieldGroups.toModelArray().forEach((customFieldGroupModel) => {
customFieldGroupModel.deleteWithRelated();
});
this.customFieldValues.delete();
this.comments.delete();
this.activities.delete();
}
deleteWithClearable() {

81
client/src/models/Comment.js Executable file
View File

@@ -0,0 +1,81 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';
export default class extends BaseModel {
static modelName = 'Comment';
static fields = {
id: attr(),
text: attr(),
createdAt: attr({
getDefault: () => new Date(),
}),
cardId: fk({
to: 'Card',
as: 'card',
relatedName: 'comments',
}),
userId: fk({
to: 'User',
as: 'user',
relatedName: 'comments',
}),
};
static reducer({ type, payload }, Comment) {
switch (type) {
case ActionTypes.SOCKET_RECONNECT_HANDLE:
Comment.all().delete();
break;
case ActionTypes.COMMENTS_FETCH__SUCCESS:
payload.comments.forEach((comment) => {
Comment.upsert(comment);
});
break;
case ActionTypes.COMMENT_CREATE:
case ActionTypes.COMMENT_CREATE_HANDLE:
case ActionTypes.COMMENT_UPDATE__SUCCESS:
case ActionTypes.COMMENT_UPDATE_HANDLE:
Comment.upsert(payload.comment);
break;
case ActionTypes.COMMENT_CREATE__SUCCESS:
Comment.withId(payload.localId).delete();
Comment.upsert(payload.comment);
break;
case ActionTypes.COMMENT_CREATE__FAILURE:
Comment.withId(payload.localId).delete();
break;
case ActionTypes.COMMENT_UPDATE:
Comment.withId(payload.id).update(payload.data);
break;
case ActionTypes.COMMENT_DELETE:
Comment.withId(payload.id).delete();
break;
case ActionTypes.COMMENT_DELETE__SUCCESS:
case ActionTypes.COMMENT_DELETE_HANDLE: {
const commentModel = Comment.withId(payload.comment.id);
if (commentModel) {
commentModel.delete();
}
break;
}
default:
}
}
}

View File

@@ -0,0 +1,125 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';
export default class extends BaseModel {
static modelName = 'CustomField';
static fields = {
id: attr(),
position: attr(),
name: attr(),
showOnFrontOfCard: attr(),
baseCustomFieldGroupId: fk({
to: 'BaseCustomFieldGroup',
as: 'baseGroup',
relatedName: 'customFields',
}),
customFieldGroupId: fk({
to: 'CustomFieldGroup',
as: 'group',
relatedName: 'customFields',
}),
};
static reducer({ type, payload }, CustomField) {
switch (type) {
case ActionTypes.LOCATION_CHANGE_HANDLE:
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.USER_UPDATE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
if (payload.customFields) {
payload.customFields.forEach((customField) => {
CustomField.upsert(customField);
});
}
break;
case ActionTypes.SOCKET_RECONNECT_HANDLE:
CustomField.all().delete();
if (payload.customFields) {
payload.customFields.forEach((customField) => {
CustomField.upsert(customField);
});
}
break;
case ActionTypes.PROJECT_CREATE_HANDLE:
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.customFields.forEach((customField) => {
CustomField.upsert(customField);
});
break;
case ActionTypes.CUSTOM_FIELD_CREATE:
case ActionTypes.CUSTOM_FIELD_CREATE_HANDLE:
case ActionTypes.CUSTOM_FIELD_UPDATE__SUCCESS:
case ActionTypes.CUSTOM_FIELD_UPDATE_HANDLE:
CustomField.upsert(payload.customField);
break;
case ActionTypes.CUSTOM_FIELD_CREATE__SUCCESS:
CustomField.withId(payload.localId).delete();
CustomField.upsert(payload.customField);
break;
case ActionTypes.CUSTOM_FIELD_CREATE__FAILURE:
CustomField.withId(payload.localId).delete();
break;
case ActionTypes.CUSTOM_FIELD_UPDATE:
CustomField.withId(payload.id).update(payload.data);
break;
case ActionTypes.CUSTOM_FIELD_DELETE:
CustomField.withId(payload.id).deleteWithRelated();
break;
case ActionTypes.CUSTOM_FIELD_DELETE__SUCCESS:
case ActionTypes.CUSTOM_FIELD_DELETE_HANDLE: {
const customFieldModel = CustomField.withId(payload.customField.id);
if (customFieldModel) {
customFieldModel.deleteWithRelated();
}
break;
}
default:
}
}
duplicate(id, data) {
return this.getClass().create({
id,
baseCustomFieldGroupId: this.baseCustomFieldGroupId,
customFieldGroupId: this.customFieldGroupId,
position: this.position,
name: this.name,
showOnFrontOfCard: this.showOnFrontOfCard,
...data,
});
}
deleteRelated() {
this.customFieldValues.delete();
}
deleteWithRelated() {
this.deleteRelated();
this.delete();
}
}

View File

@@ -0,0 +1,163 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';
export default class extends BaseModel {
static modelName = 'CustomFieldGroup';
static fields = {
id: attr(),
position: attr(),
name: attr(),
boardId: fk({
to: 'Board',
as: 'board',
relatedName: 'customFieldGroups',
}),
cardId: fk({
to: 'Card',
as: 'card',
relatedName: 'customFieldGroups',
}),
baseCustomFieldGroupId: fk({
to: 'BaseCustomFieldGroup',
as: 'baseCustomFieldGroup',
relatedName: 'customFieldGroups',
}),
};
static reducer({ type, payload }, CustomFieldGroup) {
switch (type) {
case ActionTypes.LOCATION_CHANGE_HANDLE:
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.USER_UPDATE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
if (payload.customFieldGroups) {
payload.customFieldGroups.forEach((customFieldGroup) => {
CustomFieldGroup.upsert(customFieldGroup);
});
}
break;
case ActionTypes.SOCKET_RECONNECT_HANDLE:
CustomFieldGroup.all().delete();
if (payload.customFieldGroups) {
payload.customFieldGroups.forEach((customFieldGroup) => {
CustomFieldGroup.upsert(customFieldGroup);
});
}
break;
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.customFieldGroups.forEach((customFieldGroup) => {
CustomFieldGroup.upsert(customFieldGroup);
});
break;
case ActionTypes.CUSTOM_FIELD_GROUP_CREATE:
case ActionTypes.CUSTOM_FIELD_GROUP_CREATE_HANDLE:
case ActionTypes.CUSTOM_FIELD_GROUP_UPDATE__SUCCESS:
case ActionTypes.CUSTOM_FIELD_GROUP_UPDATE_HANDLE:
CustomFieldGroup.upsert(payload.customFieldGroup);
break;
case ActionTypes.CUSTOM_FIELD_GROUP_CREATE__SUCCESS:
CustomFieldGroup.withId(payload.localId).delete();
CustomFieldGroup.upsert(payload.customFieldGroup);
break;
case ActionTypes.CUSTOM_FIELD_GROUP_CREATE__FAILURE:
CustomFieldGroup.withId(payload.localId).delete();
break;
case ActionTypes.CUSTOM_FIELD_GROUP_UPDATE:
CustomFieldGroup.withId(payload.id).update(payload.data);
break;
case ActionTypes.CUSTOM_FIELD_GROUP_DELETE:
CustomFieldGroup.withId(payload.id).deleteWithRelated();
break;
case ActionTypes.CUSTOM_FIELD_GROUP_DELETE__SUCCESS:
case ActionTypes.CUSTOM_FIELD_GROUP_DELETE_HANDLE: {
const customFieldGroupModel = CustomFieldGroup.withId(payload.customFieldGroup.id);
if (customFieldGroupModel) {
customFieldGroupModel.deleteWithRelated();
}
break;
}
default:
}
}
getCustomFieldsQuerySet() {
return this.customFields.orderBy(['position', 'id.length', 'id']);
}
getCustomFieldsModelArray() {
if (this.baseCustomFieldGroupId) {
return this.baseCustomFieldGroup.getCustomFieldsQuerySet().toModelArray();
}
return this.getCustomFieldsQuerySet().toModelArray();
}
getShownOnFrontOfCardCustomFieldsModelArray() {
return this.getCustomFieldsModelArray().filter(
(customFieldModel) => customFieldModel.showOnFrontOfCard,
);
}
duplicate(id, data, rootId) {
if (rootId === undefined) {
rootId = id; // eslint-disable-line no-param-reassign
}
const customFieldGroupModel = this.getClass().create({
id,
boardId: this.boardId,
cardId: this.cardId,
baseCustomFieldGroupId: this.baseCustomFieldGroupId,
position: this.position,
name: this.name,
...data,
});
this.customFields.toModelArray().forEach((customFieldModel) => {
customFieldModel.duplicate(
`${customFieldModel.id}-${rootId}`,
{
customFieldGroupId: customFieldGroupModel.id,
},
rootId,
);
});
return customFieldGroupModel;
}
deleteRelated() {
this.customFields.delete();
this.customFieldValues.delete();
}
deleteWithRelated() {
this.deleteRelated();
this.delete();
}
}

View File

@@ -0,0 +1,126 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';
export const buildCustomFieldValueId = (customFieldValue) =>
JSON.stringify({
cardId: customFieldValue.cardId,
customFieldGroupId: customFieldValue.customFieldGroupId,
customFieldId: customFieldValue.customFieldId,
});
const prepareCustomFieldValue = (customFieldValue) => ({
...customFieldValue,
id: buildCustomFieldValueId(customFieldValue),
});
export default class extends BaseModel {
static modelName = 'CustomFieldValue';
static fields = {
id: attr(),
content: attr(),
cardId: fk({
to: 'Card',
as: 'card',
relatedName: 'customFieldValues',
}),
customFieldGroupId: fk({
to: 'CustomFieldGroup',
as: 'customFieldGroup',
relatedName: 'customFieldValues',
}),
customFieldId: fk({
to: 'CustomField',
as: 'customField',
relatedName: 'customFieldValues',
}),
};
static reducer({ type, payload }, CustomFieldValue) {
switch (type) {
case ActionTypes.LOCATION_CHANGE_HANDLE:
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.USER_UPDATE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
if (payload.customFieldValues) {
payload.customFieldValues.forEach((customFieldValue) => {
CustomFieldValue.upsert(prepareCustomFieldValue(customFieldValue));
});
}
break;
case ActionTypes.SOCKET_RECONNECT_HANDLE:
CustomFieldValue.all().delete();
if (payload.customFieldValues) {
payload.customFieldValues.forEach((customFieldValue) => {
CustomFieldValue.upsert(prepareCustomFieldValue(customFieldValue));
});
}
break;
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.customFieldValues.forEach((customFieldValue) => {
CustomFieldValue.upsert(prepareCustomFieldValue(customFieldValue));
});
break;
case ActionTypes.CUSTOM_FIELD_VALUE_UPDATE:
case ActionTypes.CUSTOM_FIELD_VALUE_UPDATE__SUCCESS:
case ActionTypes.CUSTOM_FIELD_VALUE_UPDATE_HANDLE:
CustomFieldValue.upsert(prepareCustomFieldValue(payload.customFieldValue));
break;
case ActionTypes.CUSTOM_FIELD_VALUE_DELETE: {
const customFieldValueModel = CustomFieldValue.withId(payload.id);
if (customFieldValueModel) {
customFieldValueModel.delete();
}
break;
}
case ActionTypes.CUSTOM_FIELD_VALUE_DELETE__SUCCESS:
case ActionTypes.CUSTOM_FIELD_VALUE_DELETE_HANDLE: {
const customFieldValueModel = CustomFieldValue.withId(
buildCustomFieldValueId(payload.customFieldValue),
);
if (customFieldValueModel) {
customFieldValueModel.delete();
}
break;
}
default:
}
}
duplicate(data) {
const customFieldValue = {
cardId: this.cardId,
customFieldGroupId: this.customFieldGroupId,
customFieldId: this.customFieldId,
content: this.content,
...data,
};
return this.getClass().create({
id: buildCustomFieldValueId(customFieldValue),
...customFieldValue,
});
}
}

View File

@@ -1,3 +1,8 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
@@ -22,6 +27,8 @@ export default class extends BaseModel {
switch (type) {
case ActionTypes.LOCATION_CHANGE_HANDLE:
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.USER_UPDATE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
if (payload.labels) {
@@ -48,6 +55,7 @@ export default class extends BaseModel {
break;
case ActionTypes.LABEL_CREATE:
case ActionTypes.LABEL_FROM_CARD_CREATE:
case ActionTypes.LABEL_CREATE_HANDLE:
case ActionTypes.LABEL_UPDATE__SUCCESS:
case ActionTypes.LABEL_UPDATE_HANDLE:
@@ -55,16 +63,22 @@ export default class extends BaseModel {
break;
case ActionTypes.LABEL_CREATE__SUCCESS:
case ActionTypes.LABEL_FROM_CARD_CREATE__SUCCESS:
Label.withId(payload.localId).delete();
Label.upsert(payload.label);
break;
case ActionTypes.LABEL_CREATE__FAILURE:
case ActionTypes.LABEL_FROM_CARD_CREATE__FAILURE:
Label.withId(payload.localId).delete();
break;
case ActionTypes.LABEL_UPDATE:
Label.withId(payload.id).update(payload.data);
break;
case ActionTypes.LABEL_DELETE:
Label.withId(payload.id).delete();
Label.withId(payload.id).deleteWithRelated();
break;
case ActionTypes.LABEL_DELETE__SUCCESS:
@@ -72,7 +86,7 @@ export default class extends BaseModel {
const labelModel = Label.withId(payload.label.id);
if (labelModel) {
labelModel.delete();
labelModel.deleteWithRelated();
}
break;
@@ -81,15 +95,24 @@ export default class extends BaseModel {
}
}
static findLabelsFromText(filterText, labels) {
const selectLabel = filterText.toLocaleLowerCase();
const matchingLabels = labels.filter((label) =>
label.name ? label.name.toLocaleLowerCase().startsWith(selectLabel) : false,
);
if (matchingLabels.length === 1) {
// Appens the user to the filter
return matchingLabels[0].id;
deleteRelated() {
this.board.cards.toModelArray().forEach((cardModel) => {
try {
cardModel.labels.remove(this.id);
} catch {
/* empty */
}
});
try {
this.board.filterLabels.remove(this.id);
} catch {
/* empty */
}
return null;
}
deleteWithRelated() {
this.deleteRelated();
this.delete();
}
}

View File

@@ -1,17 +1,51 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
import User from './User';
import buildSearchParts from '../utils/build-search-parts';
import { isListFinite } from '../utils/record-helpers';
import ActionTypes from '../constants/ActionTypes';
import Config from '../constants/Config';
import { ListSortFieldNames, ListTypes, SortOrders } from '../constants/Enums';
const POSITION_BY_LIST_TYPE = {
[ListTypes.ARCHIVE]: Number.MAX_SAFE_INTEGER - 1,
[ListTypes.TRASH]: Number.MAX_SAFE_INTEGER,
};
const prepareList = (list) => {
if (list.position === undefined) {
return list;
}
return {
...list,
position: list.position === null ? POSITION_BY_LIST_TYPE[list.type] : list.position,
};
};
export default class extends BaseModel {
static modelName = 'List';
static fields = {
id: attr(),
type: attr(),
position: attr(),
name: attr(),
color: attr(),
lastCard: attr({
getDefault: () => null,
}),
isCardsFetching: attr({
getDefault: () => false,
}),
isAllCardsFetched: attr({
getDefault: () => null,
}),
boardId: fk({
to: 'Board',
as: 'board',
@@ -23,11 +57,13 @@ export default class extends BaseModel {
switch (type) {
case ActionTypes.LOCATION_CHANGE_HANDLE:
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.USER_UPDATE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
if (payload.lists) {
payload.lists.forEach((list) => {
List.upsert(list);
List.upsert(prepareList(list));
});
}
@@ -37,14 +73,28 @@ export default class extends BaseModel {
if (payload.lists) {
payload.lists.forEach((list) => {
List.upsert(list);
List.upsert(prepareList(list));
});
}
break;
case ActionTypes.USER_TO_BOARD_FILTER_ADD:
case ActionTypes.USER_FROM_BOARD_FILTER_REMOVE:
case ActionTypes.IN_BOARD_SEARCH:
case ActionTypes.LABEL_TO_BOARD_FILTER_ADD:
case ActionTypes.LABEL_FROM_BOARD_FILTER_REMOVE:
if (payload.currentListId) {
List.withId(payload.currentListId).update({
lastCard: null,
isCardsFetching: false,
isAllCardsFetched: null,
});
}
break;
case ActionTypes.BOARD_FETCH__SUCCESS:
payload.lists.forEach((list) => {
List.upsert(list);
List.upsert(prepareList(list));
});
break;
@@ -53,94 +103,192 @@ export default class extends BaseModel {
case ActionTypes.LIST_UPDATE__SUCCESS:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.LIST_SORT__SUCCESS:
case ActionTypes.LIST_SORT_HANDLE:
List.upsert(payload.list);
case ActionTypes.LIST_CARDS_MOVE__SUCCESS:
case ActionTypes.LIST_CLEAR__SUCCESS:
List.upsert(prepareList(payload.list));
break;
case ActionTypes.LIST_CREATE__SUCCESS:
List.withId(payload.localId).delete();
List.upsert(payload.list);
List.upsert(prepareList(payload.list));
break;
case ActionTypes.LIST_CREATE__FAILURE:
List.withId(payload.localId).delete();
break;
case ActionTypes.LIST_UPDATE:
List.withId(payload.id).update(payload.data);
break;
case ActionTypes.LIST_DELETE:
List.withId(payload.id).deleteWithRelated();
case ActionTypes.LIST_SORT:
List.withId(payload.id).sortCards(payload.data);
break;
case ActionTypes.LIST_DELETE__SUCCESS:
case ActionTypes.LIST_CLEAR:
List.withId(payload.id).deleteRelated();
break;
case ActionTypes.LIST_CLEAR_HANDLE: {
const listModel = List.withId(payload.list.id);
if (listModel) {
listModel.deleteRelated();
}
List.upsert(prepareList(payload.list));
break;
}
case ActionTypes.LIST_DELETE:
List.withId(payload.id).delete();
break;
case ActionTypes.LIST_DELETE__SUCCESS: {
const listModel = List.withId(payload.list.id);
if (listModel) {
listModel.delete();
}
break;
}
case ActionTypes.LIST_DELETE_HANDLE: {
const listModel = List.withId(payload.list.id);
if (listModel) {
listModel.deleteWithRelated();
if (payload.cards) {
listModel.delete();
} else {
listModel.deleteWithRelated();
}
}
break;
}
case ActionTypes.CARDS_FETCH:
List.withId(payload.listId).update({
isCardsFetching: true,
});
break;
case ActionTypes.CARDS_FETCH__SUCCESS: {
const lastCard = payload.cards[payload.cards.length - 1];
List.withId(payload.listId).update({
isCardsFetching: false,
isAllCardsFetched: payload.cards.length < Config.CARDS_LIMIT,
...(lastCard && {
lastCard: {
listChangedAt: lastCard.listChangedAt,
id: lastCard.id,
},
}),
});
break;
}
default:
}
}
getOrderedCardsQuerySet() {
return this.cards.orderBy('position');
getCardsQuerySet() {
const orderByArgs = isListFinite(this)
? [['position', 'id.length', 'id']]
: [
['listChangedAt', 'id.length', 'id'],
['desc', 'desc', 'desc'],
];
return this.cards.orderBy(...orderByArgs);
}
getFilteredOrderedCardsModelArray() {
let cardModels = this.getOrderedCardsQuerySet().toModelArray();
getCardsModelArray() {
const isFinite = isListFinite(this);
const { filterText } = this.board;
if (!isFinite && this.isAllCardsFetched === null) {
return [];
}
if (filterText !== '') {
let re = null;
const posSpace = filterText.indexOf(' ');
const cardModels = this.getCardsQuerySet().toModelArray();
if (filterText.startsWith('/')) {
re = new RegExp(filterText.substring(1), 'i');
}
let doRegularSearch = true;
if (re) {
cardModels = cardModels.filter(
(cardModel) => re.test(cardModel.name) || re.test(cardModel?.description),
);
doRegularSearch = false;
} else if (filterText.startsWith('!') && posSpace > 0) {
const creatorUserId = User.findUsersFromText(
filterText.substring(1, posSpace),
this.board.memberships.toModelArray().map((membership) => membership.user),
);
if (creatorUserId != null) {
doRegularSearch = false;
cardModels = cardModels.filter((cardModel) => cardModel.creatorUser.id === creatorUserId);
if (!isFinite && this.lastCard && this.isAllCardsFetched === false) {
return cardModels.filter((cardModel) => {
if (cardModel.listChangedAt > this.lastCard.listChangedAt) {
return true;
}
}
if (doRegularSearch) {
const lowerCasedFilter = filterText.toLocaleLowerCase();
cardModels = cardModels.filter(
(cardModel) =>
cardModel.name.toLocaleLowerCase().indexOf(lowerCasedFilter) >= 0 ||
cardModel.description?.toLocaleLowerCase().indexOf(lowerCasedFilter) >= 0,
);
if (cardModel.listChangedAt < this.lastCard.listChangedAt) {
return false;
}
if (cardModel.id.length > this.lastCard.id.length) {
return true;
}
if (cardModel.id.length < this.lastCard.id.length) {
return false;
}
return cardModel.id >= this.lastCard.id;
});
}
return cardModels;
}
getFilteredCardsModelArray() {
let cardModels = this.getCardsModelArray();
if (cardModels.length === 0) {
return cardModels;
}
if (this.board.search) {
if (this.board.search.startsWith('/')) {
let searchRegex;
try {
searchRegex = new RegExp(this.board.search.substring(1), 'i');
} catch {
return [];
}
if (searchRegex) {
cardModels = cardModels.filter(
(cardModel) =>
searchRegex.test(cardModel.name) ||
(cardModel.description && searchRegex.test(cardModel.description)),
);
}
} else {
const searchParts = buildSearchParts(this.board.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.board.filterUsers.toRefArray().map((user) => user.id);
const filterLabelIds = this.board.filterLabels.toRefArray().map((label) => label.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.board.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));
});
}
@@ -148,6 +296,51 @@ export default class extends BaseModel {
return cardModels;
}
isAvailableForUser(userModel) {
return !!this.board && this.board.isAvailableForUser(userModel);
}
sortCards(options) {
const cardModels = this.getCardsQuerySet().toModelArray();
switch (options.fieldName) {
case ListSortFieldNames.NAME:
cardModels.sort((card1, card2) => card1.name.localeCompare(card2.name));
break;
case ListSortFieldNames.DUE_DATE:
cardModels.sort((card1, card2) => {
if (card1.dueDate === null) {
return 1;
}
if (card2.dueDate === null) {
return -1;
}
return card1.dueDate - card2.dueDate;
});
break;
case ListSortFieldNames.CREATED_AT:
cardModels.sort((card1, card2) => card1.createdAt - card2.createdAt);
break;
default:
break;
}
if (options.order === SortOrders.DESC) {
cardModels.reverse();
}
cardModels.forEach((cardModel, index) => {
cardModel.update({
position: Config.POSITION_GAP * (index + 1),
});
});
}
deleteRelated() {
this.cards.toModelArray().forEach((cardModel) => {
cardModel.deleteWithRelated();

View File

@@ -1,3 +1,8 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk, oneToOne } from 'redux-orm';
import BaseModel from './BaseModel';
@@ -16,11 +21,25 @@ export default class extends BaseModel {
as: 'user',
relatedName: 'notifications',
}),
creatorUserId: fk({
to: 'User',
as: 'creatorUser',
relatedName: 'createdNotifications',
}),
boardId: fk({
to: 'Board',
as: 'board',
relatedName: 'notifications',
}),
cardId: fk({
to: 'Card',
as: 'card',
relatedName: 'notifications',
}),
commentId: oneToOne({
to: 'Comment',
as: 'comment',
}),
activityId: oneToOne({
to: 'Activity',
as: 'activity',
@@ -30,11 +49,13 @@ export default class extends BaseModel {
static reducer({ type, payload }, Notification) {
switch (type) {
case ActionTypes.LOCATION_CHANGE_HANDLE:
case ActionTypes.USER_UPDATE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
if (payload.deletedNotifications) {
payload.deletedNotifications.forEach((notification) => {
Notification.withId(notification.id).deleteWithRelated();
if (payload.notificationsToDelete) {
payload.notificationsToDelete.forEach((notification) => {
Notification.withId(notification.id).delete();
});
}
@@ -52,13 +73,27 @@ export default class extends BaseModel {
Notification.upsert(notification);
});
break;
case ActionTypes.ALL_NOTIFICATIONS_DELETE:
Notification.all().delete();
break;
case ActionTypes.ALL_NOTIFICATIONS_DELETE__SUCCESS:
payload.notifications.forEach((notification) => {
const notificationModel = Notification.withId(notification.id);
if (notificationModel) {
notificationModel.delete();
}
});
break;
case ActionTypes.NOTIFICATION_CREATE_HANDLE:
Notification.upsert(payload.notification);
break;
case ActionTypes.NOTIFICATION_DELETE:
Notification.withId(payload.id).deleteWithRelated();
Notification.withId(payload.id).delete();
break;
case ActionTypes.NOTIFICATION_DELETE__SUCCESS:
@@ -66,7 +101,7 @@ export default class extends BaseModel {
const notificationModel = Notification.withId(payload.notification.id);
if (notificationModel) {
notificationModel.deleteWithRelated();
notificationModel.delete();
}
break;
@@ -74,15 +109,4 @@ export default class extends BaseModel {
default:
}
}
deleteRelated() {
if (this.action && !this.action.isInCard) {
this.action.delete();
}
}
deleteWithRelated() {
this.deleteRelated();
this.delete();
}
}

View File

@@ -0,0 +1,117 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';
export default class extends BaseModel {
static modelName = 'NotificationService';
static fields = {
id: attr(),
url: attr(),
format: attr(),
isTesting: attr({
getDefault: () => false,
}),
userId: fk({
to: 'User',
as: 'user',
relatedName: 'notificationServices',
}),
boardId: fk({
to: 'Board',
as: 'board',
relatedName: 'notificationServices',
}),
};
static reducer({ type, payload }, NotificationService) {
switch (type) {
case ActionTypes.SOCKET_RECONNECT_HANDLE:
NotificationService.all().delete();
payload.notificationServices.forEach((notificationService) => {
NotificationService.upsert(notificationService);
});
break;
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.PROJECT_CREATE_HANDLE:
payload.notificationServices.forEach((notificationService) => {
NotificationService.upsert(notificationService);
});
break;
case ActionTypes.USER_UPDATE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
if (payload.notificationServices) {
payload.notificationServices.forEach((notificationService) => {
NotificationService.upsert(notificationService);
});
}
break;
case ActionTypes.NOTIFICATION_SERVICE_CREATE:
case ActionTypes.NOTIFICATION_SERVICE_CREATE_HANDLE:
case ActionTypes.NOTIFICATION_SERVICE_UPDATE__SUCCESS:
case ActionTypes.NOTIFICATION_SERVICE_UPDATE_HANDLE:
NotificationService.upsert(payload.notificationService);
break;
case ActionTypes.NOTIFICATION_SERVICE_CREATE__SUCCESS:
NotificationService.withId(payload.localId).delete();
NotificationService.upsert(payload.notificationService);
break;
case ActionTypes.NOTIFICATION_SERVICE_CREATE__FAILURE:
NotificationService.withId(payload.localId).delete();
break;
case ActionTypes.NOTIFICATION_SERVICE_UPDATE:
NotificationService.withId(payload.id).update(payload.data);
break;
case ActionTypes.NOTIFICATION_SERVICE_TEST:
NotificationService.withId(payload.id).update({
isTesting: true,
});
break;
case ActionTypes.NOTIFICATION_SERVICE_TEST__SUCCESS:
NotificationService.upsert({
...payload.notificationService,
isTesting: false,
});
break;
case ActionTypes.NOTIFICATION_SERVICE_TEST__FAILURE:
NotificationService.withId(payload.id).update({
isTesting: false,
});
break;
case ActionTypes.NOTIFICATION_SERVICE_DELETE:
NotificationService.withId(payload.id).delete();
break;
case ActionTypes.NOTIFICATION_SERVICE_DELETE__SUCCESS:
case ActionTypes.NOTIFICATION_SERVICE_DELETE_HANDLE: {
const notificationServiceModel = NotificationService.withId(payload.notificationService.id);
if (notificationServiceModel) {
notificationServiceModel.delete();
}
break;
}
default:
}
}
}

View File

@@ -1,8 +1,13 @@
import { attr, many } from 'redux-orm';
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, many, oneToOne } from 'redux-orm';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';
import { ProjectBackgroundTypes } from '../constants/Enums';
import { UserRoles } from '../constants/Enums';
export default class extends BaseModel {
static modelName = 'Project';
@@ -10,15 +15,27 @@ export default class extends BaseModel {
static fields = {
id: attr(),
name: attr(),
background: attr(),
backgroundImage: attr(),
isBackgroundImageUpdating: attr({
description: attr(),
backgroundType: attr(),
backgroundGradient: attr(),
isHidden: attr(),
isFavorite: attr({
getDefault: () => false,
}),
ownerProjectManagerId: oneToOne({
to: 'ProjectManager',
as: 'ownerProjectManager',
relatedName: 'ownedProject',
}),
backgroundImageId: oneToOne({
to: 'BackgroundImage',
as: 'backgroundImage',
relatedName: 'backgroundedProject', // TODO: rename?
}),
managerUsers: many({
to: 'User',
through: 'ProjectManager',
relatedName: 'projects',
relatedName: 'managerProjects',
}),
};
@@ -47,46 +64,51 @@ export default class extends BaseModel {
});
break;
case ActionTypes.PROJECT_CREATE__SUCCESS:
case ActionTypes.PROJECT_CREATE_HANDLE:
case ActionTypes.PROJECT_UPDATE__SUCCESS:
case ActionTypes.PROJECT_UPDATE_HANDLE:
Project.upsert(payload.project);
case ActionTypes.USER_UPDATE_HANDLE:
Project.all()
.toModelArray()
.forEach((projectModel) => {
if (!payload.projectIds.includes(projectModel.id)) {
projectModel.deleteWithRelated();
}
});
break;
case ActionTypes.PROJECT_UPDATE: {
const project = Project.withId(payload.id);
project.update(payload.data);
if (
payload.data.backgroundImage === null &&
project.background &&
project.background.type === ProjectBackgroundTypes.IMAGE
) {
project.background = null;
if (payload.projects) {
payload.projects.forEach((project) => {
Project.upsert(project);
});
}
break;
case ActionTypes.PROJECT_CREATE__SUCCESS:
case ActionTypes.PROJECT_CREATE_HANDLE:
case ActionTypes.PROJECT_UPDATE__SUCCESS:
Project.upsert(payload.project);
break;
case ActionTypes.PROJECT_UPDATE:
Project.withId(payload.id).update(payload.data);
break;
case ActionTypes.PROJECT_UPDATE_HANDLE: {
const projectModel = Project.withId(payload.project.id);
if (projectModel) {
if (payload.isAvailable) {
projectModel.boards.toModelArray().forEach((boardModel) => {
if (!payload.boardIds.includes(boardModel.id)) {
boardModel.deleteWithRelated();
}
});
} else {
projectModel.deleteWithRelated();
}
}
Project.upsert(payload.project);
break;
}
case ActionTypes.PROJECT_BACKGROUND_IMAGE_UPDATE:
Project.withId(payload.id).update({
isBackgroundImageUpdating: true,
});
break;
case ActionTypes.PROJECT_BACKGROUND_IMAGE_UPDATE__SUCCESS:
Project.withId(payload.project.id).update({
...payload.project,
isBackgroundImageUpdating: false,
});
break;
case ActionTypes.PROJECT_BACKGROUND_IMAGE_UPDATE__FAILURE:
Project.withId(payload.id).update({
isBackgroundImageUpdating: false,
});
break;
case ActionTypes.PROJECT_DELETE:
Project.withId(payload.id).deleteWithRelated();
@@ -101,64 +123,86 @@ export default class extends BaseModel {
break;
}
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
if (payload.project) {
const projectModel = Project.withId(payload.project.id);
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE: {
const projectModel = Project.withId(payload.projectManager.projectId);
if (projectModel) {
if (projectModel) {
if (payload.isProjectAvailable) {
projectModel.boards.toModelArray().forEach((boardModel) => {
if (payload.boardIds.includes(boardModel.id)) {
if (payload.isCurrentUser) {
boardModel.notificationServices.delete();
}
} else {
boardModel.deleteWithRelated();
}
});
} else {
projectModel.deleteWithRelated();
}
}
if (payload.project) {
Project.upsert(payload.project);
}
break;
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE__PROJECT_FETCH:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE__PROJECT_FETCH: {
const projectModel = Project.withId(payload.id);
}
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
if (!payload.isProjectAvailable) {
const projectModel = Project.withId(payload.boardMembership.projectId);
if (projectModel) {
projectModel.boards.toModelArray().forEach((boardModel) => {
if (boardModel.id !== payload.currentBoardId) {
boardModel.update({
isFetching: null,
});
if (projectModel) {
projectModel.deleteWithRelated();
}
}
boardModel.deleteRelated(payload.currentUserId);
}
});
if (payload.project) {
Project.upsert(payload.project);
}
break;
}
default:
}
}
getOrderedManagersQuerySet() {
return this.managers.orderBy('createdAt');
static getSharedQuerySet() {
return this.filter({
ownerProjectManagerId: null,
}).orderBy(['id.length', 'id']);
}
getOrderedBoardsQuerySet() {
return this.boards.orderBy('position');
getManagersQuerySet() {
return this.managers.orderBy(['id.length', 'id']);
}
getOrderedBoardsModelArrayForUser(userId) {
return this.getOrderedBoardsQuerySet()
getBackgroundImagesQuerySet() {
return this.backgroundImages.orderBy(['id.length', 'id']);
}
getBaseCustomFieldGroupsQuerySet() {
return this.baseCustomFieldGroups.orderBy(['id.length', 'id']);
}
getBoardsQuerySet() {
return this.boards.orderBy(['position', 'id.length', 'id']);
}
getBoardsModelArrayForUserWithId(userId) {
return this.getBoardsQuerySet()
.toModelArray()
.filter((boardModel) => boardModel.hasMembershipForUser(userId));
.filter((boardModel) => boardModel.hasMembershipWithUserId(userId));
}
getOrderedBoardsModelArrayAvailableForUser(userId) {
if (this.hasManagerForUser(userId)) {
return this.getOrderedBoardsQuerySet().toModelArray();
getBoardsModelArrayAvailableForUser(userModel) {
if (this.isExternalAccessibleForUser(userModel)) {
return this.getBoardsQuerySet().toModelArray();
}
return this.getOrderedBoardsModelArrayForUser(userId);
return this.getBoardsModelArrayForUserWithId(userModel.id);
}
hasManagerForUser(userId) {
hasManagerWithUserId(userId) {
return this.managers
.filter({
userId,
@@ -166,17 +210,38 @@ export default class extends BaseModel {
.exists();
}
hasMembershipInAnyBoardForUser(userId) {
return this.boards.toModelArray().some((boardModel) => boardModel.hasMembershipForUser(userId));
hasMembershipWithUserIdInAnyBoard(userId) {
return this.boards
.toModelArray()
.some((boardModel) => boardModel.hasMembershipWithUserId(userId));
}
isAvailableForUser(userId) {
return this.hasManagerForUser(userId) || this.hasMembershipInAnyBoardForUser(userId);
isExternalAccessibleForUser(userModel) {
if (!this.ownerProjectManagerId && userModel.role === UserRoles.ADMIN) {
return true;
}
return this.hasManagerWithUserId(userModel.id);
}
isAvailableForUser(userModel) {
return (
this.isExternalAccessibleForUser(userModel) ||
this.hasMembershipWithUserIdInAnyBoard(userModel.id)
);
}
deleteRelated() {
this.managers.delete();
this.backgroundImages.toModelArray().forEach((backgroundImageModel) => {
backgroundImageModel.deleteWithRelated();
});
this.baseCustomFieldGroups.toModelArray().forEach((baseCustomFieldGroupModel) => {
baseCustomFieldGroupModel.deleteWithRelated();
});
this.boards.toModelArray().forEach((boardModel) => {
boardModel.deleteWithRelated();
});

View File

@@ -1,3 +1,8 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
@@ -8,9 +13,6 @@ export default class extends BaseModel {
static fields = {
id: attr(),
createdAt: attr({
getDefault: () => new Date(),
}),
projectId: fk({
to: 'Project',
as: 'project',
@@ -40,6 +42,16 @@ export default class extends BaseModel {
ProjectManager.upsert(projectManager);
});
break;
case ActionTypes.USER_UPDATE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
if (payload.projectManagers) {
payload.projectManagers.forEach((projectManager) => {
ProjectManager.upsert(projectManager);
});
}
break;
case ActionTypes.PROJECT_MANAGER_CREATE:
ProjectManager.upsert(payload.projectManager);
@@ -49,6 +61,10 @@ export default class extends BaseModel {
ProjectManager.withId(payload.localId).delete();
ProjectManager.upsert(payload.projectManager);
break;
case ActionTypes.PROJECT_MANAGER_CREATE__FAILURE:
ProjectManager.withId(payload.localId).delete();
break;
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
ProjectManager.upsert(payload.projectManager);
@@ -74,14 +90,6 @@ export default class extends BaseModel {
break;
}
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
if (payload.projectManagers) {
payload.projectManagers.forEach((projectManager) => {
ProjectManager.upsert(projectManager);
});
}
break;
default:
}
}

View File

@@ -1,6 +1,10 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import { createLocalId } from '../utils/local-id';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';
@@ -14,17 +18,24 @@ export default class extends BaseModel {
isCompleted: attr({
getDefault: () => false,
}),
cardId: fk({
to: 'Card',
as: 'card',
taskListId: fk({
to: 'TaskList',
as: 'taskList',
relatedName: 'tasks',
}),
assigneeUserId: fk({
to: 'User',
as: 'user',
relatedName: 'assignedTasks',
}),
};
static reducer({ type, payload }, Task) {
switch (type) {
case ActionTypes.LOCATION_CHANGE_HANDLE:
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.USER_UPDATE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
@@ -46,24 +57,13 @@ export default class extends BaseModel {
break;
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.tasks.forEach((task) => {
Task.upsert(task);
});
break;
case ActionTypes.CARD_DUPLICATE:
payload.taskIds.forEach((taskId, index) => {
const taskModel = Task.withId(taskId);
Task.upsert({
...taskModel.ref,
id: `${createLocalId()}-${index}`, // TODO: hack?
cardId: payload.card.id,
});
});
break;
case ActionTypes.TASK_CREATE:
case ActionTypes.TASK_CREATE_HANDLE:
@@ -76,6 +76,10 @@ export default class extends BaseModel {
Task.withId(payload.localId).delete();
Task.upsert(payload.task);
break;
case ActionTypes.TASK_CREATE__FAILURE:
Task.withId(payload.localId).delete();
break;
case ActionTypes.TASK_UPDATE:
Task.withId(payload.id).update(payload.data);
@@ -98,4 +102,16 @@ export default class extends BaseModel {
default:
}
}
duplicate(id, data) {
return this.getClass().create({
id,
taskListId: this.taskListId,
assigneeUserId: this.assigneeUserId,
position: this.position,
name: this.name,
isCompleted: this.isCompleted,
...data,
});
}
}

134
client/src/models/TaskList.js Executable file
View File

@@ -0,0 +1,134 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';
export default class extends BaseModel {
static modelName = 'TaskList';
static fields = {
id: attr(),
position: attr(),
name: attr(),
showOnFrontOfCard: attr(),
cardId: fk({
to: 'Card',
as: 'card',
relatedName: 'taskLists',
}),
};
static reducer({ type, payload }, TaskList) {
switch (type) {
case ActionTypes.LOCATION_CHANGE_HANDLE:
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.USER_UPDATE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
if (payload.taskLists) {
payload.taskLists.forEach((taskList) => {
TaskList.upsert(taskList);
});
}
break;
case ActionTypes.SOCKET_RECONNECT_HANDLE:
TaskList.all().delete();
if (payload.taskLists) {
payload.taskLists.forEach((taskList) => {
TaskList.upsert(taskList);
});
}
break;
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.taskLists.forEach((taskList) => {
TaskList.upsert(taskList);
});
break;
case ActionTypes.TASK_LIST_CREATE:
case ActionTypes.TASK_LIST_CREATE_HANDLE:
case ActionTypes.TASK_LIST_UPDATE__SUCCESS:
case ActionTypes.TASK_LIST_UPDATE_HANDLE:
TaskList.upsert(payload.taskList);
break;
case ActionTypes.TASK_LIST_CREATE__SUCCESS:
TaskList.withId(payload.localId).delete();
TaskList.upsert(payload.taskList);
break;
case ActionTypes.TASK_LIST_CREATE__FAILURE:
TaskList.withId(payload.localId).delete();
break;
case ActionTypes.TASK_LIST_UPDATE:
TaskList.withId(payload.id).update(payload.data);
break;
case ActionTypes.TASK_LIST_DELETE:
TaskList.withId(payload.id).deleteWithRelated();
break;
case ActionTypes.TASK_LIST_DELETE__SUCCESS:
case ActionTypes.TASK_LIST_DELETE_HANDLE: {
const taskListModel = TaskList.withId(payload.taskList.id);
if (taskListModel) {
taskListModel.deleteWithRelated();
}
break;
}
default:
}
}
getTasksQuerySet() {
return this.tasks.orderBy(['position', 'id.length', 'id']);
}
duplicate(id, data, rootId) {
if (rootId === undefined) {
rootId = id; // eslint-disable-line no-param-reassign
}
const taskListModel = this.getClass().create({
id,
cardId: this.cardId,
position: this.position,
name: this.name,
showOnFrontOfCard: this.showOnFrontOfCard,
...data,
});
this.tasks.toModelArray().forEach((taskModel) => {
taskModel.duplicate(`${taskModel.id}-${rootId}`, {
taskListId: taskListModel.id,
});
});
return taskListModel;
}
deleteRelated() {
this.tasks.delete();
}
deleteWithRelated() {
this.deleteRelated();
this.delete();
}
}

View File

@@ -1,7 +1,15 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { orderBy } from 'lodash';
import { attr } from 'redux-orm';
import BaseModel from './BaseModel';
import buildSearchParts from '../utils/build-search-parts';
import ActionTypes from '../constants/ActionTypes';
import { UserRoles } from '../constants/Enums';
const DEFAULT_EMAIL_UPDATE_FORM = {
data: {
@@ -30,28 +38,42 @@ const DEFAULT_USERNAME_UPDATE_FORM = {
error: null,
};
const filterProjectModels = (projectModels, search, isHidden) => {
let filteredProjectModels = projectModels.filter(
(projectModel) => projectModel.isHidden === isHidden,
);
if (filteredProjectModels.length > 0 && search) {
const searchParts = buildSearchParts(search);
filteredProjectModels = filteredProjectModels.filter((projectModel) =>
searchParts.every((searchPart) => projectModel.name.toLowerCase().includes(searchPart)),
);
}
return filteredProjectModels;
};
export default class extends BaseModel {
static modelName = 'User';
static fields = {
id: attr(),
email: attr(),
role: attr(),
username: attr(),
name: attr(),
avatarUrl: attr(),
avatar: attr(),
phone: attr(),
organization: attr(),
language: attr(),
subscribeToOwnCards: attr(),
isAdmin: attr(),
isLocked: attr(),
isRoleLocked: attr(),
isUsernameLocked: attr(),
isDeletionLocked: attr(),
deletedAt: attr(),
createdAt: attr({
getDefault: () => new Date(),
}),
subscribeToCardWhenCommenting: attr(),
turnOffRecentCardHighlighting: attr(),
isDefaultAdmin: attr(),
isSsoUser: attr(),
isDeactivated: attr(),
lockedFieldNames: attr(),
isAvatarUpdating: attr({
getDefault: () => false,
}),
@@ -69,6 +91,9 @@ export default class extends BaseModel {
static reducer({ type, payload }, User) {
switch (type) {
case ActionTypes.LOCATION_CHANGE_HANDLE:
case ActionTypes.PROJECT_UPDATE_HANDLE:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
if (payload.users) {
payload.users.forEach((user) => {
User.upsert(user);
@@ -78,7 +103,6 @@ export default class extends BaseModel {
break;
case ActionTypes.SOCKET_RECONNECT_HANDLE:
User.all().delete();
User.upsert(payload.user);
payload.users.forEach((user) => {
@@ -117,138 +141,117 @@ export default class extends BaseModel {
case ActionTypes.USER_EMAIL_UPDATE: {
const userModel = User.withId(payload.id);
userModel.update({
emailUpdateForm: {
...userModel.emailUpdateForm,
data: payload.data,
isSubmitting: true,
},
});
userModel.emailUpdateForm = {
...userModel.emailUpdateForm,
data: payload.data,
isSubmitting: true,
};
break;
}
case ActionTypes.USER_EMAIL_UPDATE__SUCCESS: {
case ActionTypes.USER_EMAIL_UPDATE__SUCCESS:
User.withId(payload.user.id).update({
...payload.user,
emailUpdateForm: DEFAULT_EMAIL_UPDATE_FORM,
});
break;
}
case ActionTypes.USER_EMAIL_UPDATE__FAILURE: {
const userModel = User.withId(payload.id);
userModel.update({
emailUpdateForm: {
...userModel.emailUpdateForm,
isSubmitting: false,
error: payload.error,
},
});
userModel.emailUpdateForm = {
...userModel.emailUpdateForm,
isSubmitting: false,
error: payload.error,
};
break;
}
case ActionTypes.USER_EMAIL_UPDATE_ERROR_CLEAR: {
const userModel = User.withId(payload.id);
userModel.update({
emailUpdateForm: {
...userModel.emailUpdateForm,
error: null,
},
});
userModel.emailUpdateForm = {
...userModel.emailUpdateForm,
error: null,
};
break;
}
case ActionTypes.USER_PASSWORD_UPDATE: {
const userModel = User.withId(payload.id);
userModel.update({
passwordUpdateForm: {
...userModel.passwordUpdateForm,
data: payload.data,
isSubmitting: true,
},
});
userModel.passwordUpdateForm = {
...userModel.passwordUpdateForm,
data: payload.data,
isSubmitting: true,
};
break;
}
case ActionTypes.USER_PASSWORD_UPDATE__SUCCESS: {
case ActionTypes.USER_PASSWORD_UPDATE__SUCCESS:
User.withId(payload.user.id).update({
...payload.user,
passwordUpdateForm: DEFAULT_PASSWORD_UPDATE_FORM,
});
break;
}
case ActionTypes.USER_PASSWORD_UPDATE__FAILURE: {
const userModel = User.withId(payload.id);
userModel.update({
passwordUpdateForm: {
...userModel.passwordUpdateForm,
isSubmitting: false,
error: payload.error,
},
});
userModel.passwordUpdateForm = {
...userModel.passwordUpdateForm,
isSubmitting: false,
error: payload.error,
};
break;
}
case ActionTypes.USER_PASSWORD_UPDATE_ERROR_CLEAR: {
const userModel = User.withId(payload.id);
userModel.update({
passwordUpdateForm: {
...userModel.passwordUpdateForm,
error: null,
},
});
userModel.passwordUpdateForm = {
...userModel.passwordUpdateForm,
error: null,
};
break;
}
case ActionTypes.USER_USERNAME_UPDATE: {
const userModel = User.withId(payload.id);
userModel.update({
usernameUpdateForm: {
...userModel.usernameUpdateForm,
data: payload.data,
isSubmitting: true,
},
});
userModel.usernameUpdateForm = {
...userModel.usernameUpdateForm,
data: payload.data,
isSubmitting: true,
};
break;
}
case ActionTypes.USER_USERNAME_UPDATE__SUCCESS: {
case ActionTypes.USER_USERNAME_UPDATE__SUCCESS:
User.withId(payload.user.id).update({
...payload.user,
usernameUpdateForm: DEFAULT_USERNAME_UPDATE_FORM,
});
break;
}
case ActionTypes.USER_USERNAME_UPDATE__FAILURE: {
const userModel = User.withId(payload.id);
userModel.update({
usernameUpdateForm: {
...userModel.usernameUpdateForm,
isSubmitting: false,
error: payload.error,
},
});
userModel.usernameUpdateForm = {
...userModel.usernameUpdateForm,
isSubmitting: false,
error: payload.error,
};
break;
}
case ActionTypes.USER_USERNAME_UPDATE_ERROR_CLEAR: {
const userModel = User.withId(payload.id);
userModel.update({
usernameUpdateForm: {
...userModel.usernameUpdateForm,
error: null,
},
});
userModel.usernameUpdateForm = {
...userModel.usernameUpdateForm,
error: null,
};
break;
}
@@ -276,14 +279,22 @@ export default class extends BaseModel {
break;
case ActionTypes.USER_DELETE__SUCCESS:
case ActionTypes.USER_DELETE_HANDLE:
User.withId(payload.user.id).deleteWithRelated(payload.user);
case ActionTypes.USER_DELETE_HANDLE: {
const userModel = User.withId(payload.user.id);
if (userModel) {
userModel.deleteWithRelated();
}
break;
}
case ActionTypes.PROJECT_CREATE_HANDLE:
case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.COMMENTS_FETCH__SUCCESS:
case ActionTypes.COMMENT_CREATE_HANDLE:
case ActionTypes.ACTIVITIES_FETCH__SUCCESS:
case ActionTypes.NOTIFICATION_CREATE_HANDLE:
payload.users.forEach((user) => {
@@ -295,82 +306,204 @@ export default class extends BaseModel {
}
}
static getOrderedUndeletedQuerySet() {
static getAllQuerySet() {
return this.orderBy([({ name }) => name.toLowerCase(), 'id.length', 'id']);
}
static getActiveQuerySet() {
return this.filter({
deletedAt: null,
}).orderBy((user) => user.name.toLocaleLowerCase());
isDeactivated: false,
}).orderBy([({ name }) => name.toLowerCase(), 'id.length', 'id']);
}
getOrderedProjectManagersQuerySet() {
return this.projectManagers.orderBy('createdAt');
getProjectManagersQuerySet() {
return this.projectManagers.orderBy(['id.length', 'id']);
}
getOrderedBoardMembershipsQuerySet() {
return this.boardMemberships.orderBy('createdAt');
getBoardMembershipsQuerySet() {
return this.boardMemberships.orderBy(['id.length', 'id']);
}
getOrderedUnreadNotificationsQuerySet() {
getUnreadNotificationsQuerySet() {
return this.notifications
.filter({
isRead: false,
})
.orderBy('createdAt', false);
.orderBy(['id.length', 'id'], ['desc', 'desc']);
}
getOrderedAvailableProjectsModelArray() {
getNotificationServicesQuerySet() {
return this.notificationServices.orderBy(['id.length', 'id']);
}
getManagerProjectsModelArray() {
return this.getProjectManagersQuerySet()
.toModelArray()
.map(({ project: projectModel }) => projectModel);
}
getMembershipProjectsModelArray() {
const projectIds = [];
const projectModels = this.getOrderedProjectManagersQuerySet()
return this.getBoardMembershipsQuerySet()
.toModelArray()
.map(({ project: projectModel }) => {
projectIds.push(projectModel.id);
return projectModel;
});
this.getOrderedBoardMembershipsQuerySet()
.toModelArray()
.forEach(({ board: { project: projectModel } }) => {
.flatMap(({ board: { project: projectModel } }) => {
if (projectIds.includes(projectModel.id)) {
return;
return [];
}
projectIds.push(projectModel.id);
projectModels.push(projectModel);
return projectModel;
});
}
getSeparatedProjectsModelArray() {
const projectIds = [];
const managerProjectModels = this.getManagerProjectsModelArray().map((projectModel) => {
projectIds.push(projectModel.id);
return projectModel;
});
const membershipProjectModels = this.getMembershipProjectsModelArray().flatMap(
(projectModel) => {
if (projectIds.includes(projectModel.id)) {
return [];
}
projectIds.push(projectModel.id);
return projectModel;
},
);
let adminProjectModels = [];
if (this.role === UserRoles.ADMIN) {
const {
session: { Project },
} = this.getClass();
adminProjectModels = Project.getSharedQuerySet()
.toModelArray()
.flatMap((projectModel) => {
if (projectIds.includes(projectModel.id)) {
return [];
}
projectIds.push(projectModel.id);
return projectModel;
});
}
return {
managerProjectModels,
membershipProjectModels,
adminProjectModels,
};
}
getProjectsModelArray() {
const { managerProjectModels, membershipProjectModels, adminProjectModels } =
this.getSeparatedProjectsModelArray();
return [...managerProjectModels, ...membershipProjectModels, ...adminProjectModels];
}
getFavoriteProjectsModelArray(orderByArgs) {
let projectModels = this.getProjectsModelArray();
projectModels = projectModels.filter(
(projectModel) => !projectModel.isHidden && projectModel.isFavorite,
);
if (orderByArgs) {
projectModels = orderBy(projectModels, ...orderByArgs);
}
return projectModels;
}
getFilteredSeparatedProjectsModelArray(search, isHidden, orderByArgs) {
const separatedProjectModels = this.getSeparatedProjectsModelArray();
return Object.entries(separatedProjectModels).reduce((result, [key, projectModels]) => {
let filteredProjectModels = filterProjectModels(projectModels, search, isHidden);
if (orderByArgs) {
filteredProjectModels = orderBy(filteredProjectModels, ...orderByArgs);
}
return {
...result,
[key]: filteredProjectModels,
};
}, {});
}
getFilteredProjectsModelArray(search, isHidden, orderByArgs) {
let projectModels = this.getProjectsModelArray();
projectModels = filterProjectModels(projectModels, search, isHidden);
if (orderByArgs) {
projectModels = orderBy(projectModels, ...orderByArgs);
}
return projectModels;
}
deleteRelated() {
this.projectManagers.delete();
this.projectManagers.toModelArray().forEach((projectManagerModel) => {
if (projectManagerModel.ownedProject) {
projectManagerModel.ownedProject.deleteWithRelated();
} else {
projectManagerModel.delete();
}
});
this.boardMemberships.toModelArray().forEach((boardMembershipModel) => {
boardMembershipModel.deleteWithRelated();
});
this.createdCards.toModelArray().forEach((cardModel) => {
cardModel.update({
creatorUserId: null,
});
});
this.assignedTasks.toModelArray().forEach((taskModel) => {
taskModel.update({
assigneeUserId: null,
});
});
this.createdAttachments.toModelArray().forEach((attachmentModel) => {
attachmentModel.update({
creatorUserId: null,
});
});
this.comments.toModelArray().forEach((commentModel) => {
commentModel.update({
userId: null,
});
});
this.activities.toModelArray().forEach((activityModel) => {
activityModel.update({
userId: null,
});
});
this.createdNotifications.toModelArray().forEach((notificationModel) => {
notificationModel.update({
creatorUserId: null,
});
});
this.notificationServices.delete();
}
deleteWithRelated(user) {
deleteWithRelated() {
this.deleteRelated();
this.update(
user || {
deletedAt: new Date(),
},
);
}
static findUsersFromText(filterText, users) {
const selectUser = filterText.toLocaleLowerCase();
const matchingUsers = users.filter(
(user) =>
user.name.toLocaleLowerCase().startsWith(selectUser) ||
user.username.toLocaleLowerCase().startsWith(selectUser),
);
if (matchingUsers.length === 1) {
// Appens the user to the filter
return matchingUsers[0].id;
}
return null;
this.delete();
}
}

View File

@@ -1,27 +1,48 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import User from './User';
import Project from './Project';
import ProjectManager from './ProjectManager';
import BackgroundImage from './BackgroundImage';
import BaseCustomFieldGroup from './BaseCustomFieldGroup';
import Board from './Board';
import BoardMembership from './BoardMembership';
import Label from './Label';
import List from './List';
import Card from './Card';
import TaskList from './TaskList';
import Task from './Task';
import Attachment from './Attachment';
import CustomFieldGroup from './CustomFieldGroup';
import CustomField from './CustomField';
import CustomFieldValue from './CustomFieldValue';
import Comment from './Comment';
import Activity from './Activity';
import Notification from './Notification';
import NotificationService from './NotificationService';
export {
User,
Project,
ProjectManager,
BackgroundImage,
BaseCustomFieldGroup,
Board,
BoardMembership,
Label,
List,
Card,
TaskList,
Task,
Attachment,
CustomFieldGroup,
CustomField,
CustomFieldValue,
Comment,
Activity,
Notification,
NotificationService,
};