From 631c250f403172937b76ddd37bab54bc9b6dbb78 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Sat, 7 Feb 2026 05:44:34 +0200 Subject: [PATCH] Fix move and copy popup duplicate view. Thanks to mimZD and xet7 ! Fixes #6102 --- client/components/cards/cardDetails.jade | 96 +++++++++++++++++++-- client/components/cards/minicard.js | 38 ++------ client/components/sidebar/sidebarFilters.js | 92 +++++++++++++++----- 3 files changed, 165 insertions(+), 61 deletions(-) diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade index 8ac96dd16..7adaca873 100644 --- a/client/components/cards/cardDetails.jade +++ b/client/components/cards/cardDetails.jade @@ -847,27 +847,111 @@ template(name="exportCardPopup") | {{_ 'export-card-pdf'}} template(name="moveCardPopup") - +copyAndMoveCard + unless currentUser.isWorker + label {{_ 'boards'}}: + select.js-select-boards(autofocus) + each boards + option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}} + + label {{_ 'swimlanes'}}: + select.js-select-swimlanes + each swimlanes + option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}} + + label {{_ 'lists'}}: + select.js-select-lists + each lists + option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}} + + label {{_ 'cards'}}: + select.js-select-cards + each cards + option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}} + + div + input(type="radio" name="position" value="above" checked id="position-above" style="display: inline") + label(for="position-above") {{_ 'above-selected-card'}} + div + input(type="radio" name="position" value="below" id="position-below" style="display: inline") + label(for="position-below") {{_ 'below-selected-card'}} + + .edit-controls.clearfix + button.primary.confirm.js-done {{_ 'done'}} template(name="copyCardPopup") label(for='copy-card-title') {{_ 'title'}}: textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus) = getTitle - +copyAndMoveCard + unless currentUser.isWorker + label {{_ 'boards'}}: + select.js-select-boards(autofocus) + each boards + option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}} + + label {{_ 'swimlanes'}}: + select.js-select-swimlanes + each swimlanes + option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}} + + label {{_ 'lists'}}: + select.js-select-lists + each lists + option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}} + + label {{_ 'cards'}}: + select.js-select-cards + each cards + option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}} + + div + input(type="radio" name="position" value="above" checked id="position-above" style="display: inline") + label(for="position-above") {{_ 'above-selected-card'}} + div + input(type="radio" name="position" value="below" id="position-below" style="display: inline") + label(for="position-below") {{_ 'below-selected-card'}} + + .edit-controls.clearfix + button.primary.confirm.js-done {{_ 'done'}} template(name="copyManyCardsPopup") label(for='copy-checklist-cards-title') {{_ 'copyManyCardsPopup-instructions'}}: textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus) | {{_ 'copyManyCardsPopup-format'}} - +copyAndMoveCard + unless currentUser.isWorker + label {{_ 'boards'}}: + select.js-select-boards(autofocus) + each boards + option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}} + + label {{_ 'swimlanes'}}: + select.js-select-swimlanes + each swimlanes + option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}} + + label {{_ 'lists'}}: + select.js-select-lists + each lists + option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}} + + label {{_ 'cards'}}: + select.js-select-cards + each cards + option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}} + + div + input(type="radio" name="position" value="above" checked id="position-above" style="display: inline") + label(for="position-above") {{_ 'above-selected-card'}} + div + input(type="radio" name="position" value="below" id="position-below" style="display: inline") + label(for="position-below") {{_ 'below-selected-card'}} + + .edit-controls.clearfix + button.primary.confirm.js-done {{_ 'done'}} template(name="convertChecklistItemToCardPopup") label(for='convert-checklist-item-to-card-title') {{_ 'title'}}: textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus) = item.title - +copyAndMoveCard - -template(name="copyAndMoveCard") unless currentUser.isWorker label {{_ 'boards'}}: select.js-select-boards(autofocus) diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js index 22ce35e62..8be2ed4be 100644 --- a/client/components/cards/minicard.js +++ b/client/components/cards/minicard.js @@ -115,7 +115,11 @@ BlazeComponent.extendComponent({ }, 'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"), 'click .minicard-labels' : this.cardLabelsPopup, - 'click .js-open-minicard-details-menu': Popup.open('cardDetailsActions'), + 'click .js-open-minicard-details-menu'(event) { + event.preventDefault(); + event.stopPropagation(); + Popup.open('cardDetailsActions').call(this, event); + }, // Drag and drop file upload handlers 'dragover .minicard'(event) { // Only prevent default for file drags to avoid interfering with sortable @@ -306,35 +310,3 @@ BlazeComponent.extendComponent({ } }).register('editCardSortOrderPopup'); -Template.cardDetailsActionsPopup.events({ - 'click .js-due-date': Popup.open('editCardDueDate'), - 'click .js-move-card': Popup.open('moveCard'), - 'click .js-copy-card': Popup.open('copyCard'), - 'click .js-set-card-color': Popup.open('setCardColor'), - 'click .js-add-labels': Popup.open('cardLabels'), - 'click .js-link': Popup.open('linkCard'), - 'click .js-move-card-to-top'(event) { - event.preventDefault(); - const minOrder = this.getMinSort(); - this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1); - Popup.back(); - }, - async 'click .js-move-card-to-bottom'(event) { - event.preventDefault(); - const maxOrder = this.getMaxSort(); - await this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1); - Popup.back(); - }, - 'click .js-archive': Popup.afterConfirm('cardArchive', async function () { - Popup.close(); - await this.archive(); - Utils.goBoardId(this.boardId); - }), - 'click .js-toggle-watch-card'() { - const currentCard = this; - const level = currentCard.findWatcher(Meteor.userId()) ? null : 'watching'; - Meteor.call('watch', 'card', currentCard._id, level, (err, ret) => { - if (!err && ret) Popup.back(); - }); - }, -}); diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js index 4bc22f468..e69e0e5bd 100644 --- a/client/components/sidebar/sidebarFilters.js +++ b/client/components/sidebar/sidebarFilters.js @@ -1,4 +1,5 @@ import { ReactiveCache } from '/imports/reactiveCache'; +import { TAPi18n } from '/imports/i18n'; const subManager = new SubsManager(); @@ -288,6 +289,25 @@ Template.moveSelectionPopup.helpers({ isDialogOptionListId(listId) { return Template.instance().selectedListId.get() === listId; }, + isTitleDefault(title) { + if ( + title.startsWith("key 'default") && + title.endsWith('returned an object instead of string.') + ) { + const translated = `${TAPi18n.__('defaultdefault')}`; + if ( + translated.startsWith("key 'default") && + translated.endsWith('returned an object instead of string.') + ) { + return 'Default'; + } + return translated; + } + if (title === 'Default') { + return `${TAPi18n.__('defaultdefault')}`; + } + return title; + }, }); Template.moveSelectionPopup.events({ @@ -329,7 +349,7 @@ Template.moveSelectionPopup.events({ } else { // If no card selected, move to end const board = ReactiveCache.getBoard(boardId); - const cards = board.cards({ swimlaneId, listId }).sort('sort'); + const cards = board.cards({ swimlaneId, listId }).sort((a, b) => a.sort - b.sort); if (cards.length > 0) { sortIndex = cards[cards.length - 1].sort + 1; } @@ -419,6 +439,25 @@ Template.copySelectionPopup.helpers({ isDialogOptionListId(listId) { return Template.instance().selectedListId.get() === listId; }, + isTitleDefault(title) { + if ( + title.startsWith("key 'default") && + title.endsWith('returned an object instead of string.') + ) { + const translated = `${TAPi18n.__('defaultdefault')}`; + if ( + translated.startsWith("key 'default") && + translated.endsWith('returned an object instead of string.') + ) { + return 'Default'; + } + return translated; + } + if (title === 'Default') { + return `${TAPi18n.__('defaultdefault')}`; + } + return title; + }, }); Template.copySelectionPopup.events({ @@ -447,31 +486,40 @@ Template.copySelectionPopup.events({ const position = instance.position.get(); 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; - } - } + const newCardId = await Meteor.callAsync( + 'copyCard', + card._id, + boardId, + swimlaneId, + listId, + true, + { title: card.title }, + ); + if (!newCardId) return; + + const newCard = ReactiveCache.getCard(newCardId); + if (!newCard) return; + + let sortIndex = 0; + if (cardId) { + const targetCard = ReactiveCache.getCard(cardId); + if (targetCard) { + if (position === 'above') { + 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; - } + sortIndex = targetCard.sort + 0.5; } - newCard.setSort(sortIndex); + } + } else { + // To end + const board = ReactiveCache.getBoard(boardId); + const cards = board.cards({ swimlaneId, listId }).sort((a, b) => a.sort - b.sort); + if (cards.length > 0) { + sortIndex = cards[cards.length - 1].sort + 1; } } + + await newCard.move(boardId, swimlaneId, listId, sortIndex); }); EscapeActions.executeUpTo('multiselection'); },