| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115 |
- 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
- } else {
- // If staying in the same swimlane, cancel the sortable to prevent DOM manipulation issues
- $listsDom.sortable('cancel');
- }
- 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();
- },
- });
- // 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
- } else {
- // If staying in the same swimlane, cancel the sortable to prevent DOM manipulation issues
- $swimlane.sortable('cancel');
- }
- 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
- } else {
- // If staying in the same swimlane, cancel the sortable to prevent DOM manipulation issues
- $listsGroup.sortable('cancel');
- }
- 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'));
|