Make possible for lists to have different names at different swimlanes. Make possible to drag list from one swimlane to another swimlane.

Thanks to xet7 !
This commit is contained in:
Lauri Ojansivu 2025-10-10 21:14:44 +03:00
parent 39daf56811
commit 719ef87efc
5 changed files with 144 additions and 12 deletions

View file

@ -63,7 +63,7 @@ function initSortable(boardComponent, $listsDom) {
}; };
$listsDom.sortable({ $listsDom.sortable({
connectWith: '.board-canvas', connectWith: '.js-swimlane, .js-lists',
tolerance: 'pointer', tolerance: 'pointer',
helper: 'clone', helper: 'clone',
items: '.js-list:not(.js-list-composer)', items: '.js-list:not(.js-list-composer)',
@ -82,10 +82,31 @@ function initSortable(boardComponent, $listsDom) {
const nextListDom = ui.item.next('.js-list').get(0); const nextListDom = ui.item.next('.js-list').get(0);
const sortIndex = calculateIndex(prevListDom, nextListDom, 1); const sortIndex = calculateIndex(prevListDom, nextListDom, 1);
$listsDom.sortable('cancel');
const listDomElement = ui.item.get(0); const listDomElement = ui.item.get(0);
const list = Blaze.getData(listDomElement); const list = Blaze.getData(listDomElement);
// Detect if the list was dropped in a different swimlane
const targetSwimlaneDom = ui.item.closest('.js-swimlane');
let targetSwimlaneId = null;
if (targetSwimlaneDom.length > 0) {
// List was dropped in a swimlane
targetSwimlaneId = targetSwimlaneDom.attr('id').replace('swimlane-', '');
} else {
// List was dropped in lists view (not swimlanes view)
// In this case, assign to the default swimlane
const currentBoard = ReactiveCache.getBoard(Session.get('currentBoard'));
if (currentBoard) {
const defaultSwimlane = currentBoard.getDefaultSwimline();
if (defaultSwimlane) {
targetSwimlaneId = defaultSwimlane._id;
}
}
}
// Get the original swimlane ID of the list
const originalSwimlaneId = list.swimlaneId;
/* /*
Reverted incomplete change list width, Reverted incomplete change list width,
removed from below Lists.update: removed from below Lists.update:
@ -95,10 +116,44 @@ function initSortable(boardComponent, $listsDom) {
height: list._id.height(), height: list._id.height(),
*/ */
// Prepare update object
const updateData = {
sort: sortIndex.base,
};
// Check if the list was dropped in a different swimlane
const isDifferentSwimlane = targetSwimlaneId && targetSwimlaneId !== originalSwimlaneId;
// If the list was dropped in a different swimlane, update the swimlaneId
if (isDifferentSwimlane) {
updateData.swimlaneId = targetSwimlaneId;
if (process.env.DEBUG === 'true') {
console.log(`Moving list "${list.title}" from swimlane ${originalSwimlaneId} to swimlane ${targetSwimlaneId}`);
}
// Move all cards in the list to the new swimlane
const cardsInList = ReactiveCache.getCards({
listId: list._id,
archived: false
});
cardsInList.forEach(card => {
card.move(list.boardId, targetSwimlaneId, list._id);
});
if (process.env.DEBUG === 'true') {
console.log(`Moved ${cardsInList.length} cards to swimlane ${targetSwimlaneId}`);
}
// Don't cancel the sortable when moving to a different swimlane
// The DOM move should be allowed to complete
} else {
// If staying in the same swimlane, cancel the sortable to prevent DOM manipulation issues
$listsDom.sortable('cancel');
}
Lists.update(list._id, { Lists.update(list._id, {
$set: { $set: updateData,
sort: sortIndex.base,
},
}); });
boardComponent.setIsDragging(false); boardComponent.setIsDragging(false);
@ -109,10 +164,12 @@ function initSortable(boardComponent, $listsDom) {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) { if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$listsDom.sortable({ $listsDom.sortable({
handle: '.js-list-handle', handle: '.js-list-handle',
connectWith: '.js-swimlane, .js-lists',
}); });
} else { } else {
$listsDom.sortable({ $listsDom.sortable({
handle: '.js-list-header', handle: '.js-list-header',
connectWith: '.js-swimlane, .js-lists',
}); });
} }
@ -281,7 +338,7 @@ BlazeComponent.extendComponent({
boardId: Session.get('currentBoard'), boardId: Session.get('currentBoard'),
sort: sortIndex, sort: sortIndex,
type: this.isListTemplatesSwimlane ? 'template-list' : 'list', type: this.isListTemplatesSwimlane ? 'template-list' : 'list',
swimlaneId: this.currentBoard.isTemplatesBoard() ? this.currentSwimlane._id : '', swimlaneId: this.currentSwimlane._id, // Always set swimlaneId for per-swimlane list titles
}); });
titleInput.value = ''; titleInput.value = '';

View file

@ -777,13 +777,22 @@ Boards.helpers({
{ {
boardId: this._id, boardId: this._id,
archived: false, archived: false,
// Get lists for all swimlanes in this board
swimlaneId: { $in: this.swimlanes().map(s => s._id) },
}, },
{ sort: sortKey }, { sort: sortKey },
); );
}, },
draggableLists() { draggableLists() {
return ReactiveCache.getLists({ boardId: this._id }, { sort: { sort: 1 } }); return ReactiveCache.getLists(
{
boardId: this._id,
// Get lists for all swimlanes in this board
swimlaneId: { $in: this.swimlanes().map(s => s._id) }
},
{ sort: { sort: 1 } }
);
}, },
/** returns the last list /** returns the last list
@ -1171,6 +1180,7 @@ Boards.helpers({
this.subtasksDefaultListId = Lists.insert({ this.subtasksDefaultListId = Lists.insert({
title: TAPi18n.__('queue'), title: TAPi18n.__('queue'),
boardId: this._id, boardId: this._id,
swimlaneId: this.getDefaultSwimline()._id, // Set default swimlane for subtasks list
}); });
this.setSubtasksDefaultListId(this.subtasksDefaultListId); this.setSubtasksDefaultListId(this.subtasksDefaultListId);
} }
@ -1189,6 +1199,7 @@ Boards.helpers({
this.dateSettingsDefaultListId = Lists.insert({ this.dateSettingsDefaultListId = Lists.insert({
title: TAPi18n.__('queue'), title: TAPi18n.__('queue'),
boardId: this._id, boardId: this._id,
swimlaneId: this.getDefaultSwimline()._id, // Set default swimlane for date settings list
}); });
this.setDateSettingsDefaultListId(this.dateSettingsDefaultListId); this.setDateSettingsDefaultListId(this.dateSettingsDefaultListId);
} }

View file

@ -50,10 +50,10 @@ Lists.attachSchema(
}, },
swimlaneId: { swimlaneId: {
/** /**
* the swimlane associated to this list. Used for templates * the swimlane associated to this list. Required for per-swimlane list titles
*/ */
type: String, type: String,
defaultValue: '', // Remove defaultValue to make it required
}, },
createdAt: { createdAt: {
/** /**
@ -196,7 +196,7 @@ Lists.helpers({
_id = existingListWithSameName._id; _id = existingListWithSameName._id;
} else { } else {
delete this._id; delete this._id;
delete this.swimlaneId; this.swimlaneId = swimlaneId; // Set the target swimlane for the copied list
_id = Lists.insert(this); _id = Lists.insert(this);
} }
@ -231,6 +231,7 @@ Lists.helpers({
type: this.type, type: this.type,
archived: false, archived: false,
wipLimit: this.wipLimit, wipLimit: this.wipLimit,
swimlaneId: swimlaneId, // Set the target swimlane for the moved list
}); });
} }
@ -585,6 +586,7 @@ if (Meteor.isServer) {
title: req.body.title, title: req.body.title,
boardId: paramBoardId, boardId: paramBoardId,
sort: board.lists().length, sort: board.lists().length,
swimlaneId: req.body.swimlaneId || board.getDefaultSwimline()._id, // Use provided swimlaneId or default
}); });
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,

View file

@ -173,6 +173,7 @@ Swimlanes.helpers({
type: list.type, type: list.type,
archived: false, archived: false,
wipLimit: list.wipLimit, wipLimit: list.wipLimit,
swimlaneId: toSwimlaneId, // Set the target swimlane for the copied list
}); });
} }
@ -213,7 +214,7 @@ Swimlanes.helpers({
return ReactiveCache.getLists( return ReactiveCache.getLists(
{ {
boardId: this.boardId, boardId: this.boardId,
swimlaneId: { $in: [this._id, ''] }, swimlaneId: this._id, // Only get lists that belong to this specific swimlane
archived: false, archived: false,
}, },
{ sort: { modifiedAt: -1 } }, { sort: { modifiedAt: -1 } },
@ -223,7 +224,7 @@ Swimlanes.helpers({
return ReactiveCache.getLists( return ReactiveCache.getLists(
{ {
boardId: this.boardId, boardId: this.boardId,
swimlaneId: { $in: [this._id, ''] }, swimlaneId: this._id, // Only get lists that belong to this specific swimlane
//archived: false, //archived: false,
}, },
{ sort: ['sort'] }, { sort: ['sort'] },

View file

@ -1489,3 +1489,64 @@ Migrations.add('remove-user-profile-hideCheckedItems', () => {
noValidateMulti, noValidateMulti,
); );
}); });
Migrations.add('migrate-lists-to-per-swimlane', () => {
if (process.env.DEBUG === 'true') {
console.log('Starting migration: migrate-lists-to-per-swimlane');
}
try {
// Get all boards
const boards = Boards.find({}).fetch();
boards.forEach(board => {
if (process.env.DEBUG === 'true') {
console.log(`Processing board: ${board.title} (${board._id})`);
}
// Get the default swimlane for this board
const defaultSwimlane = board.getDefaultSwimline();
if (!defaultSwimlane) {
if (process.env.DEBUG === 'true') {
console.log(`No default swimlane found for board ${board._id}, skipping`);
}
return;
}
// Get all lists for this board that don't have a swimlaneId or have empty swimlaneId
const listsWithoutSwimlane = Lists.find({
boardId: board._id,
$or: [
{ swimlaneId: { $exists: false } },
{ swimlaneId: '' },
{ swimlaneId: null }
]
}).fetch();
if (process.env.DEBUG === 'true') {
console.log(`Found ${listsWithoutSwimlane.length} lists without swimlaneId in board ${board._id}`);
}
// Update each list to belong to the default swimlane
listsWithoutSwimlane.forEach(list => {
if (process.env.DEBUG === 'true') {
console.log(`Updating list "${list.title}" to belong to swimlane "${defaultSwimlane.title}"`);
}
Lists.direct.update(list._id, {
$set: {
swimlaneId: defaultSwimlane._id
}
}, noValidate);
});
});
if (process.env.DEBUG === 'true') {
console.log('Migration migrate-lists-to-per-swimlane completed successfully');
}
} catch (error) {
console.error('Error during migration migrate-lists-to-per-swimlane:', error);
throw error;
}
});