Fix move and copy popup duplicate view.

Thanks to mimZD and xet7 !

Fixes #6102
This commit is contained in:
Lauri Ojansivu 2026-02-07 05:44:34 +02:00
parent 5836e50e69
commit 631c250f40
3 changed files with 165 additions and 61 deletions

View file

@ -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)

View file

@ -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();
});
},
});

View file

@ -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');
},