Resolve merge conflicts by accepting PR #6131 changes

Co-authored-by: xet7 <15545+xet7@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-02-07 16:30:08 +00:00
parent dc0b68ee80
commit 97dd5d2064
257 changed files with 9483 additions and 14103 deletions

View file

@ -3,16 +3,168 @@ import { TAPi18n } from '/imports/i18n';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { Spinner } from '/client/lib/spinner';
import getSlug from 'limax';
import { itemsSelector } from './list';
const subManager = new SubsManager();
const InfiniteScrollIter = 10;
function sortableCards(boardComponent, $cards) {
return {
connectWith: '.js-minicards:not(.js-list-full)',
tolerance: 'pointer',
appendTo: '.board-canvas',
helper(evt, item) {
const helper = item.clone();
const cardHeight = item.height();
const cardWidth = item.width();
helper[0].setAttribute('style', `height: ${cardHeight}px !important; width: ${cardWidth}px !important;`);
if (MultiSelection.isActive()) {
const andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
if (andNOthers > 0) {
helper.append(
$(
Blaze.toHTML(
HTML.DIV(
{ class: 'and-n-other' },
TAPi18n.__('and-n-other-card', { count: andNOthers }),
),
),
),
);
}
}
return helper;
},
distance: 7,
items: itemsSelector,
placeholder: 'minicard-wrapper placeholder',
/* cursor must be tied to smaller objects, position approximately from the button
(can be computed if visually confusing) */
cursorAt: { right: 20, top: 30 },
start(evt, ui) {
const cardHeight = ui.helper.height();
ui.placeholder[0].setAttribute('style', `height: ${cardHeight}px !important;`);
EscapeActions.executeUpTo('popup-close');
boardComponent.setIsDragging(true);
},
stop(evt, ui) {
// To attribute the new index number, we need to get the DOM element
// of the previous and the following card -- if any.
const prevCardDom = ui.item.prev('.js-minicard').get(0);
const nextCardDom = ui.item.next('.js-minicard').get(0);
const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
const sortIndex = Utils.calculateIndex(prevCardDom, nextCardDom, nCards);
const listId = Blaze.getData(ui.item.parents('.list-body').get(0))._id;
const currentBoard = Utils.getCurrentBoard();
const defaultSwimlaneId = currentBoard.getDefaultSwimline()._id;
let targetSwimlaneId = null;
// only set a new swimelane ID if the swimlanes view is active
if (
Utils.boardView() === 'board-view-swimlanes' ||
currentBoard.isTemplatesBoard()
)
targetSwimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))
._id;
// Normally the jquery-ui sortable library moves the dragged DOM element
// to its new position, which disrupts Blaze reactive updates mechanism
// (especially when we move the last card of a list, or when multiple
// users move some cards at the same time). To prevent these UX glitches
// we ask sortable to gracefully cancel the move, and to put back the
// DOM in its initial state. The card move is then handled reactively by
// Blaze with the below query.
$cards.sortable('cancel');
if (MultiSelection.isActive()) {
ReactiveCache.getCards(MultiSelection.getMongoSelector(), { sort: ['sort'] }).forEach((card, i) => {
const newSwimlaneId = targetSwimlaneId
? targetSwimlaneId
: card.swimlaneId || defaultSwimlaneId;
card.move(
currentBoard._id,
newSwimlaneId,
listId,
sortIndex.base + i * sortIndex.increment,
);
});
} else {
const cardDomElement = ui.item.get(0);
const card = Blaze.getData(cardDomElement);
const newSwimlaneId = targetSwimlaneId
? targetSwimlaneId
: card.swimlaneId || defaultSwimlaneId;
card.move(currentBoard._id, newSwimlaneId, listId, sortIndex.base);
}
boardComponent.setIsDragging(false);
},
sort(event, ui) {
Utils.scrollIfNeeded(event);
},
};
};
BlazeComponent.extendComponent({
onCreated() {
// for infinite scrolling
this.cardlimit = new ReactiveVar(InfiniteScrollIter);
},
onRendered() {
// Prefer handling drag/sort in listBody rather than list as
// it is shared between mobile and desktop view
const boardComponent = BlazeComponent.getComponentForElement(document.getElementsByClassName('board-canvas')[0]);
const $cards = this.$('.js-minicards');
$cards.sortable(sortableCards(boardComponent, $cards));
this.autorun(() => {
if ($cards.data('uiSortable') || $cards.data('sortable')) {
// Use handle button on mobile, classic move otherwise
if (Utils.isMiniScreen()) {
$cards.sortable('option', 'handle', '.handle');
} else {
$cards.sortable('option', 'handle', '.minicard');
}
$cards.sortable(
'option',
'disabled',
// Disable drag-dropping when user is not member
!Utils.canModifyBoard(),
// Not disable drag-dropping while in multi-selection mode
// MultiSelection.isActive() || !Utils.canModifyBoard(),
);
}
});
// We want to re-run this function any time a card is added.
this.autorun(() => {
const currentBoardId = Tracker.nonreactive(() => {
return Session.get('currentBoard');
});
Tracker.afterFlush(() => {
$cards.find(itemsSelector).droppable({
hoverClass: 'draggable-hover-card',
accept: '.js-member,.js-label',
drop(event, ui) {
const cardId = Blaze.getData(this)._id;
const card = ReactiveCache.getCard(cardId);
if (ui.draggable.hasClass('js-member')) {
const memberId = Blaze.getData(ui.draggable.get(0)).userId;
card.assignMember(memberId);
} else {
const labelId = Blaze.getData(ui.draggable.get(0))._id;
card.addLabel(labelId);
}
},
});
});
});
},
mixins() {
return [];
},
@ -82,9 +234,10 @@ BlazeComponent.extendComponent({
evt.preventDefault();
const firstCardDom = this.find('.js-minicard:first');
const lastCardDom = this.find('.js-minicard:last');
const textarea = $(evt.currentTarget).find('textarea');
// more robust to start from the form
const textarea = $(evt.currentTarget).closest('.inlined-form').find('textarea');
const position = this.currentData().position;
const title = textarea.val().trim();
const title = $(textarea).val().trim();
let sortIndex;
if (position === 'top') {
@ -168,7 +321,6 @@ BlazeComponent.extendComponent({
// We keep the form opened, empty it, and scroll to it.
textarea.val('').focus();
autosize.update(textarea);
if (position === 'bottom') {
this.scrollToBottom();
}
@ -194,21 +346,19 @@ BlazeComponent.extendComponent({
clickOnMiniCard(evt) {
if (MultiSelection.isActive() || evt.shiftKey) {
evt.stopImmediatePropagation();
evt.preventDefault();
const methodName = evt.shiftKey ? 'toggleRange' : 'toggle';
MultiSelection[methodName](this.currentData()._id);
// If the card is already selected, we want to de-select it.
// XXX We should probably modify the minicard href attribute instead of
// overwriting the event in case the card is already selected.
} else if (Utils.isMiniScreen()) {
evt.preventDefault();
Session.set('popupCardId', this.currentData()._id);
this.cardDetailsPopup(evt);
} else if (Session.equals('currentCard', this.currentData()._id)) {
evt.stopImmediatePropagation();
evt.preventDefault();
// We need to wait a little because router gets called first,
// we probably need a level of indirection
// #FIXME remove if it works with commits we rebased on,
// which change the route declaration order
Meteor.setTimeout(() => {
Session.set('currentCard', null)
}, 50);
Utils.goBoardId(Session.get('currentBoard'));
} else {
// Allow normal href navigation, but if it's the same card URL,
@ -283,12 +433,6 @@ BlazeComponent.extendComponent({
return user && user.isVerticalScrollbars();
},
cardDetailsPopup(event) {
if (!Popup.isOpen()) {
Popup.open("cardDetails")(event);
}
},
events() {
return [
{
@ -296,6 +440,8 @@ BlazeComponent.extendComponent({
'click .js-toggle-multi-selection': this.toggleMultiSelection,
'click .open-minicard-composer': this.scrollToBottom,
submit: this.addCard,
// #FIXME remove in final MR if it works
'click .confirm': this.addCard
},
];
},
@ -401,6 +547,17 @@ BlazeComponent.extendComponent({
'click .js-link': Popup.open('linkCard'),
'click .js-search': Popup.open('searchElement'),
'click .js-card-template': Popup.open('searchElement'),
submit: this.addCard,
'click .minicard-label': (event) => {
const clickedData = BlazeComponent.getComponentForElement(event.target).currentData?.()
this.labels.set(this.labels.get().filter(e => e !== clickedData?._id));
},
'click .member': (event) => {
const clickedData = BlazeComponent.getComponentForElement(event.target).currentData?.()
this.members.set(this.members.get().filter(e => e !== clickedData?.userId));
e.preventDefault();
e.stopPropagation();
},
},
];
},
@ -409,8 +566,6 @@ BlazeComponent.extendComponent({
const editor = this;
const $textarea = this.$('textarea');
autosize($textarea);
$textarea.escapeableTextComplete(
[
// User mentions
@ -421,7 +576,9 @@ BlazeComponent.extendComponent({
callback(
$.map(currentBoard.activeMembers(), member => {
const user = ReactiveCache.getUser(member.userId);
return user.username.indexOf(term) === 0 ? user : null;
return user.username.indexOf(term) === 0 &&
// don't show already selected members
!editor.members.get().find((e) => e === member.userId) ? user : null;
}),
);
},
@ -445,8 +602,12 @@ BlazeComponent.extendComponent({
const currentBoard = Utils.getCurrentBoard();
callback(
$.map(currentBoard.labels, label => {
if (label.name == undefined) {
label.name = "";
if (
label.name == undefined ||
// don't show already selected labels
editor.getLabels().find((e) => e._id === label._id)
) {
return null;
}
if (
label.name.indexOf(term) > -1 ||
@ -503,10 +664,10 @@ BlazeComponent.extendComponent({
subManager.subscribe('board', this.boardId, false);
this.board = ReactiveCache.getBoard(this.boardId);
// List where to insert card
this.list = $(Popup._getTopStack().openerElement).closest('.js-list');
this.list = $(PopupComponent.stack[0].openerElement).closest('.js-list');
this.listId = Blaze.getData(this.list[0])._id;
// Swimlane where to insert card
const swimlane = $(Popup._getTopStack().openerElement).closest(
const swimlane = $(PopupComponent.stack[0].openerElement).closest(
'.js-swimlane',
);
this.swimlaneId = '';
@ -539,10 +700,10 @@ BlazeComponent.extendComponent({
if (!board) {
return [];
}
// Ensure default swimlane exists
board.getDefaultSwimline();
const swimlanes = ReactiveCache.getSwimlanes(
{
boardId: this.selectedBoardId.get()
@ -559,7 +720,8 @@ BlazeComponent.extendComponent({
}
const lists = ReactiveCache.getLists(
{
boardId: this.selectedBoardId.get()
boardId: this.selectedBoardId.get(),
swimlaneId: this.selectedSwimlaneId?.get?.()
},
{
sort: { sort: 1 },
@ -703,16 +865,16 @@ BlazeComponent.extendComponent({
},
onCreated() {
this.isCardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass(
this.isCardTemplateSearch = $(PopupComponent.stack[0].openerElement).hasClass(
'js-card-template',
);
this.isListTemplateSearch = $(Popup._getTopStack().openerElement).hasClass(
this.isListTemplateSearch = $(PopupComponent.stack[0].openerElement).hasClass(
'js-list-template',
);
this.isSwimlaneTemplateSearch = $(
Popup._getTopStack().openerElement,
PopupComponent.stack[0].openerElement,
).hasClass('js-open-add-swimlane-menu');
this.isBoardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass(
this.isBoardTemplateSearch = $(PopupComponent.stack[0].openerElement).hasClass(
'js-add-board',
);
this.isTemplateSearch =
@ -731,20 +893,16 @@ BlazeComponent.extendComponent({
} else {
this.board = Utils.getCurrentBoard();
}
if (!this.board) {
Popup.back();
return;
}
this.boardId = this.board._id;
this.boardId = this.board?._id;
// Subscribe to this board
subManager.subscribe('board', this.boardId, false);
this.selectedBoardId = new ReactiveVar(this.boardId);
this.list = $(Popup._getTopStack().openerElement).closest('.js-list');
if (!this.isBoardTemplateSearch) {
this.list = $(PopupComponent.stack[0].openerElement).closest('.js-list');
this.swimlaneId = '';
// Swimlane where to insert card
const swimlane = $(Popup._getTopStack().openerElement).parents(
const swimlane = $(PopupComponent.stack[0].openerElement).parents(
'.js-swimlane',
);
if (Utils.boardView() === 'board-view-swimlanes')
@ -783,11 +941,7 @@ BlazeComponent.extendComponent({
} else if (this.isSwimlaneTemplateSearch) {
return board.searchSwimlanes(this.term.get());
} else if (this.isBoardTemplateSearch) {
const boards = board.searchBoards(this.term.get());
boards.forEach(board => {
subManager.subscribe('board', board.linkedId, false);
});
return boards;
return board.searchBoards(this.term.get());
} else {
return [];
}