diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 64708dd21..f508dab3b 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -1050,7 +1050,7 @@ Template.editCardAssignerForm.events({ const title = textarea.val().trim(); if (title) { - const newCardId = Meteor.call('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, true, {title: title}); + const newCardId = await Meteor.callAsync('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, true, {title: title}); // Position the copied card if (newCardId) { @@ -1145,7 +1145,7 @@ Template.editCardAssignerForm.events({ if (title) { const titleList = JSON.parse(title); for (const obj of titleList) { - const newCardId = Meteor.call('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, false, {title: obj.title, description: obj.description}); + const newCardId = await Meteor.callAsync('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, false, {title: obj.title, description: obj.description}); // Position the copied card if (newCardId) { diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js index d85bcce51..3e2612424 100644 --- a/client/components/lists/listBody.js +++ b/client/components/lists/listBody.js @@ -817,7 +817,7 @@ BlazeComponent.extendComponent({ evt.preventDefault(); this.term.set(evt.target.searchTerm.value); }, - 'click .js-minicard'(evt) { + async 'click .js-minicard'(evt) { // 0. Common const title = $('.js-element-title') .val() @@ -835,7 +835,7 @@ BlazeComponent.extendComponent({ if (this.isTemplateSearch) { element.type = 'cardType-card'; element.linkedId = ''; - _id = element.copy(this.boardId, this.swimlaneId, this.listId); + _id = await element.copy(this.boardId, this.swimlaneId, this.listId); // 1.B Linked card } else { _id = element.link(this.boardId, this.swimlaneId, this.listId); @@ -847,13 +847,13 @@ BlazeComponent.extendComponent({ .lists() .length; element.type = 'list'; - _id = element.copy(this.boardId, this.swimlaneId); + _id = await element.copy(this.boardId, this.swimlaneId); } else if (this.isSwimlaneTemplateSearch) { element.sort = ReactiveCache.getBoard(this.boardId) .swimlanes() .length; element.type = 'swimlane'; - _id = element.copy(this.boardId); + _id = await element.copy(this.boardId); } else if (this.isBoardTemplateSearch) { Meteor.call( 'copyBoard', diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js index f6786c7d1..4bc22f468 100644 --- a/client/components/sidebar/sidebarFilters.js +++ b/client/components/sidebar/sidebarFilters.js @@ -105,10 +105,15 @@ BlazeComponent.extendComponent({ }, }).register('filterSidebar'); -function mutateSelectedCards(mutationName, ...args) { - ReactiveCache.getCards(MultiSelection.getMongoSelector(), {sort: ['sort']}).forEach(card => { - card[mutationName](...args); - }); +async function mutateSelectedCards(mutationNameOrCallback, ...args) { + const cards = ReactiveCache.getCards(MultiSelection.getMongoSelector(), {sort: ['sort']}); + for (const card of cards) { + if (typeof mutationNameOrCallback === 'function') { + await mutationNameOrCallback(card); + } else { + await card[mutationNameOrCallback](...args); + } + } } BlazeComponent.extendComponent({ @@ -441,28 +446,31 @@ Template.copySelectionPopup.events({ const cardId = instance.selectedCardId.get(); const position = instance.position.get(); - mutateSelectedCards((card) => { - const newCard = card.copy(boardId, swimlaneId, listId); - if (newCard) { - let sortIndex = 0; - if (cardId) { - const targetCard = ReactiveCache.getCard(cardId); - if (targetCard) { - if (position === 'above') { - sortIndex = targetCard.sort - 0.5; - } else { - sortIndex = targetCard.sort + 0.5; + mutateSelectedCards(async (card) => { + const newCardId = await card.copy(boardId, swimlaneId, listId); + if (newCardId) { + const newCard = ReactiveCache.getCard(newCardId); + if (newCard) { + let sortIndex = 0; + if (cardId) { + const targetCard = ReactiveCache.getCard(cardId); + if (targetCard) { + if (position === 'above') { + sortIndex = targetCard.sort - 0.5; + } else { + sortIndex = targetCard.sort + 0.5; + } + } + } else { + // To end + const board = ReactiveCache.getBoard(boardId); + const cards = board.cards({ swimlaneId, listId }).sort('sort'); + if (cards.length > 0) { + sortIndex = cards[cards.length - 1].sort + 1; } } - } else { - // To end - const board = ReactiveCache.getBoard(boardId); - const cards = board.cards({ swimlaneId, listId }).sort('sort'); - if (cards.length > 0) { - sortIndex = cards[cards.length - 1].sort + 1; - } + newCard.setSort(sortIndex); } - newCard.setSort(sortIndex); } }); EscapeActions.executeUpTo('multiselection'); diff --git a/models/cards.js b/models/cards.js index dbd2f39cb..87be370d8 100644 --- a/models/cards.js +++ b/models/cards.js @@ -573,6 +573,10 @@ Cards.helpers({ }, async mapCustomFieldsToBoard(boardId) { + // Guard against undefined/null customFields + if (!this.customFields || !Array.isArray(this.customFields)) { + return []; + } // Map custom fields to new board const result = []; for (const cf of this.customFields) { @@ -603,7 +607,7 @@ Cards.helpers({ }, - copy(boardId, swimlaneId, listId) { + async copy(boardId, swimlaneId, listId) { const oldId = this._id; const oldCard = ReactiveCache.getCard(oldId); @@ -633,7 +637,7 @@ Cards.helpers({ delete this.labelIds; this.labelIds = newCardLabels; - this.customFields = this.mapCustomFieldsToBoard(newBoard._id); + this.customFields = await this.mapCustomFieldsToBoard(newBoard._id); } delete this._id; @@ -2097,7 +2101,7 @@ Cards.helpers({ cardNumber: newCardNumber }); - mutatedFields.customFields = this.mapCustomFieldsToBoard(newBoard._id); + mutatedFields.customFields = await this.mapCustomFieldsToBoard(newBoard._id); } await Cards.updateAsync(this._id, { $set: mutatedFields }); @@ -3077,7 +3081,7 @@ if (Meteor.isServer) { * @param mergeCardValues this values into the copied card * @return the new card id */ - copyCard(cardId, boardId, swimlaneId, listId, insertAtTop, mergeCardValues) { + async copyCard(cardId, boardId, swimlaneId, listId, insertAtTop, mergeCardValues) { check(cardId, String); check(boardId, String); check(swimlaneId, String); @@ -3096,7 +3100,7 @@ if (Meteor.isServer) { card.sort = sort + 1; } - const ret = card.copy(boardId, swimlaneId, listId); + const ret = await card.copy(boardId, swimlaneId, listId); return ret; }, }); diff --git a/models/lists.js b/models/lists.js index bc5b102a3..77cfea3fd 100644 --- a/models/lists.js +++ b/models/lists.js @@ -196,7 +196,7 @@ Lists.allow({ }); Lists.helpers({ - copy(boardId, swimlaneId) { + async copy(boardId, swimlaneId) { const oldId = this._id; const oldSwimlaneId = this.swimlaneId || null; this.boardId = boardId; @@ -217,13 +217,16 @@ Lists.helpers({ } // Copy all cards in list - ReactiveCache.getCards({ + const cards = ReactiveCache.getCards({ swimlaneId: oldSwimlaneId, listId: oldId, archived: false, - }).forEach(card => { - card.copy(boardId, swimlaneId, _id); }); + for (const card of cards) { + await card.copy(boardId, swimlaneId, _id); + } + + return _id; }, async move(boardId, swimlaneId) { diff --git a/models/swimlanes.js b/models/swimlanes.js index 6eda75982..ce07eb53a 100644 --- a/models/swimlanes.js +++ b/models/swimlanes.js @@ -147,7 +147,7 @@ Swimlanes.allow({ }); Swimlanes.helpers({ - copy(boardId) { + async copy(boardId) { const oldId = this._id; const oldBoardId = this.boardId; this.boardId = boardId; @@ -163,12 +163,15 @@ Swimlanes.helpers({ } // Copy all lists in swimlane - ReactiveCache.getLists(query).forEach(list => { + const lists = ReactiveCache.getLists(query); + for (const list of lists) { list.type = 'list'; list.swimlaneId = oldId; list.boardId = boardId; - list.copy(boardId, _id); - }); + await list.copy(boardId, _id); + } + + return _id; }, async move(toBoardId) {