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
+146 -32
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();