diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade index e5bf2de4f..e823c6395 100644 --- a/client/components/cards/cardDetails.jade +++ b/client/components/cards/cardDetails.jade @@ -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'}} diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 99eeaee5c..760e96e50 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -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(); diff --git a/client/components/sidebar/sidebar.css b/client/components/sidebar/sidebar.css index f6b237978..5b0ad44cf 100644 --- a/client/components/sidebar/sidebar.css +++ b/client/components/sidebar/sidebar.css @@ -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; } diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade index 433d6daa8..ae14cdae9 100644 --- a/client/components/sidebar/sidebarFilters.jade +++ b/client/components/sidebar/sidebarFilters.jade @@ -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'}} diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js index 64c6f3d8c..f6786c7d1 100644 --- a/client/components/sidebar/sidebarFilters.js +++ b/client/components/sidebar/sidebarFilters.js @@ -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'); }, }); diff --git a/client/config/blazeHelpers.js b/client/config/blazeHelpers.js index 333913dfc..87418921a 100644 --- a/client/config/blazeHelpers.js +++ b/client/config/blazeHelpers.js @@ -80,3 +80,5 @@ Blaze.registerHelper('canMoveCard', () => Blaze.registerHelper('canModifyBoard', () => Utils.canModifyBoard(), ); + +Blaze.registerHelper('add', (a, b) => a + b); diff --git a/client/lib/dialogWithBoardSwimlaneListCard.js b/client/lib/dialogWithBoardSwimlaneListCard.js index a516fde72..38befd4df 100644 --- a/client/lib/dialogWithBoardSwimlaneListCard.js +++ b/client/lib/dialogWithBoardSwimlaneListCard.js @@ -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, diff --git a/imports/i18n/data/en.i18n.json b/imports/i18n/data/en.i18n.json index a75041a1a..1bd9d162c 100644 --- a/imports/i18n/data/en.i18n.json +++ b/imports/i18n/data/en.i18n.json @@ -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", diff --git a/models/cards.js b/models/cards.js index 2d7c20c26..023c918ee 100644 --- a/models/cards.js +++ b/models/cards.js @@ -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)