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({
connectWith: '.board-canvas',
connectWith: '.js-swimlane, .js-lists',
tolerance: 'pointer',
helper: 'clone',
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 sortIndex = calculateIndex(prevListDom, nextListDom, 1);
$listsDom.sortable('cancel');
const listDomElement = ui.item.get(0);
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,
removed from below Lists.update:
@ -95,10 +116,44 @@ function initSortable(boardComponent, $listsDom) {
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, {
$set: {
sort: sortIndex.base,
},
$set: updateData,
});
boardComponent.setIsDragging(false);
@ -109,10 +164,12 @@ function initSortable(boardComponent, $listsDom) {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$listsDom.sortable({
handle: '.js-list-handle',
connectWith: '.js-swimlane, .js-lists',
});
} else {
$listsDom.sortable({
handle: '.js-list-header',
connectWith: '.js-swimlane, .js-lists',
});
}
@ -281,7 +338,7 @@ BlazeComponent.extendComponent({
boardId: Session.get('currentBoard'),
sort: sortIndex,
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 = '';

View file

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

View file

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

View file

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

View file

@ -1489,3 +1489,64 @@ Migrations.add('remove-user-profile-hideCheckedItems', () => {
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;
}
});