feat: Add ability to copy/cut cards with shortcut support

This commit is contained in:
Maksim Eltyshev
2025-12-09 14:58:01 +01:00
parent 52acc9de90
commit 9e6e38fcf7
113 changed files with 1616 additions and 259 deletions

View File

@@ -60,6 +60,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.attachments) {
payload.attachments.forEach((attachment) => {
Attachment.upsert(prepareAttachment(attachment));
@@ -80,6 +81,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.attachments.forEach((attachment) => {
Attachment.upsert(prepareAttachment(attachment));

View File

@@ -3,6 +3,7 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import keyBy from 'lodash/keyBy';
import { attr, fk, many, oneToOne } from 'redux-orm';
import BaseModel from './BaseModel';
@@ -316,33 +317,28 @@ export default class extends BaseModel {
case ActionTypes.CARD_UPDATE: {
const cardModel = Card.withId(payload.id);
// TODO: introduce separate action?
if (payload.data.boardId && payload.data.boardId !== cardModel.boardId) {
cardModel.deleteWithRelated();
} else {
if (payload.data.listId && payload.data.listId !== cardModel.listId) {
payload.data.listChangedAt = new Date(); // eslint-disable-line no-param-reassign
}
if (payload.data.dueDate !== undefined) {
if (payload.data.dueDate) {
if (!cardModel.dueDate) {
payload.data.isDueCompleted = false; // eslint-disable-line no-param-reassign
}
} else {
payload.data.isDueCompleted = null; // eslint-disable-line no-param-reassign
}
}
if (payload.data.isClosed !== undefined && payload.data.isClosed !== cardModel.isClosed) {
cardModel.linkedTasks.update({
isCompleted: payload.data.isClosed,
});
}
cardModel.update(payload.data);
if (payload.data.listId && payload.data.listId !== cardModel.listId) {
payload.data.listChangedAt = new Date(); // eslint-disable-line no-param-reassign
}
if (payload.data.dueDate !== undefined) {
if (payload.data.dueDate) {
if (!cardModel.dueDate) {
payload.data.isDueCompleted = false; // eslint-disable-line no-param-reassign
}
} else {
payload.data.isDueCompleted = null; // eslint-disable-line no-param-reassign
}
}
if (payload.data.isClosed !== undefined && payload.data.isClosed !== cardModel.isClosed) {
cardModel.linkedTasks.update({
isCompleted: payload.data.isClosed,
});
}
cardModel.update(payload.data);
break;
}
case ActionTypes.CARD_UPDATE_HANDLE: {
@@ -378,14 +374,86 @@ export default class extends BaseModel {
break;
}
case ActionTypes.CARD_DUPLICATE:
Card.withId(payload.id).duplicate(payload.localId, payload.data);
case ActionTypes.CARD_TRANSFER: {
const cardModel = Card.withId(payload.id);
if (cardModel) {
cardModel.update(payload.data);
cardModel.syncAfterBoardChange();
}
break;
case ActionTypes.CARD_DUPLICATE__SUCCESS: {
Card.withId(payload.localId).deleteWithRelated();
}
case ActionTypes.CARD_TRANSFER__SUCCESS: {
const cardModel = Card.withId(payload.card.id);
const cardModel = Card.upsert(payload.card);
if (cardModel) {
cardModel.deleteWithRelated();
}
Card.upsert(payload.card);
if (payload.cardMemberships) {
payload.cardMemberships.forEach(({ cardId, userId }) => {
Card.withId(cardId).users.add(userId);
});
}
if (payload.cardLabels) {
payload.cardLabels.forEach(({ cardId, labelId }) => {
Card.withId(cardId).labels.add(labelId);
});
}
break;
}
case ActionTypes.CARD_TRANSFER__FAILURE: {
const cardModel = Card.withId(payload.id);
if (cardModel) {
cardModel.deleteWithRelated();
}
if (payload.card) {
Card.upsert(payload.card);
}
if (payload.cardMemberships) {
payload.cardMemberships.forEach(({ cardId, userId }) => {
Card.withId(cardId).users.add(userId);
});
}
if (payload.cardLabels) {
payload.cardLabels.forEach(({ cardId, labelId }) => {
Card.withId(cardId).labels.add(labelId);
});
}
break;
}
case ActionTypes.CARD_DUPLICATE: {
let cardModel = Card.withId(payload.id);
if (cardModel) {
cardModel = cardModel.duplicate(payload.localId, {
...payload.data,
listChangedAt: new Date(),
});
cardModel.syncAfterBoardChange();
}
break;
}
case ActionTypes.CARD_DUPLICATE__SUCCESS: {
let cardModel = Card.withId(payload.localId);
if (cardModel) {
cardModel.deleteWithRelated();
}
cardModel = Card.upsert(payload.card);
payload.cardMemberships.forEach(({ userId }) => {
cardModel.users.add(userId);
@@ -397,10 +465,15 @@ export default class extends BaseModel {
break;
}
case ActionTypes.CARD_DUPLICATE__FAILURE:
Card.withId(payload.localId).deleteWithRelated();
case ActionTypes.CARD_DUPLICATE__FAILURE: {
const cardModel = Card.withId(payload.localId);
if (cardModel) {
cardModel.deleteWithRelated();
}
break;
}
case ActionTypes.CARD_DELETE:
Card.withId(payload.id).deleteWithRelated();
@@ -637,6 +710,47 @@ export default class extends BaseModel {
return cardModel;
}
syncAfterBoardChange() {
if (!this.board) {
return;
}
const boardMemberships = this.board.memberships.toRefArray();
const userIdsSet = new Set(boardMemberships.map(({ userId }) => userId));
this.users.toRefArray().forEach((user) => {
if (userIdsSet.has(user.id)) {
return;
}
this.users.remove(user.id);
});
this.taskLists.toModelArray().forEach((taskListModel) => {
taskListModel.tasks.toModelArray().forEach((taskModel) => {
if (!taskModel.assigneeUserId || userIdsSet.has(taskModel.assigneeUserId)) {
return;
}
taskModel.update({
assigneeUserId: null,
});
});
});
const labels = this.board.labels.toRefArray();
const labelByName = keyBy(labels, 'name');
this.labels.toRefArray().forEach((label) => {
if (!labelByName[label.name]) {
return;
}
this.labels.remove(label.id);
this.labels.add(labelByName[label.name].id);
});
}
deleteClearable() {
this.users.clear();
this.labels.clear();

View File

@@ -38,6 +38,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.customFields) {
payload.customFields.forEach((customField) => {
CustomField.upsert(customField);
@@ -59,6 +60,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.customFields.forEach((customField) => {
CustomField.upsert(customField);

View File

@@ -42,6 +42,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.customFieldGroups) {
payload.customFieldGroups.forEach((customFieldGroup) => {
CustomFieldGroup.upsert(customFieldGroup);
@@ -62,6 +63,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.customFieldGroups.forEach((customFieldGroup) => {
CustomFieldGroup.upsert(customFieldGroup);

View File

@@ -53,6 +53,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.customFieldValues) {
payload.customFieldValues.forEach((customFieldValue) => {
CustomFieldValue.upsert(prepareCustomFieldValue(customFieldValue));
@@ -73,6 +74,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.customFieldValues.forEach((customFieldValue) => {
CustomFieldValue.upsert(prepareCustomFieldValue(customFieldValue));

View File

@@ -45,6 +45,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.tasks) {
payload.tasks.forEach((task) => {
Task.upsert(task);
@@ -65,6 +66,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.tasks.forEach((task) => {
Task.upsert(task);

View File

@@ -34,6 +34,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.taskLists) {
payload.taskLists.forEach((taskList) => {
TaskList.upsert(taskList);
@@ -54,6 +55,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.taskLists.forEach((taskList) => {
TaskList.upsert(taskList);

View File

@@ -105,6 +105,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
case ActionTypes.LIST_UPDATE_HANDLE:
case ActionTypes.CARD_UPDATE_HANDLE:
case ActionTypes.CARD_TRANSFER__FAILURE:
if (payload.users) {
payload.users.forEach((user) => {
User.upsert(user);
@@ -351,6 +352,7 @@ export default class extends BaseModel {
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARDS_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_TRANSFER__SUCCESS:
case ActionTypes.COMMENTS_FETCH__SUCCESS:
case ActionTypes.COMMENT_CREATE_HANDLE:
case ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS: