From 1e6252de7f26f3af14a99fb63b5dac27ba0576f3 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Mon, 20 Oct 2025 00:22:26 +0300 Subject: [PATCH] When opening board, migrate from Shared Lists to Per-Swimlane Lists. Thanks to xet7 ! Fixes #5952 --- client/components/boards/boardBody.js | 97 ++++++++++++ client/components/boards/boardHeader.jade | 5 - client/components/boards/boardHeader.js | 22 --- imports/i18n/data/en.i18n.json | 7 +- models/boards.js | 5 +- server/00checkStartup.js | 8 +- server/boardMigrationDetector.js | 14 +- server/cronMigrationManager.js | 173 +--------------------- 8 files changed, 112 insertions(+), 219 deletions(-) diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index 56c38f590..7f8883d0b 100644 --- a/client/components/boards/boardBody.js +++ b/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 this.startAttachmentMigrationIfNeeded(boardId); } 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) { try { // Check if board has already been migrated diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index 372dfb07f..208753243 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -130,11 +130,6 @@ 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'}}") | ☰ diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index 574b45e4d..72271f44a 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -152,32 +152,10 @@ 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({ diff --git a/imports/i18n/data/en.i18n.json b/imports/i18n/data/en.i18n.json index f106b6528..48daee11b 100644 --- a/imports/i18n/data/en.i18n.json +++ b/imports/i18n/data/en.i18n.json @@ -1488,10 +1488,5 @@ "weight": "Weight", "idle": "Idle", "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" } diff --git a/models/boards.js b/models/boards.js index f62016d71..d08e7fe9e 100644 --- a/models/boards.js +++ b/models/boards.js @@ -778,10 +778,11 @@ Boards.helpers({ return this.permission === 'public'; }, - hasLegacyLists() { - return this.hasLegacyLists === true; + hasSharedListsConverted() { + return this.hasSharedListsConverted === true; }, + cards() { const ret = ReactiveCache.getCards( { boardId: this._id, archived: false }, diff --git a/server/00checkStartup.js b/server/00checkStartup.js index d4d152a1e..2ff0f2bcc 100644 --- a/server/00checkStartup.js +++ b/server/00checkStartup.js @@ -40,8 +40,6 @@ if (errors.length > 0) { // Import cron job storage for persistent job tracking 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'; diff --git a/server/boardMigrationDetector.js b/server/boardMigrationDetector.js index cd553ac47..b1dfb2f1e 100644 --- a/server/boardMigrationDetector.js +++ b/server/boardMigrationDetector.js @@ -337,13 +337,13 @@ class BoardMigrationDetector { // Export singleton instance 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({ diff --git a/server/cronMigrationManager.js b/server/cronMigrationManager.js index a42e9b335..413f214ba 100644 --- a/server/cronMigrationManager.js +++ b/server/cronMigrationManager.js @@ -233,17 +233,6 @@ class CronMigrationManager { 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' - } ]; } @@ -441,14 +430,6 @@ 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 }, @@ -465,10 +446,7 @@ class CronMigrationManager { async executeMigrationStep(jobId, stepIndex, stepData, stepId) { 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; for (let i = 0; i <= progressSteps; i++) { 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 @@ -1420,53 +1309,6 @@ 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 @@ -1666,17 +1508,4 @@ Meteor.methods({ 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(); - } });