| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114 |
- import { ReactiveCache } from '/imports/reactiveCache';
- import dragscroll from '@wekanteam/dragscroll';
- const { calculateIndex } = Utils;
- function currentListIsInThisSwimlane(swimlaneId) {
- const currentList = Utils.getCurrentList();
- return (
- currentList &&
- (currentList.swimlaneId === swimlaneId || currentList.swimlaneId === '')
- );
- }
- function currentCardIsInThisList(listId, swimlaneId) {
- const currentCard = Utils.getCurrentCard();
- //const currentUser = ReactiveCache.getCurrentUser();
- if (
- //currentUser &&
- //currentUser.profile &&
- Utils.boardView() === 'board-view-swimlanes'
- )
- return (
- currentCard &&
- currentCard.listId === listId &&
- currentCard.swimlaneId === swimlaneId
- );
- else if (
- //currentUser &&
- //currentUser.profile &&
- Utils.boardView() === 'board-view-lists'
- )
- return (
- currentCard &&
- currentCard.listId === listId
- );
- // https://github.com/wekan/wekan/issues/1623
- // https://github.com/ChronikEwok/wekan/commit/cad9b20451bb6149bfb527a99b5001873b06c3de
- // TODO: In public board, if you would like to switch between List/Swimlane view, you could
- // 1) If there is no view cookie, save to cookie board-view-lists
- // board-view-lists / board-view-swimlanes / board-view-cal
- // 2) If public user changes clicks board-view-lists then change view and
- // then change view and save cookie with view value
- // without using currentuser above, because currentuser is null.
- }
- function initSortable(boardComponent, $listsDom) {
- // Safety check: ensure we have valid DOM elements
- if (!$listsDom || $listsDom.length === 0) {
- console.error('initSortable: No valid DOM elements provided');
- return;
- }
-
- // Check if sortable is already initialized
- if ($listsDom.data('uiSortable') || $listsDom.data('sortable')) {
- $listsDom.sortable('destroy');
- }
-
-
- // We want to animate the card details window closing. We rely on CSS
- // transition for the actual animation.
- $listsDom._uihooks = {
- removeElement(node) {
- const removeNode = _.once(() => {
- node.parentNode.removeChild(node);
- });
- if ($(node).hasClass('js-card-details')) {
- $(node).css({
- flexBasis: 0,
- padding: 0,
- });
- $listsDom.one(CSSEvents.transitionend, removeNode);
- } else {
- removeNode();
- }
- },
- };
-
- // 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',
- tolerance: 'pointer',
- appendTo: '.board-canvas',
- helper(evt, item) {
- const helper = item.clone();
- helper.css('z-index', 1000);
- return helper;
- },
- items: '.js-list:not(.js-list-composer)',
- placeholder: 'list placeholder',
- 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);
- 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;
- }
- // 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) {
- console.error('Error getting target swimlane ID:', 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);
- /*
- 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
- });
- 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) {
- console.error('Error updating list:', error);
- return;
- }
- 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');
- // Skip the complex autorun and options for now
- }
- BlazeComponent.extendComponent({
- onRendered() {
- const boardComponent = this.parentComponent();
- const $listsDom = this.$('.js-lists');
-
- if (!Utils.getCurrentCardId()) {
- boardComponent.scrollLeft();
- }
- // Try a simpler approach - initialize sortable directly like cards do
-
- // Wait for DOM to be ready
- setTimeout(() => {
- 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',
- appendTo: '.board-canvas',
- helper: 'clone',
- items: '.js-list:not(.js-list-composer)',
- placeholder: 'list placeholder',
- distance: 7,
- handle: '.js-list-handle',
- 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');
- boardComponent.setIsDragging(true);
- },
- stop(evt, ui) {
- boardComponent.setIsDragging(false);
- }
- });
- } else {
- }
- }, 100);
- },
- onCreated() {
- this.draggingActive = new ReactiveVar(false);
- this._isDragging = false;
- this._lastDragPositionX = 0;
- },
- id() {
- return this._id;
- },
- currentCardIsInThisList(listId, swimlaneId) {
- return currentCardIsInThisList(listId, swimlaneId);
- },
- currentListIsInThisSwimlane(swimlaneId) {
- return currentListIsInThisSwimlane(swimlaneId);
- },
- visible(list) {
- if (list.archived) {
- // Show archived list only when filter archive is on
- if (!Filter.archive.isSelected()) {
- return false;
- }
- }
- if (Filter.lists._isActive()) {
- if (!list.title.match(Filter.lists.getRegexSelector())) {
- return false;
- }
- }
- if (Filter.hideEmpty.isSelected()) {
- // Check for cards in all swimlanes, not just the current one
- // This ensures lists with cards in other swimlanes are still visible
- const cards = list.cards();
- if (cards.length === 0) {
- return false;
- }
- }
- return true;
- },
- events() {
- return [
- {
- // Click-and-drag action
- 'mousedown .board-canvas'(evt) {
- // Translating the board canvas using the click-and-drag action can
- // conflict with the build-in browser mechanism to select text. We
- // define a list of elements in which we disable the dragging because
- // the user will legitimately expect to be able to select some text with
- // his mouse.
- const noDragInside = ['a', 'input', 'textarea', 'p'].concat(
- Utils.isTouchScreenOrShowDesktopDragHandles()
- ? ['.js-list-handle', '.js-swimlane-header-handle']
- : ['.js-list-header'],
- ).concat([
- '.js-list-resize-handle',
- '.js-swimlane-resize-handle'
- ]);
- 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
- ) {
- this._isDragging = true;
- this._lastDragPositionX = evt.clientX;
- }
- },
- mouseup() {
- if (this._isDragging) {
- this._isDragging = false;
- }
- },
- mousemove(evt) {
- if (this._isDragging) {
- // Update the canvas position
- this.listsDom.scrollLeft -= evt.clientX - this._lastDragPositionX;
- this._lastDragPositionX = evt.clientX;
- // Disable browser text selection while dragging
- evt.stopPropagation();
- evt.preventDefault();
- // Don't close opened card or inlined form at the end of the
- // click-and-drag.
- EscapeActions.executeUpTo('popup-close');
- EscapeActions.preventNextClick();
- }
- },
- },
- ];
- },
- swimlaneHeight() {
- const user = ReactiveCache.getCurrentUser();
- const swimlane = Template.currentData();
-
- let height;
- if (user) {
- // For logged-in users, get from user profile
- height = user.getSwimlaneHeightFromStorage(swimlane.boardId, swimlane._id);
- } else {
- // For non-logged-in users, get from localStorage
- try {
- const stored = localStorage.getItem('wekan-swimlane-heights');
- if (stored) {
- const heights = JSON.parse(stored);
- if (heights[swimlane.boardId] && heights[swimlane.boardId][swimlane._id]) {
- height = heights[swimlane.boardId][swimlane._id];
- } else {
- height = -1;
- }
- } else {
- height = -1;
- }
- } catch (e) {
- console.warn('Error reading swimlane height from localStorage:', e);
- height = -1;
- }
- }
-
- return height == -1 ? "auto" : (height + 5 + "px");
- },
- onRendered() {
- // Initialize swimlane resize functionality immediately
- this.initializeSwimlaneResize();
- },
- initializeSwimlaneResize() {
- // Check if we're still in a valid template context
- if (!Template.currentData()) {
- 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');
- Meteor.setTimeout(() => {
- if (!this.isDestroyed) {
- this.initializeSwimlaneResize();
- }
- }, 100);
- return;
- }
-
-
- if ($resizeHandle.length === 0) {
- return;
- }
- let isResizing = false;
- let startY = 0;
- let startHeight = 0;
- const minHeight = 100;
- const maxHeight = 2000;
- const startResize = (e) => {
- 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();
- };
- const doResize = (e) => {
- 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`);
- $swimlane[0].style.setProperty('min-height', `${newHeight}px`);
- $swimlane[0].style.setProperty('max-height', `${newHeight}px`);
- $swimlane[0].style.setProperty('flex', 'none');
- $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`);
- $swimlane[0].style.setProperty('min-height', `${finalHeight}px`);
- $swimlane[0].style.setProperty('max-height', `${finalHeight}px`);
- $swimlane[0].style.setProperty('flex', 'none');
- $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
- Meteor.call('applySwimlaneHeightToStorage', boardId, swimlaneId, finalHeight, (error, result) => {
- if (error) {
- console.error('Error saving swimlane height:', error);
- } else {
- if (process.env.DEBUG === 'true') {
- }
- }
- });
- } else {
- // For non-logged-in users, save to localStorage directly
- 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();
- };
- // Mouse events
- $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');
- BlazeComponent.extendComponent({
- onCreated() {
- this.currentBoard = Utils.getCurrentBoard();
- this.isListTemplatesSwimlane =
- this.currentBoard.isTemplatesBoard() &&
- this.currentData().isListTemplatesSwimlane();
- this.currentSwimlane = this.currentData();
- },
- // Proxy
- open() {
- this.childComponents('inlinedForm')[0].open();
- },
- events() {
- return [
- {
- submit(evt) {
- evt.preventDefault();
- const titleInput = this.find('.list-name-input');
- const title = titleInput?.value.trim();
- if (!title) return;
- let sortIndex = 0;
- const lastList = this.currentBoard.getLastList();
- const boardId = Utils.getCurrentBoardId();
- const positionInput = this.find('.list-position-input');
- if (positionInput) {
- const positionId = positionInput.value.trim();
- const selectedList = ReactiveCache.getList({ boardId, _id: positionId, archived: false });
- if (selectedList) {
- sortIndex = selectedList.sort + 1;
- } else {
- sortIndex = Utils.calculateIndexData(lastList, null).base;
- }
- } else {
- sortIndex = Utils.calculateIndexData(lastList, null).base;
- }
- Lists.insert({
- title,
- boardId: Session.get('currentBoard'),
- sort: sortIndex,
- type: this.isListTemplatesSwimlane ? 'template-list' : 'list',
- swimlaneId: this.currentSwimlane._id, // Always set swimlaneId for per-swimlane list titles
- });
- titleInput.value = '';
- titleInput.focus();
- }
- },
- {
- 'click .js-list-template': Popup.open('searchElement'),
- },
- ];
- },
- }).register('addListForm');
- Template.swimlane.helpers({
- canSeeAddList() {
- return ReactiveCache.getCurrentUser().isBoardAdmin();
- },
-
- lists() {
- // Return per-swimlane lists for this swimlane
- return this.myLists();
- }
- });
- // Initialize sortable on DOM elements
- setTimeout(() => {
- const $swimlaneElements = $('.swimlane');
- const $listsGroupElements = $('.list-group');
-
- // 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: '.js-list-handle',
- 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;
- }
- // 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');
- });
- }
- });
- }
- });
-
- // 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({
- 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: '.js-list-handle',
- 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;
- }
- // 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');
- });
- }
- });
- }
- });
- }, 1000);
- BlazeComponent.extendComponent({
- currentCardIsInThisList(listId, swimlaneId) {
- return currentCardIsInThisList(listId, swimlaneId);
- },
- visible(list) {
- if (list.archived) {
- // Show archived list only when filter archive is on
- if (!Filter.archive.isSelected()) {
- return false;
- }
- }
- if (Filter.lists._isActive()) {
- if (!list.title.match(Filter.lists.getRegexSelector())) {
- return false;
- }
- }
- if (Filter.hideEmpty.isSelected()) {
- // Check for cards in all swimlanes, not just the current one
- // This ensures lists with cards in other swimlanes are still visible
- const cards = list.cards();
- if (cards.length === 0) {
- return false;
- }
- }
- return true;
- },
- 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 $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',
- appendTo: '.board-canvas',
- helper: 'clone',
- items: '.js-list:not(.js-list-composer)',
- placeholder: 'list placeholder',
- distance: 7,
- handle: '.js-list-handle',
- 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');
- boardComponent.setIsDragging(true);
- },
- stop(evt, ui) {
- boardComponent.setIsDragging(false);
- }
- });
- } else {
- }
- }, 100);
- },
- }).register('listsGroup');
- class MoveSwimlaneComponent extends BlazeComponent {
- serverMethod = 'moveSwimlane';
- onCreated() {
- this.currentSwimlane = this.currentData();
- }
- board() {
- return Utils.getCurrentBoard();
- }
- toBoardsSelector() {
- return {
- archived: false,
- 'members.userId': Meteor.userId(),
- type: 'board',
- _id: { $ne: this.board()._id },
- };
- }
- toBoards() {
- const ret = ReactiveCache.getBoards(this.toBoardsSelector(), { sort: { title: 1 } });
- return ret;
- }
- events() {
- return [
- {
- 'click .js-done'() {
- const bSelect = $('.js-select-boards')[0];
- let boardId;
- if (bSelect) {
- boardId = bSelect.options[bSelect.selectedIndex].value;
- Meteor.call(this.serverMethod, this.currentSwimlane._id, boardId);
- }
- Popup.back();
- },
- },
- ];
- }
- }
- MoveSwimlaneComponent.register('moveSwimlanePopup');
- (class extends MoveSwimlaneComponent {
- serverMethod = 'copySwimlane';
- toBoardsSelector() {
- const selector = super.toBoardsSelector();
- delete selector._id;
- return selector;
- }
- }.register('copySwimlanePopup'));
|