From 223c38c50d9928e5eead39ee628422baa423f1a4 Mon Sep 17 00:00:00 2001 From: Lilou Date: Mon, 29 Dec 2025 02:45:48 +0100 Subject: [PATCH] Set sortable methods of lists only once --- client/components/swimlanes/swimlanes.js | 646 ++++++++--------------- models/lists.js | 10 +- 2 files changed, 230 insertions(+), 426 deletions(-) diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js index 863cfe600..a210a27a0 100644 --- a/client/components/swimlanes/swimlanes.js +++ b/client/components/swimlanes/swimlanes.js @@ -2,7 +2,147 @@ import { ReactiveCache } from '/imports/reactiveCache'; import dragscroll from '@wekanteam/dragscroll'; const { calculateIndex } = Utils; +function saveSorting(ui) { + // To attribute the new index number, we need to get the DOM element + // of the previous and the following list -- if any. + const prevListDom = ui.item.prev('.js-list').get(0); + const nextListDom = ui.item.next('.js-list').get(0); + const sortIndex = calculateIndex(prevListDom, nextListDom, 1); + const listDomElement = ui.item.get(0); + if (!listDomElement) { + return; + } + + let list; + try { + list = Blaze.getData(listDomElement); + } catch (error) { + return; + } + + if (!list) { + return; + } + + // 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 + try { + targetSwimlaneId = targetSwimlaneDom.attr('id').replace('swimlane-', ''); + } catch (error) { + return; + } + } 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 (handle backward compatibility) + const originalSwimlaneId = list.getEffectiveSwimlaneId ? list.getEffectiveSwimlaneId() : (list.swimlaneId || null); + + // 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; + + // 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); + }); + + // Don't cancel the sortable when moving to a different swimlane + // The DOM move should be allowed to complete + } + // Allow reordering within the same swimlane by not canceling the sortable + + try { + Lists.update(list._id, { + $set: updateData, + }); + } catch (error) { + return; + } + + // Save to localStorage for non-logged-in users (backup) + if (!Meteor.userId()) { + try { + const boardId = list.boardId; + const listId = list._id; + const listOrderKey = `wekan-list-order-${boardId}`; + + let listOrder = JSON.parse(localStorage.getItem(listOrderKey) || '{}'); + if (!listOrder.lists) listOrder.lists = []; + + const listIndex = listOrder.lists.findIndex(l => l.id === listId); + if (listIndex >= 0) { + listOrder.lists[listIndex].sort = sortIndex.base; + listOrder.lists[listIndex].swimlaneId = updateData.swimlaneId; + listOrder.lists[listIndex].updatedAt = new Date().toISOString(); + } else { + listOrder.lists.push({ + id: listId, + sort: sortIndex.base, + swimlaneId: updateData.swimlaneId + }); + } + + localStorage.setItem(listOrderKey, JSON.stringify(listOrder)); + } catch (e) { + } + } + + // Persist to server + Meteor.call('updateListSort', list._id, list.boardId, updateData, function (error) { + if (error) { + Meteor.subscribe('board', list.boardId, false); + } + }); + + // Try to get board component + try { + const boardComponent = BlazeComponent.getComponentForElement(ui.item[0]); + if (boardComponent && boardComponent.setIsDragging) { + boardComponent.setIsDragging(false); + } + } catch (e) { + // Silent fail + } + + // Re-enable dragscroll after list dragging is complete + try { + dragscroll.reset(); + } catch (e) { + // Silent fail + } + + // Re-enable dragscroll on all swimlanes + $('.js-swimlane').each(function () { + $(this).addClass('dragscroll'); + }); +} function currentListIsInThisSwimlane(swimlaneId) { const currentList = Utils.getCurrentList(); @@ -118,29 +258,29 @@ function initSortable(boardComponent, $listsDom) { }, }; - + // Add click debugging for drag handles $listsDom.on('mousedown', '.js-list-handle', function(e) { e.stopPropagation(); }); - + $listsDom.on('mousedown', '.js-list-header', function(e) { }); - + // Add debugging for any mousedown on lists $listsDom.on('mousedown', '.js-list', function(e) { }); - + // Add debugging for sortable events $listsDom.on('sortstart', function(e, ui) { }); - + $listsDom.on('sortbeforestop', function(e, ui) { }); - + $listsDom.on('sortstop', function(e, ui) { }); - + try { $listsDom.sortable({ connectWith: '.js-swimlane, .js-lists', @@ -156,196 +296,44 @@ function initSortable(boardComponent, $listsDom) { distance: 3, forcePlaceholderSize: true, cursor: 'move', - start(evt, ui) { - ui.helper.css('z-index', 1000); - ui.placeholder.height(ui.helper.height()); - ui.placeholder.width(ui.helper.width()); - EscapeActions.executeUpTo('popup-close'); - boardComponent.setIsDragging(true); - - // Add visual feedback for list being dragged - ui.item.addClass('ui-sortable-helper'); - - // Disable dragscroll during list dragging to prevent interference - try { - dragscroll.reset(); - } catch (e) { - } - - // Also disable dragscroll on all swimlanes during list dragging - $('.js-swimlane').each(function() { - $(this).removeClass('dragscroll'); - }); - }, - beforeStop(evt, ui) { - // Clean up visual feedback - ui.item.removeClass('ui-sortable-helper'); - }, - 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 prevListDom = ui.item.prev('.js-list').get(0); - const nextListDom = ui.item.next('.js-list').get(0); - const sortIndex = calculateIndex(prevListDom, nextListDom, 1); + start(evt, ui) { + ui.helper.css('z-index', 1000); + ui.placeholder.height(ui.helper.height()); + ui.placeholder.width(ui.helper.width()); + EscapeActions.executeUpTo('popup-close'); + boardComponent.setIsDragging(true); - const listDomElement = ui.item.get(0); - if (!listDomElement) { - console.error('List DOM element not found during drag stop'); - return; - } - - let list; - try { - list = Blaze.getData(listDomElement); - } catch (error) { - console.error('Error getting list data:', error); - return; - } - - if (!list) { - console.error('List data not found for element:', listDomElement); - return; - } + // Add visual feedback for list being dragged + ui.item.addClass('ui-sortable-helper'); - // 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 + // Disable dragscroll during list dragging to prevent interference try { - targetSwimlaneId = targetSwimlaneDom.attr('id').replace('swimlane-', ''); - } catch (error) { - console.error('Error getting target swimlane ID:', error); - return; + dragscroll.reset(); + } catch (e) { } - } 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 (handle backward compatibility) - const originalSwimlaneId = list.getEffectiveSwimlaneId ? list.getEffectiveSwimlaneId() : (list.swimlaneId || null); - - /* - Reverted incomplete change list width, - removed from below Lists.update: - https://github.com/wekan/wekan/issues/4558 - $set: { - width: list._id.width(), - 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; - - // Move all cards in the list to the new swimlane - const cardsInList = ReactiveCache.getCards({ - listId: list._id, - archived: false + // Also disable dragscroll on all swimlanes during list dragging + $('.js-swimlane').each(function() { + $(this).removeClass('dragscroll'); }); - - cardsInList.forEach(card => { - card.move(list.boardId, targetSwimlaneId, list._id); - }); - - - // Don't cancel the sortable when moving to a different swimlane - // The DOM move should be allowed to complete + }, + beforeStop(evt, ui) { + // Clean up visual feedback + ui.item.removeClass('ui-sortable-helper'); + }, + stop(evt, ui) { + saveSorting(ui); } - // Allow reordering within the same swimlane by not canceling the sortable - - // IMMEDIATELY update local collection for UI responsiveness - try { - Lists.update(list._id, { - $set: updateData, - }); - } catch (error) { - console.error('Error updating list locally:', error); - } - - // Save to localStorage for non-logged-in users (backup) - if (!Meteor.userId()) { - try { - const boardId = list.boardId; - const listId = list._id; - const listOrderKey = `wekan-list-order-${boardId}`; - - let listOrder = JSON.parse(localStorage.getItem(listOrderKey) || '{}'); - if (!listOrder.lists) listOrder.lists = []; - - // Find and update the list order entry - const listIndex = listOrder.lists.findIndex(l => l.id === listId); - if (listIndex >= 0) { - listOrder.lists[listIndex].sort = sortIndex.base; - listOrder.lists[listIndex].swimlaneId = updateData.swimlaneId; - listOrder.lists[listIndex].updatedAt = new Date().toISOString(); - } else { - listOrder.lists.push({ - id: listId, - sort: sortIndex.base, - swimlaneId: updateData.swimlaneId, - updatedAt: new Date().toISOString() - }); - } - - localStorage.setItem(listOrderKey, JSON.stringify(listOrder)); - } catch (e) { - console.warn('Failed to save list order to localStorage:', e); - } - } - - // Call server method to ensure persistence (with callback for error handling) - Meteor.call('updateListSort', list._id, list.boardId, updateData, function(error, result) { - if (error) { - console.error('Server update list sort failed:', error); - // Revert the local update if server fails (will be refreshed by pubsub) - Meteor.subscribe('board', list.boardId, false); - } else { - console.debug('List sort successfully saved to server'); - } - }); - - boardComponent.setIsDragging(false); - - // Re-enable dragscroll after list dragging is complete - try { - dragscroll.reset(); - } catch (e) { - } - - // Re-enable dragscroll on all swimlanes - $('.js-swimlane').each(function() { - $(this).addClass('dragscroll'); - }); - }, - }); + }); } catch (error) { console.error('Error initializing list sortable:', error); return; } - - + + // Check if drag handles exist const dragHandles = $listsDom.find('.js-list-handle'); - + // Check if lists exist const lists = $listsDom.find('.js-list'); @@ -364,7 +352,7 @@ BlazeComponent.extendComponent({ syncListOrderFromStorage(boardId); }, 500); } - + if (!Utils.getCurrentCardId()) { boardComponent.scrollLeft(); @@ -372,27 +360,27 @@ BlazeComponent.extendComponent({ // Try a simpler approach - initialize sortable directly like cards do this.initializeSwimlaneResize(); - + // Wait for DOM to be ready setTimeout(() => { const handleSelector = Utils.isTouchScreenOrShowDesktopDragHandles() ? '.js-list-handle' : '.js-list-header'; const $lists = this.$('.js-list'); - + const $parent = $lists.parent(); - + if ($lists.length > 0) { - + // Check for drag handles const $handles = $parent.find('.js-list-handle'); - + // Test if drag handles are clickable $handles.on('click', function(e) { e.preventDefault(); e.stopPropagation(); }); - + $parent.sortable({ connectWith: '.js-swimlane, .js-lists', tolerance: 'pointer', @@ -412,6 +400,7 @@ BlazeComponent.extendComponent({ }, stop(evt, ui) { boardComponent.setIsDragging(false); + saveSorting(ui); } }); // Reactively update handle when user toggles desktop drag handles @@ -486,11 +475,11 @@ BlazeComponent.extendComponent({ const isResizeHandle = $(evt.target).closest('.js-list-resize-handle, .js-swimlane-resize-handle').length > 0; const isInNoDragArea = $(evt.target).closest(noDragInside.join(',')).length > 0; - + if (isResizeHandle) { return; } - + if ( !isInNoDragArea && this.$('.swimlane').prop('clientHeight') > evt.offsetY @@ -525,7 +514,7 @@ BlazeComponent.extendComponent({ swimlaneHeight() { const user = ReactiveCache.getCurrentUser(); const swimlane = Template.currentData(); - + let height; if (user) { // For logged-in users, get from user profile @@ -560,11 +549,11 @@ BlazeComponent.extendComponent({ console.warn('No current template data available for swimlane resize initialization'); return; } - + const swimlane = Template.currentData(); const $swimlane = $(`#swimlane-${swimlane._id}`); const $resizeHandle = $swimlane.find('.js-swimlane-resize-handle'); - + // Check if elements exist if (!$swimlane.length || !$resizeHandle.length) { console.warn('Swimlane or resize handle not found, retrying in 100ms'); @@ -575,8 +564,8 @@ BlazeComponent.extendComponent({ }, 100); return; } - - + + if ($resizeHandle.length === 0) { return; } @@ -591,13 +580,13 @@ BlazeComponent.extendComponent({ isResizing = true; startY = e.pageY || e.originalEvent.touches[0].pageY; startHeight = parseInt($swimlane.css('height')) || 300; - - + + $swimlane.addClass('swimlane-resizing'); $('body').addClass('swimlane-resizing-active'); $('body').css('user-select', 'none'); - - + + e.preventDefault(); e.stopPropagation(); }; @@ -606,12 +595,12 @@ BlazeComponent.extendComponent({ if (!isResizing) { return; } - + const currentY = e.pageY || e.originalEvent.touches[0].pageY; const deltaY = currentY - startY; const newHeight = Math.max(minHeight, Math.min(maxHeight, startHeight + deltaY)); - - + + // Apply the new height immediately for real-time feedback $swimlane[0].style.setProperty('--swimlane-height', `${newHeight}px`); $swimlane[0].style.setProperty('height', `${newHeight}px`); @@ -621,22 +610,22 @@ BlazeComponent.extendComponent({ $swimlane[0].style.setProperty('flex-basis', 'auto'); $swimlane[0].style.setProperty('flex-grow', '0'); $swimlane[0].style.setProperty('flex-shrink', '0'); - - + + e.preventDefault(); e.stopPropagation(); }; const stopResize = (e) => { if (!isResizing) return; - + isResizing = false; - + // Calculate final height const currentY = e.pageY || e.originalEvent.touches[0].pageY; const deltaY = currentY - startY; const finalHeight = Math.max(minHeight, Math.min(maxHeight, startHeight + deltaY)); - + // Ensure the final height is applied $swimlane[0].style.setProperty('--swimlane-height', `${finalHeight}px`); $swimlane[0].style.setProperty('height', `${finalHeight}px`); @@ -646,19 +635,19 @@ BlazeComponent.extendComponent({ $swimlane[0].style.setProperty('flex-basis', 'auto'); $swimlane[0].style.setProperty('flex-grow', '0'); $swimlane[0].style.setProperty('flex-shrink', '0'); - + // Remove visual feedback but keep the height $swimlane.removeClass('swimlane-resizing'); $('body').removeClass('swimlane-resizing-active'); $('body').css('user-select', ''); - + // Save the new height using the existing system const boardId = swimlane.boardId; const swimlaneId = swimlane._id; - + if (process.env.DEBUG === 'true') { } - + const currentUser = ReactiveCache.getCurrentUser(); if (currentUser) { // For logged-in users, use server method @@ -675,21 +664,21 @@ BlazeComponent.extendComponent({ try { const stored = localStorage.getItem('wekan-swimlane-heights'); let heights = stored ? JSON.parse(stored) : {}; - + if (!heights[boardId]) { heights[boardId] = {}; } heights[boardId][swimlaneId] = finalHeight; - + localStorage.setItem('wekan-swimlane-heights', JSON.stringify(heights)); - + if (process.env.DEBUG === 'true') { } } catch (e) { console.warn('Error saving swimlane height to localStorage:', e); } } - + e.preventDefault(); }; @@ -697,18 +686,18 @@ BlazeComponent.extendComponent({ $resizeHandle.on('mousedown', startResize); $(document).on('mousemove', doResize); $(document).on('mouseup', stopResize); - + // Touch events for mobile $resizeHandle.on('touchstart', startResize, { passive: false }); $(document).on('touchmove', doResize, { passive: false }); $(document).on('touchend', stopResize, { passive: false }); - - + + // Prevent dragscroll interference $resizeHandle.on('mousedown', (e) => { e.stopPropagation(); }); - + }, }).register('swimlane'); @@ -780,7 +769,7 @@ Template.swimlane.helpers({ canSeeAddList() { return ReactiveCache.getCurrentUser().isBoardAdmin(); }, - + lists() { // Return per-swimlane lists for this swimlane return this.myLists(); @@ -791,204 +780,19 @@ Template.swimlane.helpers({ } }); + // Initialize sortable on DOM elements setTimeout(() => { - const $swimlaneElements = $('.swimlane'); const $listsGroupElements = $('.list-group'); const computeHandle = () => ( Utils.isTouchScreenOrShowDesktopDragHandles() ? '.js-list-handle' : '.js-list-header' ); - - // Initialize sortable on ALL swimlane elements (even empty ones) - $swimlaneElements.each(function(index) { - const $swimlane = $(this); - const $lists = $swimlane.find('.js-list'); - - // Only initialize on swimlanes that have the .js-lists class (the container for lists) - if ($swimlane.hasClass('js-lists')) { - $swimlane.sortable({ - connectWith: '.js-swimlane, .js-lists', - tolerance: 'pointer', - appendTo: '.board-canvas', - helper: 'clone', - items: '.js-list:not(.js-list-composer)', - placeholder: 'list placeholder', - distance: 7, - handle: computeHandle(), - disabled: !Utils.canModifyBoard(), - start(evt, ui) { - ui.helper.css('z-index', 1000); - ui.placeholder.height(ui.helper.height()); - ui.placeholder.width(ui.helper.width()); - EscapeActions.executeUpTo('popup-close'); - // Try to get board component - try { - const boardComponent = BlazeComponent.getComponentForElement(ui.item[0]); - if (boardComponent && boardComponent.setIsDragging) { - boardComponent.setIsDragging(true); - } - } catch (e) { - // Silent fail - } - }, - stop(evt, ui) { - // To attribute the new index number, we need to get the DOM element - // of the previous and the following list -- if any. - const prevListDom = ui.item.prev('.js-list').get(0); - const nextListDom = ui.item.next('.js-list').get(0); - const sortIndex = calculateIndex(prevListDom, nextListDom, 1); - const listDomElement = ui.item.get(0); - if (!listDomElement) { - return; - } - - let list; - try { - list = Blaze.getData(listDomElement); - } catch (error) { - return; - } - - if (!list) { - return; - } - - // 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 - try { - targetSwimlaneId = targetSwimlaneDom.attr('id').replace('swimlane-', ''); - } catch (error) { - return; - } - } 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 (handle backward compatibility) - const originalSwimlaneId = list.getEffectiveSwimlaneId ? list.getEffectiveSwimlaneId() : (list.swimlaneId || null); - - // 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; - - // 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); - }); - - // Don't cancel the sortable when moving to a different swimlane - // The DOM move should be allowed to complete - } - // Allow reordering within the same swimlane by not canceling the sortable - - try { - Lists.update(list._id, { - $set: updateData, - }); - } catch (error) { - return; - } - - // Save to localStorage for non-logged-in users (backup) - if (!Meteor.userId()) { - try { - const boardId = list.boardId; - const listId = list._id; - const listOrderKey = `wekan-list-order-${boardId}`; - - let listOrder = JSON.parse(localStorage.getItem(listOrderKey) || '{}'); - if (!listOrder.lists) listOrder.lists = []; - - const listIndex = listOrder.lists.findIndex(l => l.id === listId); - if (listIndex >= 0) { - listOrder.lists[listIndex].sort = sortIndex.base; - listOrder.lists[listIndex].swimlaneId = updateData.swimlaneId; - listOrder.lists[listIndex].updatedAt = new Date().toISOString(); - } else { - listOrder.lists.push({ - id: listId, - sort: sortIndex.base, - swimlaneId: updateData.swimlaneId, - updatedAt: new Date().toISOString() - }); - } - - localStorage.setItem(listOrderKey, JSON.stringify(listOrder)); - } catch (e) { - } - } - - // Persist to server - Meteor.call('updateListSort', list._id, list.boardId, updateData, function(error) { - if (error) { - Meteor.subscribe('board', list.boardId, false); - } - }); - - // Try to get board component - try { - const boardComponent = BlazeComponent.getComponentForElement(ui.item[0]); - if (boardComponent && boardComponent.setIsDragging) { - boardComponent.setIsDragging(false); - } - } catch (e) { - // Silent fail - } - - // Re-enable dragscroll after list dragging is complete - try { - dragscroll.reset(); - } catch (e) { - // Silent fail - } - - // Re-enable dragscroll on all swimlanes - $('.js-swimlane').each(function() { - $(this).addClass('dragscroll'); - }); - } - }); - // Reactively adjust handle when setting changes - Tracker.autorun(() => { - const newHandle = computeHandle(); - if ($swimlane.data('uiSortable') || $swimlane.data('sortable')) { - try { $swimlane.sortable('option', 'handle', newHandle); } catch (e) {} - } - }); - } - }); - // Initialize sortable on ALL listsGroup elements (even empty ones) $listsGroupElements.each(function(index) { const $listsGroup = $(this); const $lists = $listsGroup.find('.js-list'); - + // Only initialize on listsGroup elements that have the .js-lists class if ($listsGroup.hasClass('js-lists')) { $listsGroup.sortable({ @@ -1027,14 +831,14 @@ setTimeout(() => { if (!listDomElement) { return; } - + let list; try { list = Blaze.getData(listDomElement); } catch (error) { return; } - + if (!list) { return; } @@ -1145,14 +949,14 @@ setTimeout(() => { } catch (e) { // Silent fail } - + // Re-enable dragscroll after list dragging is complete try { dragscroll.reset(); } catch (e) { // Silent fail } - + // Re-enable dragscroll on all swimlanes $('.js-swimlane').each(function() { $(this).addClass('dragscroll'); @@ -1201,34 +1005,34 @@ BlazeComponent.extendComponent({ onRendered() { const boardComponent = this.parentComponent(); const $listsDom = this.$('.js-lists'); - + if (!Utils.getCurrentCardId()) { boardComponent.scrollLeft(); } // Try a simpler approach for listsGroup too - + // Wait for DOM to be ready setTimeout(() => { const handleSelector = Utils.isTouchScreenOrShowDesktopDragHandles() ? '.js-list-handle' : '.js-list-header'; const $lists = this.$('.js-list'); - + const $parent = $lists.parent(); - + if ($lists.length > 0) { - + // Check for drag handles const $handles = $parent.find('.js-list-handle'); - + // Test if drag handles are clickable $handles.on('click', function(e) { e.preventDefault(); e.stopPropagation(); }); - + $parent.sortable({ connectWith: '.js-swimlane, .js-lists', tolerance: 'pointer', diff --git a/models/lists.js b/models/lists.js index dbf0a6efb..84828a791 100644 --- a/models/lists.js +++ b/models/lists.js @@ -481,7 +481,7 @@ Meteor.methods({ throw new Meteor.Error('list-not-found', 'List not found'); } - const validUpdateFields = ['sort', 'swimlaneId']; + const validUpdateFields = ['sort', 'swimlaneId', 'updatedAt', 'modifiedAt']; Object.keys(updateData).forEach(field => { if (!validUpdateFields.includes(field)) { throw new Meteor.Error('invalid-field', `Field ${field} is not allowed`); @@ -934,7 +934,7 @@ Lists.helpers({ hasMovedFromOriginalPosition() { const history = this.getOriginalPosition(); if (!history) return false; - + const currentSwimlaneId = this.swimlaneId || null; return history.originalPosition.sort !== this.sort || history.originalSwimlaneId !== currentSwimlaneId; @@ -946,9 +946,9 @@ Lists.helpers({ getOriginalPositionDescription() { const history = this.getOriginalPosition(); if (!history) return 'No original position data'; - - const swimlaneInfo = history.originalSwimlaneId ? - ` in swimlane ${history.originalSwimlaneId}` : + + const swimlaneInfo = history.originalSwimlaneId ? + ` in swimlane ${history.originalSwimlaneId}` : ' in default swimlane'; return `Original position: ${history.originalPosition.sort || 0}${swimlaneInfo}`; },