Browse Source

When opening board, add missing lists.

Thanks to xet7 !

Fixes #5926
Lauri Ojansivu 2 days ago
parent
commit
80777b4663

+ 58 - 0
client/components/boards/boardBody.js

@@ -115,6 +115,9 @@ BlazeComponent.extendComponent({
       // Convert shared lists to per-swimlane lists if needed
       // Convert shared lists to per-swimlane lists if needed
       await this.convertSharedListsToPerSwimlane(boardId);
       await this.convertSharedListsToPerSwimlane(boardId);
 
 
+      // Fix missing lists migration (for cards with wrong listId references)
+      await this.fixMissingLists(boardId);
+
       // Start attachment migration in background if needed
       // Start attachment migration in background if needed
       this.startAttachmentMigrationIfNeeded(boardId);
       this.startAttachmentMigrationIfNeeded(boardId);
     } catch (error) {
     } catch (error) {
@@ -236,6 +239,61 @@ BlazeComponent.extendComponent({
     }
     }
   },
   },
 
 
+  async fixMissingLists(boardId) {
+    try {
+      const board = ReactiveCache.getBoard(boardId);
+      if (!board) return;
+
+      // Check if board has already been processed for missing lists fix
+      if (board.fixMissingListsCompleted) {
+        if (process.env.DEBUG === 'true') {
+          console.log(`Board ${boardId} has already been processed for missing lists fix`);
+        }
+        return;
+      }
+
+      // Check if migration is needed
+      const needsMigration = await new Promise((resolve, reject) => {
+        Meteor.call('fixMissingListsMigration.needsMigration', boardId, (error, result) => {
+          if (error) {
+            reject(error);
+          } else {
+            resolve(result);
+          }
+        });
+      });
+
+      if (!needsMigration) {
+        if (process.env.DEBUG === 'true') {
+          console.log(`Board ${boardId} does not need missing lists fix`);
+        }
+        return;
+      }
+
+      if (process.env.DEBUG === 'true') {
+        console.log(`Starting fix missing lists migration for board ${boardId}`);
+      }
+
+      // Execute the migration
+      const result = await new Promise((resolve, reject) => {
+        Meteor.call('fixMissingListsMigration.execute', boardId, (error, result) => {
+          if (error) {
+            reject(error);
+          } else {
+            resolve(result);
+          }
+        });
+      });
+
+      if (result && result.success) {
+        console.log(`Successfully fixed missing lists for board ${boardId}: created ${result.createdLists} lists, updated ${result.updatedCards} cards`);
+      }
+
+    } catch (error) {
+      console.error('Error fixing missing lists:', error);
+    }
+  },
+
   async startAttachmentMigrationIfNeeded(boardId) {
   async startAttachmentMigrationIfNeeded(boardId) {
     try {
     try {
       // Check if board has already been migrated
       // Check if board has already been migrated

+ 0 - 14
models/boards.js

@@ -803,13 +803,6 @@ Boards.helpers({
       {
       {
         boardId: this._id,
         boardId: this._id,
         archived: false,
         archived: false,
-        // Get lists for all swimlanes in this board, plus lists without swimlaneId for backward compatibility
-        $or: [
-          { swimlaneId: { $in: this.swimlanes().map(s => s._id) } },
-          { swimlaneId: { $exists: false } },
-          { swimlaneId: '' },
-          { swimlaneId: null }
-        ],
       },
       },
       { sort: sortKey },
       { sort: sortKey },
     );
     );
@@ -819,13 +812,6 @@ Boards.helpers({
     return ReactiveCache.getLists(
     return ReactiveCache.getLists(
       {
       {
         boardId: this._id,
         boardId: this._id,
-        // Get lists for all swimlanes in this board, plus lists without swimlaneId for backward compatibility
-        $or: [
-          { swimlaneId: { $in: this.swimlanes().map(s => s._id) } },
-          { swimlaneId: { $exists: false } },
-          { swimlaneId: '' },
-          { swimlaneId: null }
-        ]
       },
       },
       { sort: { sort: 1 } }
       { sort: { sort: 1 } }
     );
     );

+ 4 - 16
models/swimlanes.js

@@ -211,33 +211,20 @@ Swimlanes.helpers({
     return this.draggableLists();
     return this.draggableLists();
   },
   },
   newestLists() {
   newestLists() {
-    // sorted lists from newest to the oldest, by its creation date or its cards' last modification date
-    // Include lists without swimlaneId for backward compatibility (they belong to default swimlane)
+    // Revert to shared lists across swimlanes: filter by board only
     return ReactiveCache.getLists(
     return ReactiveCache.getLists(
       {
       {
         boardId: this.boardId,
         boardId: this.boardId,
-        $or: [
-          { swimlaneId: this._id },
-          { swimlaneId: { $exists: false } },
-          { swimlaneId: '' },
-          { swimlaneId: null }
-        ],
         archived: false,
         archived: false,
       },
       },
       { sort: { modifiedAt: -1 } },
       { sort: { modifiedAt: -1 } },
     );
     );
   },
   },
   draggableLists() {
   draggableLists() {
-    // Include lists without swimlaneId for backward compatibility (they belong to default swimlane)
+    // Revert to shared lists across swimlanes: filter by board only
     return ReactiveCache.getLists(
     return ReactiveCache.getLists(
       {
       {
         boardId: this.boardId,
         boardId: this.boardId,
-        $or: [
-          { swimlaneId: this._id },
-          { swimlaneId: { $exists: false } },
-          { swimlaneId: '' },
-          { swimlaneId: null }
-        ],
         //archived: false,
         //archived: false,
       },
       },
       { sort: ['sort'] },
       { sort: ['sort'] },
@@ -245,7 +232,8 @@ Swimlanes.helpers({
   },
   },
 
 
   myLists() {
   myLists() {
-    return ReactiveCache.getLists({ swimlaneId: this._id });
+    // Revert to shared lists: provide lists by board for this swimlane's board
+    return ReactiveCache.getLists({ boardId: this.boardId });
   },
   },
 
 
   allCards() {
   allCards() {

+ 3 - 0
server/00checkStartup.js

@@ -40,6 +40,9 @@ if (errors.length > 0) {
 // Import cron job storage for persistent job tracking
 // Import cron job storage for persistent job tracking
 import './cronJobStorage';
 import './cronJobStorage';
 
 
+// Import migrations
+import './migrations/fixMissingListsMigration';
+
 // Note: Automatic migrations are disabled - migrations only run when opening boards
 // Note: Automatic migrations are disabled - migrations only run when opening boards
 // import './boardMigrationDetector';
 // import './boardMigrationDetector';
 // import './cronMigrationManager';
 // import './cronMigrationManager';

+ 266 - 0
server/migrations/fixMissingListsMigration.js

@@ -0,0 +1,266 @@
+/**
+ * Fix Missing Lists Migration
+ * 
+ * This migration fixes the issue where cards have incorrect listId references
+ * due to the per-swimlane lists change. It detects cards with mismatched
+ * listId/swimlaneId and creates the missing lists.
+ * 
+ * Issue: When upgrading from v7.94 to v8.02, cards that were in different
+ * swimlanes but shared the same list now have wrong listId references.
+ * 
+ * Example:
+ * - Card1: listId: 'HB93dWNnY5bgYdtxc', swimlaneId: 'sK69SseWkh3tMbJvg'
+ * - Card2: listId: 'HB93dWNnY5bgYdtxc', swimlaneId: 'XeecF9nZxGph4zcT4'
+ * 
+ * Card2 should have a different listId that corresponds to its swimlane.
+ */
+
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import { ReactiveCache } from '/imports/reactiveCache';
+
+class FixMissingListsMigration {
+  constructor() {
+    this.name = 'fix-missing-lists';
+    this.version = 1;
+  }
+
+  /**
+   * Check if migration is needed for a board
+   */
+  needsMigration(boardId) {
+    try {
+      const board = ReactiveCache.getBoard(boardId);
+      if (!board) return false;
+
+      // Check if board has already been processed
+      if (board.fixMissingListsCompleted) {
+        return false;
+      }
+
+      // Check if there are cards with mismatched listId/swimlaneId
+      const cards = ReactiveCache.getCards({ boardId });
+      const lists = ReactiveCache.getLists({ boardId });
+      
+      // Create a map of listId -> swimlaneId for existing lists
+      const listSwimlaneMap = new Map();
+      lists.forEach(list => {
+        listSwimlaneMap.set(list._id, list.swimlaneId || '');
+      });
+
+      // Check for cards with mismatched listId/swimlaneId
+      for (const card of cards) {
+        const expectedSwimlaneId = listSwimlaneMap.get(card.listId);
+        if (expectedSwimlaneId && expectedSwimlaneId !== card.swimlaneId) {
+          console.log(`Found mismatched card: ${card._id}, listId: ${card.listId}, card swimlaneId: ${card.swimlaneId}, list swimlaneId: ${expectedSwimlaneId}`);
+          return true;
+        }
+      }
+
+      return false;
+    } catch (error) {
+      console.error('Error checking if migration is needed:', error);
+      return false;
+    }
+  }
+
+  /**
+   * Execute the migration for a board
+   */
+  async executeMigration(boardId) {
+    try {
+      console.log(`Starting fix missing lists migration for board ${boardId}`);
+      
+      const board = ReactiveCache.getBoard(boardId);
+      if (!board) {
+        throw new Error(`Board ${boardId} not found`);
+      }
+
+      const cards = ReactiveCache.getCards({ boardId });
+      const lists = ReactiveCache.getLists({ boardId });
+      const swimlanes = ReactiveCache.getSwimlanes({ boardId });
+
+      // Create maps for efficient lookup
+      const listSwimlaneMap = new Map();
+      const swimlaneListsMap = new Map();
+      
+      lists.forEach(list => {
+        listSwimlaneMap.set(list._id, list.swimlaneId || '');
+        if (!swimlaneListsMap.has(list.swimlaneId || '')) {
+          swimlaneListsMap.set(list.swimlaneId || '', []);
+        }
+        swimlaneListsMap.get(list.swimlaneId || '').push(list);
+      });
+
+      // Group cards by swimlaneId
+      const cardsBySwimlane = new Map();
+      cards.forEach(card => {
+        if (!cardsBySwimlane.has(card.swimlaneId)) {
+          cardsBySwimlane.set(card.swimlaneId, []);
+        }
+        cardsBySwimlane.get(card.swimlaneId).push(card);
+      });
+
+      let createdLists = 0;
+      let updatedCards = 0;
+
+      // Process each swimlane
+      for (const [swimlaneId, swimlaneCards] of cardsBySwimlane) {
+        if (!swimlaneId) continue;
+
+        // Get existing lists for this swimlane
+        const existingLists = swimlaneListsMap.get(swimlaneId) || [];
+        const existingListTitles = new Set(existingLists.map(list => list.title));
+
+        // Group cards by their current listId
+        const cardsByListId = new Map();
+        swimlaneCards.forEach(card => {
+          if (!cardsByListId.has(card.listId)) {
+            cardsByListId.set(card.listId, []);
+          }
+          cardsByListId.get(card.listId).push(card);
+        });
+
+        // For each listId used by cards in this swimlane
+        for (const [listId, cardsInList] of cardsByListId) {
+          const originalList = lists.find(l => l._id === listId);
+          if (!originalList) continue;
+
+          // Check if this list's swimlaneId matches the card's swimlaneId
+          const listSwimlaneId = listSwimlaneMap.get(listId);
+          if (listSwimlaneId === swimlaneId) {
+            // List is already correctly assigned to this swimlane
+            continue;
+          }
+
+          // Check if we already have a list with the same title in this swimlane
+          let targetList = existingLists.find(list => list.title === originalList.title);
+          
+          if (!targetList) {
+            // Create a new list for this swimlane
+            const newListData = {
+              title: originalList.title,
+              boardId: boardId,
+              swimlaneId: swimlaneId,
+              sort: originalList.sort || 0,
+              archived: originalList.archived || false,
+              createdAt: new Date(),
+              modifiedAt: new Date(),
+              type: originalList.type || 'list'
+            };
+
+            // Copy other properties if they exist
+            if (originalList.color) newListData.color = originalList.color;
+            if (originalList.wipLimit) newListData.wipLimit = originalList.wipLimit;
+            if (originalList.wipLimitEnabled) newListData.wipLimitEnabled = originalList.wipLimitEnabled;
+            if (originalList.wipLimitSoft) newListData.wipLimitSoft = originalList.wipLimitSoft;
+            if (originalList.starred) newListData.starred = originalList.starred;
+            if (originalList.collapsed) newListData.collapsed = originalList.collapsed;
+
+            // Insert the new list
+            const newListId = ReactiveCache.getCollection('lists').insert(newListData);
+            targetList = { _id: newListId, ...newListData };
+            createdLists++;
+            
+            console.log(`Created new list "${originalList.title}" for swimlane ${swimlaneId}`);
+          }
+
+          // Update all cards in this group to use the correct listId
+          for (const card of cardsInList) {
+            ReactiveCache.getCollection('cards').update(card._id, {
+              $set: {
+                listId: targetList._id,
+                modifiedAt: new Date()
+              }
+            });
+            updatedCards++;
+          }
+        }
+      }
+
+      // Mark board as processed
+      ReactiveCache.getCollection('boards').update(boardId, {
+        $set: {
+          fixMissingListsCompleted: true,
+          fixMissingListsCompletedAt: new Date()
+        }
+      });
+
+      console.log(`Fix missing lists migration completed for board ${boardId}: created ${createdLists} lists, updated ${updatedCards} cards`);
+      
+      return {
+        success: true,
+        createdLists,
+        updatedCards
+      };
+
+    } catch (error) {
+      console.error(`Error executing fix missing lists migration for board ${boardId}:`, error);
+      throw error;
+    }
+  }
+
+  /**
+   * Get migration status for a board
+   */
+  getMigrationStatus(boardId) {
+    try {
+      const board = ReactiveCache.getBoard(boardId);
+      if (!board) {
+        return { status: 'board_not_found' };
+      }
+
+      if (board.fixMissingListsCompleted) {
+        return { 
+          status: 'completed',
+          completedAt: board.fixMissingListsCompletedAt
+        };
+      }
+
+      const needsMigration = this.needsMigration(boardId);
+      return {
+        status: needsMigration ? 'needed' : 'not_needed'
+      };
+
+    } catch (error) {
+      console.error('Error getting migration status:', error);
+      return { status: 'error', error: error.message };
+    }
+  }
+}
+
+// Export singleton instance
+export const fixMissingListsMigration = new FixMissingListsMigration();
+
+// Meteor methods
+Meteor.methods({
+  'fixMissingListsMigration.check'(boardId) {
+    check(boardId, String);
+    
+    if (!this.userId) {
+      throw new Meteor.Error('not-authorized');
+    }
+    
+    return fixMissingListsMigration.getMigrationStatus(boardId);
+  },
+
+  'fixMissingListsMigration.execute'(boardId) {
+    check(boardId, String);
+    
+    if (!this.userId) {
+      throw new Meteor.Error('not-authorized');
+    }
+    
+    return fixMissingListsMigration.executeMigration(boardId);
+  },
+
+  'fixMissingListsMigration.needsMigration'(boardId) {
+    check(boardId, String);
+    
+    if (!this.userId) {
+      throw new Meteor.Error('not-authorized');
+    }
+    
+    return fixMissingListsMigration.needsMigration(boardId);
+  }
+});