Explorar o código

Legacy Lists button at one board view to restore missing lists/cards.

Thanks to xet7 !

Fixes #5952
Lauri Ojansivu hai 4 días
pai
achega
951d2e4937

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

@@ -130,6 +130,11 @@ template(name="boardHeaderBar")
           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
       a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
         | ☰

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

@@ -152,9 +152,32 @@ BlazeComponent.extendComponent({
         'click .js-log-in'() {
           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');
 
 Template.boardHeaderBar.helpers({

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

@@ -1488,5 +1488,10 @@
   "weight": "Weight",
   "idle": "Idle",
   "complete": "Complete",
-  "cron": "Cron"
+  "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"
 }

+ 4 - 0
models/boards.js

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

+ 194 - 12
server/cronMigrationManager.js

@@ -232,6 +232,17 @@ class CronMigrationManager {
         cronName: 'migration_lists_per_swimlane',
         schedule: 'every 1 minute',
         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'
       }
     ];
   }
@@ -357,9 +368,16 @@ class CronMigrationManager {
    * Execute a migration job
    */
   async executeMigrationJob(jobId, jobData) {
+    if (!jobData) {
+      throw new Error('Job data is required for migration execution');
+    }
+    
     const { stepId } = jobData;
-    const step = this.migrationSteps.find(s => s.id === stepId);
+    if (!stepId) {
+      throw new Error('Step ID is required in job data');
+    }
     
+    const step = this.migrationSteps.find(s => s.id === stepId);
     if (!step) {
       throw new Error(`Migration step ${stepId} not found`);
     }
@@ -378,7 +396,7 @@ class CronMigrationManager {
       });
 
       // Execute step
-      await this.executeMigrationStep(jobId, i, stepData);
+      await this.executeMigrationStep(jobId, i, stepData, stepId);
 
       // Mark step as completed
       cronJobStorage.saveJobStep(jobId, i, {
@@ -423,6 +441,14 @@ class CronMigrationManager {
           { name: 'Cleanup old data', duration: 1000 }
         );
         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:
         steps.push(
           { name: `Execute ${step.name}`, duration: 2000 },
@@ -436,22 +462,116 @@ class CronMigrationManager {
   /**
    * Execute a migration step
    */
-  async executeMigrationStep(jobId, stepIndex, stepData) {
+  async executeMigrationStep(jobId, stepIndex, stepData, stepId) {
     const { name, duration } = stepData;
     
-    // Simulate step execution with progress updates
-    const progressSteps = 10;
-    for (let i = 0; i <= progressSteps; i++) {
-      const progress = Math.round((i / progressSteps) * 100);
+    if (stepId === 'restore-legacy-lists') {
+      await this.executeRestoreLegacyListsMigration(jobId, stepIndex, stepData);
+    } else {
+      // Simulate step execution with progress updates for other migrations
+      const progressSteps = 10;
+      for (let i = 0; i <= progressSteps; i++) {
+        const progress = Math.round((i / progressSteps) * 100);
+        
+        // Update step progress
+        cronJobStorage.saveJobStep(jobId, stepIndex, {
+          progress,
+          currentAction: `Executing: ${name} (${progress}%)`
+        });
+        
+        // Simulate work
+        await new Promise(resolve => setTimeout(resolve, duration / progressSteps));
+      }
+    }
+  }
+
+  /**
+   * 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');
       
-      // Update step progress
+      // Step 1: Identify legacy lists
       cronJobStorage.saveJobStep(jobId, stepIndex, {
-        progress,
-        currentAction: `Executing: ${name} (${progress}%)`
+        progress: 25,
+        currentAction: 'Identifying legacy lists...'
       });
       
-      // Simulate work
-      await new Promise(resolve => setTimeout(resolve, duration / progressSteps));
+      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;
     }
   }
 
@@ -1299,6 +1419,54 @@ class CronMigrationManager {
     
     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
@@ -1496,5 +1664,19 @@ Meteor.methods({
     // Import the board migration detector
     const { boardMigrationDetector } = require('./boardMigrationDetector');
     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();
   }
 });