Browse Source

When opening board, migrate from Shared Lists to Per-Swimlane Lists.

Thanks to xet7 !

Fixes #5952
Lauri Ojansivu 3 days ago
parent
commit
1e6252de7f

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

@@ -112,6 +112,9 @@ BlazeComponent.extendComponent({
         }
         }
       }
       }
 
 
+      // Convert shared lists to per-swimlane lists if needed
+      await this.convertSharedListsToPerSwimlane(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) {
@@ -139,6 +142,100 @@ BlazeComponent.extendComponent({
     }
     }
   },
   },
 
 
+  async convertSharedListsToPerSwimlane(boardId) {
+    try {
+      const board = ReactiveCache.getBoard(boardId);
+      if (!board) return;
+
+      // Check if board has already been processed for shared lists conversion
+      if (board.hasSharedListsConverted) {
+        if (process.env.DEBUG === 'true') {
+          console.log(`Board ${boardId} has already been processed for shared lists conversion`);
+        }
+        return;
+      }
+
+      // Get all lists for this board
+      const allLists = board.lists();
+      const swimlanes = board.swimlanes();
+      
+      if (swimlanes.length === 0) {
+        if (process.env.DEBUG === 'true') {
+          console.log(`Board ${boardId} has no swimlanes, skipping shared lists conversion`);
+        }
+        return;
+      }
+
+      // Find shared lists (lists with empty swimlaneId or null swimlaneId)
+      const sharedLists = allLists.filter(list => !list.swimlaneId || list.swimlaneId === '');
+      
+      if (sharedLists.length === 0) {
+        if (process.env.DEBUG === 'true') {
+          console.log(`Board ${boardId} has no shared lists to convert`);
+        }
+        // Mark as processed even if no shared lists
+        Meteor.call('boards.update', boardId, { $set: { hasSharedListsConverted: true } });
+        return;
+      }
+
+      if (process.env.DEBUG === 'true') {
+        console.log(`Converting ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`);
+      }
+
+      // Convert each shared list to per-swimlane lists
+      for (const sharedList of sharedLists) {
+        // Create a copy of the list for each swimlane
+        for (const swimlane of swimlanes) {
+          // Check if this list already exists in this swimlane
+          const existingList = Lists.findOne({
+            boardId: boardId,
+            swimlaneId: swimlane._id,
+            title: sharedList.title
+          });
+
+          if (!existingList) {
+            // Create a new list in this swimlane
+            const newListData = {
+              title: sharedList.title,
+              boardId: boardId,
+              swimlaneId: swimlane._id,
+              sort: sharedList.sort || 0,
+              archived: sharedList.archived || false,
+              createdAt: new Date(),
+              modifiedAt: new Date()
+            };
+
+            // Copy other properties if they exist
+            if (sharedList.color) newListData.color = sharedList.color;
+            if (sharedList.wipLimit) newListData.wipLimit = sharedList.wipLimit;
+            if (sharedList.wipLimitEnabled) newListData.wipLimitEnabled = sharedList.wipLimitEnabled;
+            if (sharedList.wipLimitSoft) newListData.wipLimitSoft = sharedList.wipLimitSoft;
+
+            Lists.insert(newListData);
+          }
+        }
+
+        // Archive or remove the original shared list
+        Lists.update(sharedList._id, {
+          $set: {
+            archived: true,
+            modifiedAt: new Date()
+          }
+        });
+      }
+
+      // Mark board as processed
+      Meteor.call('boards.update', boardId, { $set: { hasSharedListsConverted: true } });
+
+      if (process.env.DEBUG === 'true') {
+        console.log(`Successfully converted ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`);
+      }
+
+    } catch (error) {
+      console.error('Error converting shared lists to per-swimlane:', error);
+    }
+  },
+
   async startAttachmentMigrationIfNeeded(boardId) {
   async startAttachmentMigrationIfNeeded(boardId) {
     try {
     try {
       // Check if board has already been migrated
       // Check if board has already been migrated

+ 0 - 5
client/components/boards/boardHeader.jade

@@ -130,11 +130,6 @@ template(name="boardHeaderBar")
           a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
           a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
             | ❌
             | ❌
 
 
-      if currentUser.isBoardAdmin
-        a.board-header-btn.js-restore-legacy-lists(title="{{_ 'restore-legacy-lists'}}")
-          | 🔄
-          | {{_ 'legacy-lists'}}
-
       .separator
       .separator
       a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
       a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
         | ☰
         | ☰

+ 0 - 22
client/components/boards/boardHeader.js

@@ -152,32 +152,10 @@ BlazeComponent.extendComponent({
         'click .js-log-in'() {
         'click .js-log-in'() {
           FlowRouter.go('atSignIn');
           FlowRouter.go('atSignIn');
         },
         },
-        'click .js-restore-legacy-lists'() {
-          this.restoreLegacyLists();
-        },
       },
       },
     ];
     ];
   },
   },
 
 
