Bläddra i källkod

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 !
Lauri Ojansivu 5 dagar sedan
förälder
incheckning
719ef87efc
5 ändrade filer med 144 tillägg och 12 borttagningar
  1. 63 6
      client/components/swimlanes/swimlanes.js
  2. 12 1
      models/boards.js
  3. 5 3
      models/lists.js
  4. 3 2
      models/swimlanes.js
  5. 61 0
      server/migrations.js

+ 63 - 6
client/components/swimlanes/swimlanes.js

@@ -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 = '';

+ 12 - 1
models/boards.js

@@ -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);
     }

+ 5 - 3
models/lists.js

@@ -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,

+ 3 - 2
models/swimlanes.js

@@ -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'] },

+ 61 - 0
server/migrations.js

@@ -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;
+  }
+});