diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index f508dab3b..5f69d4bd8 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -1012,6 +1012,9 @@ Template.editCardAssignerForm.events({ return ret; } async setDone(cardId, options) { + // Capture DOM values immediately before any async operations + const position = this.$('input[name="position"]:checked').val(); + ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); const card = this.data(); let sortIndex = 0; @@ -1019,7 +1022,6 @@ Template.editCardAssignerForm.events({ if (cardId) { const targetCard = ReactiveCache.getCard(cardId); if (targetCard) { - const position = this.$('input[name="position"]:checked').val(); if (position === 'above') { sortIndex = targetCard.sort - 0.5; } else { @@ -1042,37 +1044,40 @@ Template.editCardAssignerForm.events({ return ret; } async setDone(cardId, options) { - ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); - const card = this.data(); - - // const textarea = $('#copy-card-title'); + // Capture DOM values immediately before any async operations const textarea = this.$('#copy-card-title'); const title = textarea.val().trim(); + const position = this.$('input[name="position"]:checked').val(); + + ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); + const card = this.data(); if (title) { const newCardId = await Meteor.callAsync('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, true, {title: title}); - // Position the copied card + // Position the copied card (newCard may be null for cross-board copies + // if the client hasn't received the publication update yet) if (newCardId) { const newCard = ReactiveCache.getCard(newCardId); - let sortIndex = 0; + if (newCard) { + let sortIndex = 0; - if (cardId) { - const targetCard = ReactiveCache.getCard(cardId); - if (targetCard) { - const position = this.$('input[name="position"]:checked').val(); - if (position === 'above') { - sortIndex = targetCard.sort - 0.5; - } else { - sortIndex = targetCard.sort + 0.5; + if (cardId) { + const targetCard = ReactiveCache.getCard(cardId); + if (targetCard) { + if (position === 'above') { + sortIndex = targetCard.sort - 0.5; + } else { + sortIndex = targetCard.sort + 0.5; + } } + } else { + // If no card selected, copy to end + sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1; } - } else { - // If no card selected, copy to end - sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1; - } - await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex); + await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex); + } } // In case the filter is active we need to add the newly inserted card in @@ -1091,11 +1096,13 @@ Template.editCardAssignerForm.events({ return ret; } async setDone(cardId, options) { - ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); - const card = this.data(); - + // Capture DOM values immediately before any async operations const textarea = this.$('#copy-card-title'); const title = textarea.val().trim(); + const position = this.$('input[name="position"]:checked').val(); + + ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); + const card = this.data(); if (title) { const _id = Cards.insert({ @@ -1111,7 +1118,6 @@ Template.editCardAssignerForm.events({ if (cardId) { const targetCard = ReactiveCache.getCard(cardId); if (targetCard) { - const position = this.$('input[name="position"]:checked').val(); if (position === 'above') { sortIndex = targetCard.sort - 0.5; } else { @@ -1136,11 +1142,13 @@ Template.editCardAssignerForm.events({ return ret; } async setDone(cardId, options) { - ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); - const card = this.data(); - + // Capture DOM values immediately before any async operations const textarea = this.$('#copy-card-title'); const title = textarea.val().trim(); + const position = this.$('input[name="position"]:checked').val(); + + ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); + const card = this.data(); if (title) { const titleList = JSON.parse(title); @@ -1155,7 +1163,6 @@ Template.editCardAssignerForm.events({ if (cardId) { const targetCard = ReactiveCache.getCard(cardId); if (targetCard) { - const position = this.$('input[name="position"]:checked').val(); if (position === 'above') { sortIndex = targetCard.sort - 0.5; } else { diff --git a/client/lib/dialogWithBoardSwimlaneList.js b/client/lib/dialogWithBoardSwimlaneList.js index 0471efd88..888601a56 100644 --- a/client/lib/dialogWithBoardSwimlaneList.js +++ b/client/lib/dialogWithBoardSwimlaneList.js @@ -186,7 +186,7 @@ export class DialogWithBoardSwimlaneList extends BlazeComponent { events() { return [ { - 'click .js-done'() { + async 'click .js-done'() { const boardSelect = this.$('.js-select-boards')[0]; const boardId = boardSelect.options[boardSelect.selectedIndex].value; @@ -201,7 +201,11 @@ export class DialogWithBoardSwimlaneList extends BlazeComponent { 'swimlaneId' : swimlaneId, 'listId' : listId, } - this.setDone(boardId, swimlaneId, listId, options); + try { + await this.setDone(boardId, swimlaneId, listId, options); + } catch (e) { + console.error('Error in list dialog operation:', e); + } Popup.back(2); }, 'change .js-select-boards'(event) { diff --git a/client/lib/dialogWithBoardSwimlaneListCard.js b/client/lib/dialogWithBoardSwimlaneListCard.js index 77ceb968b..10421c3c1 100644 --- a/client/lib/dialogWithBoardSwimlaneListCard.js +++ b/client/lib/dialogWithBoardSwimlaneListCard.js @@ -80,7 +80,7 @@ export class DialogWithBoardSwimlaneListCard extends DialogWithBoardSwimlaneList events() { return [ { - 'click .js-done'() { + async 'click .js-done'() { const boardSelect = this.$('.js-select-boards')[0]; const boardId = boardSelect.options[boardSelect.selectedIndex].value; @@ -99,7 +99,11 @@ export class DialogWithBoardSwimlaneListCard extends DialogWithBoardSwimlaneList 'listId' : listId, 'cardId': cardId, } - this.setDone(cardId, options); + try { + await this.setDone(cardId, options); + } catch (e) { + console.error('Error in card dialog operation:', e); + } Popup.back(2); }, 'change .js-select-boards'(event) { diff --git a/models/cards.js b/models/cards.js index 87be370d8..450fb0e35 100644 --- a/models/cards.js +++ b/models/cards.js @@ -611,6 +611,15 @@ Cards.helpers({ const oldId = this._id; const oldCard = ReactiveCache.getCard(oldId); + // Work on a shallow copy to avoid mutating the source card in ReactiveCache + const cardData = { ...this }; + delete cardData._id; + + // Normalize customFields to ensure it's always an array + if (!Array.isArray(cardData.customFields)) { + cardData.customFields = []; + } + // we must only copy the labels and custom fields if the target board // differs from the source board if (this.boardId !== boardId) { @@ -633,19 +642,16 @@ Cards.helpers({ }), '_id', ); - // now set the new label ids - delete this.labelIds; - this.labelIds = newCardLabels; + cardData.labelIds = newCardLabels; - this.customFields = await this.mapCustomFieldsToBoard(newBoard._id); + cardData.customFields = await this.mapCustomFieldsToBoard(newBoard._id); } - delete this._id; - this.boardId = boardId; - this.cardNumber = ReactiveCache.getBoard(boardId).getNextCardNumber(); - this.swimlaneId = swimlaneId; - this.listId = listId; - const _id = Cards.insert(this); + cardData.boardId = boardId; + cardData.cardNumber = ReactiveCache.getBoard(boardId).getNextCardNumber(); + cardData.swimlaneId = swimlaneId; + cardData.listId = listId; + const _id = Cards.insert(cardData); // Copy attachments oldCard.attachments() @@ -669,8 +675,6 @@ Cards.helpers({ ReactiveCache.getCardComments({ cardId: oldId }).forEach(cmt => { cmt.copy(_id); }); - // restore the id, otherwise new copies will fail - this._id = oldId; return _id; }, @@ -2102,6 +2106,11 @@ Cards.helpers({ }); mutatedFields.customFields = await this.mapCustomFieldsToBoard(newBoard._id); + + // Ensure customFields is always an array (guards against legacy {} data) + if (!Array.isArray(mutatedFields.customFields)) { + mutatedFields.customFields = []; + } } await Cards.updateAsync(this._id, { $set: mutatedFields });