-  restoreLegacyLists() {
-    // Show confirmation dialog
-    if (confirm('Are you sure you want to restore legacy lists to their original shared state? This will make them appear in all swimlanes.')) {
-      // Call cron method to restore legacy lists
-      Meteor.call('cron.triggerRestoreLegacyLists', (error, result) => {
-        if (error) {
-          console.error('Error restoring legacy lists:', error);
-          alert(`Error: ${error.message}`);
-        } else {
-          console.log('Successfully triggered restore legacy lists migration:', result);
-          alert(`Migration triggered successfully. Job ID: ${result.jobId}`);
-          // Refresh the board to show the restored lists
-          setTimeout(() => {
-            window.location.reload();
-          }, 2000);
-        }
-      });
-    }
-  },
 }).register('boardHeaderBar');
 }).register('boardHeaderBar');
 
 
 Template.boardHeaderBar.helpers({
 Template.boardHeaderBar.helpers({

+ 1 - 6
imports/i18n/data/en.i18n.json

@@ -1488,10 +1488,5 @@
   "weight": "Weight",
   "weight": "Weight",
   "idle": "Idle",
   "idle": "Idle",
   "complete": "Complete",
   "complete": "Complete",
-  "cron": "Cron",
-  "legacy-lists": "Legacy Lists",
-  "restore-legacy-lists": "Restore Legacy Lists",
-  "legacy-lists-restore": "Legacy Lists Restore",
-  "legacy-lists-restore-description": "Restore legacy lists to their original shared state across all swimlanes",
-  "restoring-legacy-lists": "Restoring legacy lists"
+  "cron": "Cron"
 }
 }

+ 3 - 2
models/boards.js

@@ -778,10 +778,11 @@ Boards.helpers({
     return this.permission === 'public';
     return this.permission === 'public';
   },
   },
 
 
-  hasLegacyLists() {
-    return this.hasLegacyLists === true;
+  hasSharedListsConverted() {
+    return this.hasSharedListsConverted === true;
   },
   },
 
 
+
   cards() {
   cards() {
     const ret = ReactiveCache.getCards(
     const ret = ReactiveCache.getCards(
       { boardId: this._id, archived: false },
       { boardId: this._id, archived: false },

+ 3 - 5
server/00checkStartup.js

@@ -40,8 +40,6 @@ 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 board migration detector for automatic board migrations
-import './boardMigrationDetector';
-
-// Import cron migration manager for cron-based migrations
-import './cronMigrationManager';
+// Note: Automatic migrations are disabled - migrations only run when opening boards
+// import './boardMigrationDetector';
+// import './cronMigrationManager';

+ 7 - 7
server/boardMigrationDetector.js

@@ -337,13 +337,13 @@ class BoardMigrationDetector {
 // Export singleton instance
 // Export singleton instance
 export const boardMigrationDetector = new BoardMigrationDetector();
 export const boardMigrationDetector = new BoardMigrationDetector();
 
 
-// Start the detector on server startup
-Meteor.startup(() => {
-  // Wait a bit for the system to initialize
-  Meteor.setTimeout(() => {
-    boardMigrationDetector.start();
-  }, 10000); // Start after 10 seconds
-});
+// Note: Automatic migration detector is disabled - migrations only run when opening boards
+// Meteor.startup(() => {
+//   // Wait a bit for the system to initialize
+//   Meteor.setTimeout(() => {
+//     boardMigrationDetector.start();
+//   }, 10000); // Start after 10 seconds
+// });
 
 
 // Meteor methods for client access
 // Meteor methods for client access
 Meteor.methods({
 Meteor.methods({

+ 1 - 172
server/cronMigrationManager.js

@@ -233,17 +233,6 @@ class CronMigrationManager {
         schedule: 'every 1 minute',
         schedule: 'every 1 minute',
         status: 'stopped'
         status: 'stopped'
       },
       },
-      {
-        id: 'restore-legacy-lists',
-        name: 'Restore Legacy Lists',
-        description: 'Restore legacy lists to their original shared state across all swimlanes',
-        weight: 3,
-        completed: false,
-        progress: 0,
-        cronName: 'migration_restore_legacy_lists',
-        schedule: 'every 1 minute',
-        status: 'stopped'
-      }
     ];
     ];
   }
   }
 
 
@@ -441,14 +430,6 @@ class CronMigrationManager {
           { name: 'Cleanup old data', duration: 1000 }
           { name: 'Cleanup old data', duration: 1000 }
         );
         );
         break;
         break;
-      case 'restore-legacy-lists':
-        steps.push(
-          { name: 'Identify legacy lists', duration: 1000 },
-          { name: 'Restore lists to shared state', duration: 2000 },
-          { name: 'Update board settings', duration: 500 },
-          { name: 'Verify restoration', duration: 500 }
-        );
-        break;
       default:
       default:
         steps.push(
         steps.push(
           { name: `Execute ${step.name}`, duration: 2000 },
           { name: `Execute ${step.name}`, duration: 2000 },
@@ -465,10 +446,7 @@ class CronMigrationManager {
   async executeMigrationStep(jobId, stepIndex, stepData, stepId) {
   async executeMigrationStep(jobId, stepIndex, stepData, stepId) {
     const { name, duration } = stepData;
     const { name, duration } = stepData;
     
     
-    if (stepId === 'restore-legacy-lists') {
-      await this.executeRestoreLegacyListsMigration(jobId, stepIndex, stepData);
-    } else {
-      // Simulate step execution with progress updates for other migrations
+    // Simulate step execution with progress updates for other migrations
       const progressSteps = 10;
       const progressSteps = 10;
       for (let i = 0; i <= progressSteps; i++) {
       for (let i = 0; i <= progressSteps; i++) {
         const progress = Math.round((i / progressSteps) * 100);
         const progress = Math.round((i / progressSteps) * 100);
@@ -485,95 +463,6 @@ class CronMigrationManager {
     }
     }
   }
   }
 
 
-  /**
-   * Execute the restore legacy lists migration
-   */
-  async executeRestoreLegacyListsMigration(jobId, stepIndex, stepData) {
-    const { name } = stepData;
-    
-    try {
-      // Import collections directly for server-side access
-      const { default: Boards } = await import('/models/boards');
-      const { default: Lists } = await import('/models/lists');
-      
-      // Step 1: Identify legacy lists
-      cronJobStorage.saveJobStep(jobId, stepIndex, {
-        progress: 25,
-        currentAction: 'Identifying legacy lists...'
-      });
-      
-      const boards = Boards.find({}).fetch();
-      const migrationDate = new Date('2025-10-10T21:14:44.000Z'); // Date of commit 719ef87efceacfe91461a8eeca7cf74d11f4cc0a
-      let totalLegacyLists = 0;
-      
-      for (const board of boards) {
-        const allLists = Lists.find({ boardId: board._id }).fetch();
-        const legacyLists = allLists.filter(list => {
-          const listDate = list.createdAt || new Date(0);
-          return listDate < migrationDate && list.swimlaneId && list.swimlaneId !== '';
-        });
-        totalLegacyLists += legacyLists.length;
-      }
-      
-      // Step 2: Restore lists to shared state
-      cronJobStorage.saveJobStep(jobId, stepIndex, {
-        progress: 50,
-        currentAction: 'Restoring lists to shared state...'
-      });
-      
-      let restoredCount = 0;
-      
-      for (const board of boards) {
-        const allLists = Lists.find({ boardId: board._id }).fetch();
-        const legacyLists = allLists.filter(list => {
-          const listDate = list.createdAt || new Date(0);
-          return listDate < migrationDate && list.swimlaneId && list.swimlaneId !== '';
-        });
-        
-        // Restore legacy lists to shared state (empty swimlaneId)
-        for (const list of legacyLists) {
-          Lists.direct.update(list._id, {
-            $set: {
-              swimlaneId: ''
-            }
-          });
-          restoredCount++;
-        }
-        
-        // Mark the board as having legacy lists
-        if (legacyLists.length > 0) {
-          Boards.direct.update(board._id, {
-            $set: {
-              hasLegacyLists: true
-            }
-          });
-        }
-      }
-      
-      // Step 3: Update board settings
-      cronJobStorage.saveJobStep(jobId, stepIndex, {
-        progress: 75,
-        currentAction: 'Updating board settings...'
-      });
-      
-      // Step 4: Verify restoration
-      cronJobStorage.saveJobStep(jobId, stepIndex, {
-        progress: 100,
-        currentAction: `Verification complete. Restored ${restoredCount} legacy lists.`
-      });
-      
-      console.log(`Successfully restored ${restoredCount} legacy lists across ${boards.length} boards`);
-      
-    } catch (error) {
-      console.error('Error during restore legacy lists migration:', error);
-      cronJobStorage.saveJobStep(jobId, stepIndex, {
-        progress: 0,
-        currentAction: `Error: ${error.message}`,
-        status: 'error'
-      });
-      throw error;
-    }
-  }
 
 
   /**
   /**
    * Execute a board operation job
    * Execute a board operation job
@@ -1420,53 +1309,6 @@ class CronMigrationManager {
     return stats;
     return stats;
   }
   }
 
 
-  /**
-   * Trigger restore legacy lists migration
-   */
-  async triggerRestoreLegacyListsMigration() {
-    try {
-      // Find the restore legacy lists step
-      const step = this.migrationSteps.find(s => s.id === 'restore-legacy-lists');
-      if (!step) {
-        throw new Error('Restore legacy lists migration step not found');
-      }
-
-      // Create a job for this migration
-      const jobId = `restore_legacy_lists_${Date.now()}`;
-      cronJobStorage.addToQueue(jobId, 'migration', step.weight, {
-        stepId: step.id,
-        stepName: step.name,
-        stepDescription: step.description
-      });
-
-      // Save initial job status
-      cronJobStorage.saveJobStatus(jobId, {
-        jobType: 'migration',
-        status: 'pending',
-        progress: 0,
-        stepId: step.id,
-        stepName: step.name
-      });
-
-      // Execute the migration immediately
-      const jobData = {
-        stepId: step.id,
-        stepName: step.name,
-        stepDescription: step.description
-      };
-      await this.executeMigrationJob(jobId, jobData);
-
-      return {
-        success: true,
-        jobId: jobId,
-        message: 'Restore legacy lists migration triggered successfully'
-      };
-
-    } catch (error) {
-      console.error('Error triggering restore legacy lists migration:', error);
-      throw new Meteor.Error('migration-trigger-failed', `Failed to trigger migration: ${error.message}`);
-    }
-  }
 }
 }
 
 
 // Export singleton instance
 // Export singleton instance
@@ -1666,17 +1508,4 @@ Meteor.methods({
     return boardMigrationDetector.forceScan();
     return boardMigrationDetector.forceScan();
   },
   },
 
 
-  'cron.triggerRestoreLegacyLists'() {
-    if (!this.userId) {
-      throw new Meteor.Error('not-authorized');
-    }
-
-    // Check if user is admin (optional - you can remove this if you want any user to trigger it)
-    const user = ReactiveCache.getCurrentUser();
-    if (!user || !user.isAdmin) {
-      throw new Meteor.Error('not-authorized', 'Only administrators can trigger this migration');
-    }
-
-    return cronMigrationManager.triggerRestoreLegacyListsMigration();
-  }
 });
 });