From 1b25d1d5720d4f486a10d2acce37e315cf9b6057 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Wed, 5 Nov 2025 17:06:26 +0200 Subject: [PATCH] Moved migrations from opening board to right sidebar / Migrations. Thanks to xet7 ! --- client/components/boards/boardBody.js | 21 +-- client/components/sidebar/sidebar.jade | 4 + client/components/sidebar/sidebar.js | 5 + .../components/sidebar/sidebarMigrations.jade | 69 +++++++++ .../components/sidebar/sidebarMigrations.js | 143 ++++++++++++++++++ imports/i18n/data/en.i18n.json | 24 +++ server/migrations/fixAllFileUrls.js | 73 ++++----- 7 files changed, 277 insertions(+), 62 deletions(-) create mode 100644 client/components/sidebar/sidebarMigrations.jade create mode 100644 client/components/sidebar/sidebarMigrations.js diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index e8e83a134..4e14d8001 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -99,24 +99,9 @@ BlazeComponent.extendComponent({ return; } - // Check if board needs comprehensive migration - const needsMigration = await this.checkComprehensiveMigration(boardId); - - if (needsMigration) { - // Start comprehensive migration - this.isMigrating.set(true); - const success = await this.executeComprehensiveMigration(boardId); - this.isMigrating.set(false); - - if (success) { - this.isBoardReady.set(true); - } else { - console.error('Comprehensive migration failed, setting ready to true anyway'); - this.isBoardReady.set(true); // Still show board even if migration failed - } - } else { - this.isBoardReady.set(true); - } + // Automatic migration disabled - migrations must be run manually from sidebar + // Board admins can run migrations from the sidebar Migrations menu + this.isBoardReady.set(true); } catch (error) { console.error('Error during board conversion check:', error); diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index 662c84ad8..4a216c336 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -587,6 +587,10 @@ template(name="boardMenuPopup") | 📦 | {{_ 'archived-items'}} if currentUser.isBoardAdmin + li + a.js-open-migrations + | 🔧 + | {{_ 'migrations'}} li a.js-change-board-color | 🎨 diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index 18f271691..5831e601a 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -13,6 +13,7 @@ const viewTitles = { multiselection: 'multi-selection', customFields: 'custom-fields', archives: 'archives', + migrations: 'migrations', }; BlazeComponent.extendComponent({ @@ -271,6 +272,10 @@ Template.boardMenuPopup.events({ Sidebar.setView('archives'); Popup.back(); }, + 'click .js-open-migrations'() { + Sidebar.setView('migrations'); + Popup.back(); + }, 'click .js-change-board-color': Popup.open('boardChangeColor'), 'click .js-change-background-image': Popup.open('boardChangeBackgroundImage'), 'click .js-board-info-on-my-boards': Popup.open('boardInfoOnMyBoards'), diff --git a/client/components/sidebar/sidebarMigrations.jade b/client/components/sidebar/sidebarMigrations.jade new file mode 100644 index 000000000..5666b36c1 --- /dev/null +++ b/client/components/sidebar/sidebarMigrations.jade @@ -0,0 +1,69 @@ +template(name='migrationsSidebar') + if currentUser.isBoardAdmin + .sidebar-migrations + h3 + | 🔧 + | {{_ 'migrations'}} + p.quiet {{_ 'migrations-description'}} + + .migrations-list + h4 {{_ 'board-migrations'}} + .migration-item + a.js-run-migration(data-migration="comprehensive") + .migration-name + | {{_ 'comprehensive-board-migration'}} + .migration-status + if comprehensiveMigrationNeeded + span.badge.badge-warning {{_ 'migration-needed'}} + else + span.badge.badge-success {{_ 'migration-complete'}} + + .migration-item + a.js-run-migration(data-migration="fixMissingLists") + .migration-name + | {{_ 'fix-missing-lists-migration'}} + .migration-status + if fixMissingListsNeeded + span.badge.badge-warning {{_ 'migration-needed'}} + else + span.badge.badge-success {{_ 'migration-complete'}} + + hr + h4 {{_ 'global-migrations'}} + .migration-item + a.js-run-migration(data-migration="fixAvatarUrls") + .migration-name + | {{_ 'fix-avatar-urls-migration'}} + .migration-status + if fixAvatarUrlsNeeded + span.badge.badge-warning {{_ 'migration-needed'}} + else + span.badge.badge-success {{_ 'migration-complete'}} + + .migration-item + a.js-run-migration(data-migration="fixAllFileUrls") + .migration-name + | {{_ 'fix-all-file-urls-migration'}} + .migration-status + if fixAllFileUrlsNeeded + span.badge.badge-warning {{_ 'migration-needed'}} + else + span.badge.badge-success {{_ 'migration-complete'}} + else + p.quiet {{_ 'migrations-admin-only'}} + +template(name='runComprehensiveMigrationPopup') + p {{_ 'run-comprehensive-migration-confirm'}} + button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}} + +template(name='runFixMissingListsMigrationPopup') + p {{_ 'run-fix-missing-lists-migration-confirm'}} + button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}} + +template(name='runFixAvatarUrlsMigrationPopup') + p {{_ 'run-fix-avatar-urls-migration-confirm'}} + button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}} + +template(name='runFixAllFileUrlsMigrationPopup') + p {{_ 'run-fix-all-file-urls-migration-confirm'}} + button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}} diff --git a/client/components/sidebar/sidebarMigrations.js b/client/components/sidebar/sidebarMigrations.js new file mode 100644 index 000000000..c8f58a081 --- /dev/null +++ b/client/components/sidebar/sidebarMigrations.js @@ -0,0 +1,143 @@ +import { ReactiveCache } from '/imports/reactiveCache'; +import { TAPi18n } from '/imports/i18n'; + +BlazeComponent.extendComponent({ + onCreated() { + this.migrationStatuses = new ReactiveVar({}); + this.loadMigrationStatuses(); + }, + + loadMigrationStatuses() { + const boardId = Session.get('currentBoard'); + if (!boardId) return; + + // Check comprehensive migration + Meteor.call('comprehensiveBoardMigration.needsMigration', boardId, (err, res) => { + if (!err) { + const statuses = this.migrationStatuses.get(); + statuses.comprehensive = res; + this.migrationStatuses.set(statuses); + } + }); + + // Check fix missing lists migration + Meteor.call('fixMissingListsMigration.needsMigration', boardId, (err, res) => { + if (!err) { + const statuses = this.migrationStatuses.get(); + statuses.fixMissingLists = res; + this.migrationStatuses.set(statuses); + } + }); + + // Check fix avatar URLs migration (global) + Meteor.call('fixAvatarUrls.needsMigration', (err, res) => { + if (!err) { + const statuses = this.migrationStatuses.get(); + statuses.fixAvatarUrls = res; + this.migrationStatuses.set(statuses); + } + }); + + // Check fix all file URLs migration (global) + Meteor.call('fixAllFileUrls.needsMigration', (err, res) => { + if (!err) { + const statuses = this.migrationStatuses.get(); + statuses.fixAllFileUrls = res; + this.migrationStatuses.set(statuses); + } + }); + }, + + comprehensiveMigrationNeeded() { + return this.migrationStatuses.get().comprehensive === true; + }, + + fixMissingListsNeeded() { + return this.migrationStatuses.get().fixMissingLists === true; + }, + + fixAvatarUrlsNeeded() { + return this.migrationStatuses.get().fixAvatarUrls === true; + }, + + fixAllFileUrlsNeeded() { + return this.migrationStatuses.get().fixAllFileUrls === true; + }, + + runMigration(migrationType) { + const boardId = Session.get('currentBoard'); + + let methodName; + let methodArgs = []; + + switch (migrationType) { + case 'comprehensive': + methodName = 'comprehensiveBoardMigration.execute'; + methodArgs = [boardId]; + break; + + case 'fixMissingLists': + methodName = 'fixMissingListsMigration.execute'; + methodArgs = [boardId]; + break; + + case 'fixAvatarUrls': + methodName = 'fixAvatarUrls.execute'; + break; + + case 'fixAllFileUrls': + methodName = 'fixAllFileUrls.execute'; + break; + } + + if (methodName) { + Meteor.call(methodName, ...methodArgs, (err, result) => { + if (err) { + console.error('Migration failed:', err); + // Show error notification + Alert.error(TAPi18n.__('migration-failed') + ': ' + (err.message || err.reason)); + } else { + console.log('Migration completed:', result); + // Show success notification + Alert.success(TAPi18n.__('migration-successful')); + + // Reload migration statuses + Meteor.setTimeout(() => { + this.loadMigrationStatuses(); + }, 1000); + } + }); + } + }, + + events() { + return [ + { + 'click .js-run-migration[data-migration="comprehensive"]': Popup.afterConfirm('runComprehensiveMigration', function() { + const component = BlazeComponent.getComponentForElement(this); + if (component) { + component.runMigration('comprehensive'); + } + }), + 'click .js-run-migration[data-migration="fixMissingLists"]': Popup.afterConfirm('runFixMissingListsMigration', function() { + const component = BlazeComponent.getComponentForElement(this); + if (component) { + component.runMigration('fixMissingLists'); + } + }), + 'click .js-run-migration[data-migration="fixAvatarUrls"]': Popup.afterConfirm('runFixAvatarUrlsMigration', function() { + const component = BlazeComponent.getComponentForElement(this); + if (component) { + component.runMigration('fixAvatarUrls'); + } + }), + 'click .js-run-migration[data-migration="fixAllFileUrls"]': Popup.afterConfirm('runFixAllFileUrlsMigration', function() { + const component = BlazeComponent.getComponentForElement(this); + if (component) { + component.runMigration('fixAllFileUrls'); + } + }), + }, + ]; + }, +}).register('migrationsSidebar'); diff --git a/imports/i18n/data/en.i18n.json b/imports/i18n/data/en.i18n.json index eef27e1fe..e19bd8ef1 100644 --- a/imports/i18n/data/en.i18n.json +++ b/imports/i18n/data/en.i18n.json @@ -1404,7 +1404,31 @@ "back-to-settings": "Back to Settings", "board-id": "Board ID", "board-migration": "Board Migration", + "board-migrations": "Board Migrations", "card-show-lists-on-minicard": "Show Lists on Minicard", + "comprehensive-board-migration": "Comprehensive Board Migration", + "comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.", + "fix-missing-lists-migration": "Fix Missing Lists", + "fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.", + "fix-avatar-urls-migration": "Fix Avatar URLs", + "fix-avatar-urls-migration-description": "Updates avatar URLs to use the correct storage backend and fixes broken avatar references.", + "fix-all-file-urls-migration": "Fix All File URLs", + "fix-all-file-urls-migration-description": "Updates all file attachment URLs to use the correct storage backend and fixes broken file references.", + "global-migrations": "Global Migrations", + "migration-needed": "Migration Needed", + "migration-complete": "Complete", + "migration-running": "Running...", + "migration-successful": "Migration completed successfully", + "migration-failed": "Migration failed", + "migrations": "Migrations", + "migrations-admin-only": "Only board administrators can run migrations", + "migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.", + "no-issues-found": "No issues found", + "run-migration": "Run Migration", + "run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?", + "run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?", + "run-fix-avatar-urls-migration-confirm": "This will update avatar URLs across all boards to use the correct storage backend. This is a global operation. Continue?", + "run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs across all boards to use the correct storage backend. This is a global operation. Continue?", "cleanup": "Cleanup", "cleanup-old-jobs": "Cleanup Old Jobs", "completed": "Completed", diff --git a/server/migrations/fixAllFileUrls.js b/server/migrations/fixAllFileUrls.js index caba86e68..6b3be9ccf 100644 --- a/server/migrations/fixAllFileUrls.js +++ b/server/migrations/fixAllFileUrls.js @@ -30,15 +30,11 @@ class FixAllFileUrlsMigration { } } - // Check for problematic attachment URLs in cards - const cards = ReactiveCache.getCards({}); - for (const card of cards) { - if (card.attachments) { - for (const attachment of card.attachments) { - if (attachment.url && this.hasProblematicUrl(attachment.url)) { - return true; - } - } + // Check for problematic attachment URLs + const attachments = ReactiveCache.getAttachments({}); + for (const attachment of attachments) { + if (attachment.url && this.hasProblematicUrl(attachment.url)) { + return true; } } @@ -206,51 +202,40 @@ class FixAllFileUrlsMigration { } /** - * Fix attachment URLs in card references + * Fix attachment URLs in the Attachments collection */ async fixCardAttachmentUrls() { - const cards = ReactiveCache.getCards({}); - let cardsFixed = 0; + const attachments = ReactiveCache.getAttachments({}); + let attachmentsFixed = 0; - for (const card of cards) { - if (card.attachments) { - let needsUpdate = false; - const updatedAttachments = card.attachments.map(attachment => { - if (attachment.url && this.hasProblematicUrl(attachment.url)) { - try { - const fileId = attachment._id || extractFileIdFromUrl(attachment.url, 'attachment'); - const cleanUrl = fileId ? generateUniversalAttachmentUrl(fileId) : cleanFileUrl(attachment.url, 'attachment'); - - if (cleanUrl && cleanUrl !== attachment.url) { - needsUpdate = true; - return { ...attachment, url: cleanUrl }; + for (const attachment of attachments) { + if (attachment.url && this.hasProblematicUrl(attachment.url)) { + try { + const fileId = attachment._id || extractFileIdFromUrl(attachment.url, 'attachment'); + const cleanUrl = fileId ? generateUniversalAttachmentUrl(fileId) : cleanFileUrl(attachment.url, 'attachment'); + + if (cleanUrl && cleanUrl !== attachment.url) { + // Update attachment with fixed URL + Attachments.update(attachment._id, { + $set: { + url: cleanUrl, + modifiedAt: new Date() } - } catch (error) { - console.error(`Error fixing card attachment URL:`, error); + }); + + attachmentsFixed++; + + if (process.env.DEBUG === 'true') { + console.log(`Fixed attachment URL ${attachment._id}`); } } - return attachment; - }); - - if (needsUpdate) { - // Update card with fixed attachment URLs - Cards.update(card._id, { - $set: { - attachments: updatedAttachments, - modifiedAt: new Date() - } - }); - - cardsFixed++; - - if (process.env.DEBUG === 'true') { - console.log(`Fixed attachment URLs in card ${card._id}`); - } + } catch (error) { + console.error(`Error fixing attachment URL:`, error); } } } - return cardsFixed; + return attachmentsFixed; } }