Fix copy move card at board and MultiSelect to have numbered target of board, card above or below. Added MultiSelect change color.

Thanks to mimZD and xet7 !

Fixes #6045
This commit is contained in:
Lauri Ojansivu 2025-12-29 19:09:45 +02:00
parent db4b04d837
commit 74f1dfde72
9 changed files with 510 additions and 30 deletions

View file

@ -828,17 +828,29 @@ template(name="copyAndMoveCard")
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each boards
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{title}}
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}}") {{title}}
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{title}}
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'cards'}}:
select.js-select-cards
each cards
option(value="{{_id}}") {{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'}}

View file

@ -31,6 +31,7 @@ import CardComments from '/models/cardComments';
import { ALLOWED_COLORS } from '/config/const';
import { UserAvatar } from '../users/userAvatar';
import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList';
import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard';
import { handleFileUpload } from './attachments';
import uploadProgressManager from '../../lib/uploadProgressManager';
@ -973,26 +974,42 @@ Template.editCardAssignerForm.events({
});
/** Move Card Dialog */
(class extends DialogWithBoardSwimlaneList {
(class extends DialogWithBoardSwimlaneListCard {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret;
}
setDone(boardId, swimlaneId, listId, options) {
setDone(cardId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data();
const minOrder = card.getMinSort(listId, swimlaneId);
card.move(boardId, swimlaneId, listId, minOrder - 1);
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;
}
}
} else {
// If no card selected, move to end
sortIndex = card.getMaxSort(options.listId, options.swimlaneId) + 1;
}
card.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
}
}).register('moveCardPopup');
/** Copy Card Dialog */
(class extends DialogWithBoardSwimlaneList {
(class extends DialogWithBoardSwimlaneListCard {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret;
}
setDone(boardId, swimlaneId, listId, options) {
setDone(cardId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data();
@ -1001,8 +1018,30 @@ Template.editCardAssignerForm.events({
const title = textarea.val().trim();
if (title) {
// insert new card to the top of new list
const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, listId, true, {title: title});
const newCardId = Meteor.call('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, true, {title: title});
// Position the copied card
if (newCardId) {
const newCard = ReactiveCache.getCard(newCardId);
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;
}
}
} else {
// If no card selected, copy to end
sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1;
}
newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
}
// In case the filter is active we need to add the newly inserted card in
// the list of exceptions -- cards that are not filtered. Otherwise the
@ -1014,12 +1053,12 @@ Template.editCardAssignerForm.events({
}).register('copyCardPopup');
/** Convert Checklist-Item to card dialog */
(class extends DialogWithBoardSwimlaneList {
(class extends DialogWithBoardSwimlaneListCard {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret;
}
setDone(boardId, swimlaneId, listId, options) {
setDone(cardId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data();
@ -1029,14 +1068,29 @@ Template.editCardAssignerForm.events({
if (title) {
const _id = Cards.insert({
title: title,
listId: listId,
boardId: boardId,
swimlaneId: swimlaneId,
listId: options.listId,
boardId: options.boardId,
swimlaneId: options.swimlaneId,
sort: 0,
});
const card = ReactiveCache.getCard(_id);
const minOrder = card.getMinSort();
card.move(card.boardId, card.swimlaneId, card.listId, minOrder - 1);
const newCard = ReactiveCache.getCard(_id);
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;
}
}
} else {
sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1;
}
newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
Filter.addException(_id);
}
@ -1044,12 +1098,12 @@ Template.editCardAssignerForm.events({
}).register('convertChecklistItemToCardPopup');
/** Copy many cards dialog */
(class extends DialogWithBoardSwimlaneList {
(class extends DialogWithBoardSwimlaneListCard {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret;
}
setDone(boardId, swimlaneId, listId, options) {
setDone(cardId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data();
@ -1059,7 +1113,29 @@ Template.editCardAssignerForm.events({
if (title) {
const titleList = JSON.parse(title);
for (const obj of titleList) {
const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, listId, false, {title: obj.title, description: obj.description});
const newCardId = Meteor.call('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, false, {title: obj.title, description: obj.description});
// Position the copied card
if (newCardId) {
const newCard = ReactiveCache.getCard(newCardId);
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;
}
}
} else {
sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1;
}
newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
}
// In case the filter is active we need to add the newly inserted card in
// the list of exceptions -- cards that are not filtered. Otherwise the
@ -1109,6 +1185,51 @@ BlazeComponent.extendComponent({
},
}).register('setCardColorPopup');
BlazeComponent.extendComponent({
onCreated() {
this.currentColor = new ReactiveVar(null);
},
colors() {
return ALLOWED_COLORS.map((color) => ({ color, name: '' }));
},
isSelected(color) {
return this.currentColor.get() === color;
},
events() {
return [
{
'click .js-palette-color'(event) {
// Extract color from class name like "card-details-red"
const classes = $(event.currentTarget).attr('class').split(' ');
const colorClass = classes.find(cls => cls.startsWith('card-details-'));
const color = colorClass ? colorClass.replace('card-details-', '') : null;
this.currentColor.set(color);
},
'click .js-submit'(event) {
event.preventDefault();
const color = this.currentColor.get();
// Use MultiSelection to get selected cards and set color on each
ReactiveCache.getCards(MultiSelection.getMongoSelector()).forEach(card => {
card.setColor(color);
});
Popup.back();
},
'click .js-remove-color'(event) {
event.preventDefault();
// Use MultiSelection to get selected cards and remove color from each
ReactiveCache.getCards(MultiSelection.getMongoSelector()).forEach(card => {
card.setColor(null);
});
Popup.back();
},
},
];
},
}).register('setSelectionColorPopup');
BlazeComponent.extendComponent({
onCreated() {
this.currentCard = this.currentData();

View file

@ -162,6 +162,9 @@ body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-
border-radius: 3px;
background: #e6e6e6;
}
.sidebar .sidebar-content .sidebar-btn * {
color: #fff;
}
.sidebar .sidebar-content .sidebar-btn:hover * {
color: #fff;
}

View file

@ -206,6 +206,12 @@ template(name="multiselectionSidebar")
| ⋯
if currentUser.isBoardAdmin
hr
a.sidebar-btn.js-selection-color
| 🎨
span {{_ 'selection-color'}}
a.sidebar-btn.js-copy-selection
| 📋
span {{_ 'copy-selection'}}
a.sidebar-btn.js-move-selection
| 📤
span {{_ 'move-selection'}}
@ -224,4 +230,76 @@ template(name="disambiguateMultiMemberPopup")
button.wide.js-assign-member {{_ 'assign-member'}}
template(name="moveSelectionPopup")
+boardLists
h3 {{_ 'moveSelectionPopup-title'}}
label {{_ 'boards'}}:
select.js-select-boards
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}}. {{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}}") {{add @index 1}}. {{title}}
div
input(type="radio" name="position" value="above" checked id="position-above-move" style="display: inline")
label(for="position-above-move") {{_ 'above-selected-card'}}
div
input(type="radio" name="position" value="below" id="position-below-move" style="display: inline")
label(for="position-below-move") {{_ 'below-selected-card'}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
template(name="copySelectionPopup")
h3 {{_ 'copySelectionPopup-title'}}
label {{_ 'boards'}}:
select.js-select-boards
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}}. {{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}}") {{add @index 1}}. {{title}}
div
input(type="radio" name="position" value="above" checked id="position-above-copy" style="display: inline")
label(for="position-above-copy") {{_ 'above-selected-card'}}
div
input(type="radio" name="position" value="below" id="position-below-copy" style="display: inline")
label(for="position-below-copy") {{_ 'below-selected-card'}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
template(name="setSelectionColorPopup")
h3 {{_ 'setSelectionColorPopup-title'}}
form.edit-label
.palette-colors: each colors
unless $eq color 'white'
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color)
| ✅
button.primary.confirm.js-submit {{_ 'save'}}
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}

View file

@ -162,6 +162,8 @@ BlazeComponent.extendComponent({
}
},
'click .js-move-selection': Popup.open('moveSelection'),
'click .js-copy-selection': Popup.open('copySelection'),
'click .js-selection-color': Popup.open('setSelectionColor'),
'click .js-archive-selection'() {
mutateSelectedCards('archive');
EscapeActions.executeUpTo('multiselection');
@ -202,10 +204,267 @@ Template.disambiguateMultiMemberPopup.events({
},
});
Template.moveSelectionPopup.onCreated(function() {
this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
this.selectedSwimlaneId = new ReactiveVar('');
this.selectedListId = new ReactiveVar('');
this.selectedCardId = new ReactiveVar('');
this.position = new ReactiveVar('above');
this.getBoardData = function(boardId) {
const self = this;
Meteor.subscribe('board', boardId, false, {
onReady() {
const sameBoardId = self.selectedBoardId.get() === boardId;
self.selectedBoardId.set(boardId);
if (!sameBoardId) {
self.setFirstSwimlaneId();
self.setFirstListId();
}
},
});
};
this.setFirstSwimlaneId = function() {
try {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
const swimlaneId = board.swimlanes()[0]._id;
this.selectedSwimlaneId.set(swimlaneId);
} catch (e) {}
};
this.setFirstListId = function() {
try {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
const listId = board.lists()[0]._id;
this.selectedListId.set(listId);
} catch (e) {}
};
this.getBoardData(Session.get('currentBoard'));
this.setFirstSwimlaneId();
this.setFirstListId();
});
Template.moveSelectionPopup.helpers({
boards() {
return ReactiveCache.getBoards(
{
archived: false,
'members.userId': Meteor.userId(),
_id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() },
},
{
sort: { sort: 1 },
},
);
},
swimlanes() {
const board = ReactiveCache.getBoard(Template.instance().selectedBoardId.get());
return board ? board.swimlanes() : [];
},
lists() {
const board = ReactiveCache.getBoard(Template.instance().selectedBoardId.get());
return board ? board.lists() : [];
},
cards() {
const instance = Template.instance();
const list = ReactiveCache.getList(instance.selectedListId.get());
if (!list) return [];
return list.cards(instance.selectedSwimlaneId.get()).sort((a, b) => a.sort - b.sort);
},
isDialogOptionBoardId(boardId) {
return Template.instance().selectedBoardId.get() === boardId;
},
isDialogOptionSwimlaneId(swimlaneId) {
return Template.instance().selectedSwimlaneId.get() === swimlaneId;
},
isDialogOptionListId(listId) {
return Template.instance().selectedListId.get() === listId;
},
});
Template.moveSelectionPopup.events({
'click .js-select-list'() {
// Move the minicard to the end of the target list
mutateSelectedCards('moveToEndOfList', { listId: this._id });
'change .js-select-boards'(event) {
const boardId = $(event.currentTarget).val();
Template.instance().getBoardData(boardId);
},
'change .js-select-swimlanes'(event) {
Template.instance().selectedSwimlaneId.set($(event.currentTarget).val());
},
'change .js-select-lists'(event) {
Template.instance().selectedListId.set($(event.currentTarget).val());
},
'change .js-select-cards'(event) {
Template.instance().selectedCardId.set($(event.currentTarget).val());
},
'change input[name="position"]'(event) {
Template.instance().position.set($(event.currentTarget).val());
},
'click .js-done'() {
const instance = Template.instance();
const boardId = instance.selectedBoardId.get();
const swimlaneId = instance.selectedSwimlaneId.get();
const listId = instance.selectedListId.get();
const cardId = instance.selectedCardId.get();
const position = instance.position.get();
// Calculate sortIndex
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 {
// If no card selected, move 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;
}
}
mutateSelectedCards('move', boardId, swimlaneId, listId, sortIndex);
EscapeActions.executeUpTo('multiselection');
},
});
Template.copySelectionPopup.onCreated(function() {
this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
this.selectedSwimlaneId = new ReactiveVar('');
this.selectedListId = new ReactiveVar('');
this.selectedCardId = new ReactiveVar('');
this.position = new ReactiveVar('above');
this.getBoardData = function(boardId) {
const self = this;
Meteor.subscribe('board', boardId, false, {
onReady() {
const sameBoardId = self.selectedBoardId.get() === boardId;
self.selectedBoardId.set(boardId);
if (!sameBoardId) {
self.setFirstSwimlaneId();
self.setFirstListId();
}
},
});
};
this.setFirstSwimlaneId = function() {
try {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
const swimlaneId = board.swimlanes()[0]._id;
this.selectedSwimlaneId.set(swimlaneId);
} catch (e) {}
};
this.setFirstListId = function() {
try {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
const listId = board.lists()[0]._id;
this.selectedListId.set(listId);
} catch (e) {}
};
this.getBoardData(Session.get('currentBoard'));
this.setFirstSwimlaneId();
this.setFirstListId();
});
Template.copySelectionPopup.helpers({
boards() {
return ReactiveCache.getBoards(
{
archived: false,
'members.userId': Meteor.userId(),
_id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() },
},
{
sort: { sort: 1 },
},
);
},
swimlanes() {
const board = ReactiveCache.getBoard(Template.instance().selectedBoardId.get());
return board ? board.swimlanes() : [];
},
lists() {
const board = ReactiveCache.getBoard(Template.instance().selectedBoardId.get());
return board ? board.lists() : [];
},
cards() {
const instance = Template.instance();
const list = ReactiveCache.getList(instance.selectedListId.get());
if (!list) return [];
return list.cards(instance.selectedSwimlaneId.get()).sort((a, b) => a.sort - b.sort);
},
isDialogOptionBoardId(boardId) {
return Template.instance().selectedBoardId.get() === boardId;
},
isDialogOptionSwimlaneId(swimlaneId) {
return Template.instance().selectedSwimlaneId.get() === swimlaneId;
},
isDialogOptionListId(listId) {
return Template.instance().selectedListId.get() === listId;
},
});
Template.copySelectionPopup.events({
'change .js-select-boards'(event) {
const boardId = $(event.currentTarget).val();
Template.instance().getBoardData(boardId);
},
'change .js-select-swimlanes'(event) {
Template.instance().selectedSwimlaneId.set($(event.currentTarget).val());
},
'change .js-select-lists'(event) {
Template.instance().selectedListId.set($(event.currentTarget).val());
},
'change .js-select-cards'(event) {
Template.instance().selectedCardId.set($(event.currentTarget).val());
},
'change input[name="position"]'(event) {
Template.instance().position.set($(event.currentTarget).val());
},
'click .js-done'() {
const instance = Template.instance();
const boardId = instance.selectedBoardId.get();
const swimlaneId = instance.selectedSwimlaneId.get();
const listId = instance.selectedListId.get();
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;
}
}
} 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);
}
});
EscapeActions.executeUpTo('multiselection');
},
});

View file

@ -80,3 +80,5 @@ Blaze.registerHelper('canMoveCard', () =>
Blaze.registerHelper('canModifyBoard', () =>
Utils.canModifyBoard(),
);
Blaze.registerHelper('add', (a, b) => a + b);

View file

@ -45,7 +45,7 @@ export class DialogWithBoardSwimlaneListCard extends DialogWithBoardSwimlaneList
const swimlaneId = swimlaneSelect.options[swimlaneSelect.selectedIndex].value;
const cardSelect = this.$('.js-select-cards')[0];
const cardId = cardSelect.options[cardSelect.selectedIndex].value;
const cardId = cardSelect.options.length > 0 ? cardSelect.options[cardSelect.selectedIndex].value : null;
const options = {
'boardId' : boardId,

View file

@ -561,6 +561,8 @@
"moveCardToBottom-title": "Move to Bottom",
"moveCardToTop-title": "Move to Top",
"moveSelectionPopup-title": "Move selection",
"copySelectionPopup-title": "Copy selection",
"selection-color": "Selection Color",
"multi-selection": "Multi-Selection",
"multi-selection-label": "Set label for selection",
"multi-selection-member": "Set member for selection",
@ -767,6 +769,7 @@
"editCardReceivedDatePopup-title": "Change received date",
"editCardEndDatePopup-title": "Change end date",
"setCardColorPopup-title": "Set color",
"setSelectionColorPopup-title": "Set selection color",
"setCardActionsColorPopup-title": "Choose a color",
"setSwimlaneColorPopup-title": "Choose a color",
"setListColorPopup-title": "Choose a color",
@ -957,6 +960,8 @@
"a-endAt": "modified ending time to be",
"a-startAt": "modified starting time to be",
"a-receivedAt": "modified received time to be",
"above-selected-card": "Above selected card",
"below-selected-card": "Below selected card",
"almostdue": "current due time %s is approaching",
"pastdue": "current due time %s is past",
"duenow": "current due time %s is today",

View file

@ -2018,8 +2018,8 @@ Cards.mutations({
};
},
moveToEndOfList({ listId } = {}) {
let swimlaneId = this.swimlaneId;
moveToEndOfList({ listId, swimlaneId } = {}) {
swimlaneId = swimlaneId || this.swimlaneId;
const boardId = this.boardId;
let sortIndex = 0;
@ -2030,7 +2030,7 @@ Cards.mutations({
swimlaneId = board.getDefaultSwimline()._id;
}
// Move the minicard to the end of the target list
let parentElementDom = $(`#swimlane-${this.swimlaneId}`).get(0);
let parentElementDom = $(`#swimlane-${swimlaneId}`).get(0);
if (!parentElementDom) parentElementDom = $(':root');
const lastCardDom = $(parentElementDom)