/** * Restore All Archived Migration * * Restores all archived swimlanes, lists, and cards. * If any restored items are missing swimlaneId, listId, or cardId, * creates/assigns proper IDs to make them visible. */ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import { ReactiveCache } from '/imports/reactiveCache'; import { TAPi18n } from '/imports/i18n'; import Boards from '/models/boards'; import Lists from '/models/lists'; import Cards from '/models/cards'; import Swimlanes from '/models/swimlanes'; class RestoreAllArchivedMigration { constructor() { this.name = 'restoreAllArchived'; this.version = 1; } /** * Check if migration is needed for a board */ needsMigration(boardId) { try { const archivedSwimlanes = ReactiveCache.getSwimlanes({ boardId, archived: true }); const archivedLists = ReactiveCache.getLists({ boardId, archived: true }); const archivedCards = ReactiveCache.getCards({ boardId, archived: true }); return archivedSwimlanes.length > 0 || archivedLists.length > 0 || archivedCards.length > 0; } catch (error) { console.error('Error checking if restoreAllArchived migration is needed:', error); return false; } } /** * Execute the migration */ async executeMigration(boardId) { try { const results = { swimlanesRestored: 0, listsRestored: 0, cardsRestored: 0, itemsFixed: 0, errors: [] }; const board = ReactiveCache.getBoard(boardId); if (!board) { throw new Error('Board not found'); } // Get archived items const archivedSwimlanes = ReactiveCache.getSwimlanes({ boardId, archived: true }); const archivedLists = ReactiveCache.getLists({ boardId, archived: true }); const archivedCards = ReactiveCache.getCards({ boardId, archived: true }); // Get active items for reference const activeSwimlanes = ReactiveCache.getSwimlanes({ boardId, archived: false }); const activeLists = ReactiveCache.getLists({ boardId, archived: false }); // Restore all archived swimlanes for (const swimlane of archivedSwimlanes) { Swimlanes.update(swimlane._id, { $set: { archived: false, updatedAt: new Date() } }); results.swimlanesRestored++; if (process.env.DEBUG === 'true') { console.log(`Restored swimlane: ${swimlane.title}`); } } // Restore all archived lists and fix missing swimlaneId for (const list of archivedLists) { const updateFields = { archived: false, updatedAt: new Date() }; // Fix missing swimlaneId if (!list.swimlaneId) { // Try to find a suitable swimlane or use default let targetSwimlane = activeSwimlanes.find(s => !s.archived); if (!targetSwimlane) { // No active swimlane found, create default const swimlaneId = Swimlanes.insert({ title: TAPi18n.__('default'), boardId: boardId, sort: 0, createdAt: new Date(), updatedAt: new Date(), archived: false }); targetSwimlane = ReactiveCache.getSwimlane(swimlaneId); } updateFields.swimlaneId = targetSwimlane._id; results.itemsFixed++; if (process.env.DEBUG === 'true') { console.log(`Fixed missing swimlaneId for list: ${list.title}`); } } Lists.update(list._id, { $set: updateFields }); results.listsRestored++; if (process.env.DEBUG === 'true') { console.log(`Restored list: ${list.title}`); } } // Refresh lists after restoration const allLists = ReactiveCache.getLists({ boardId, archived: false }); const allSwimlanes = ReactiveCache.getSwimlanes({ boardId, archived: false }); // Restore all archived cards and fix missing IDs for (const card of archivedCards) { const updateFields = { archived: false, updatedAt: new Date() }; let needsFix = false; // Fix missing listId if (!card.listId) { // Find or create a default list let targetList = allLists.find(l => !l.archived); if (!targetList) { // No active list found, create one const defaultSwimlane = allSwimlanes.find(s => !s.archived) || allSwimlanes[0]; const listId = Lists.insert({ title: TAPi18n.__('default'), boardId: boardId, swimlaneId: defaultSwimlane._id, sort: 0, createdAt: new Date(), updatedAt: new Date(), archived: false }); targetList = ReactiveCache.getList(listId); } updateFields.listId = targetList._id; needsFix = true; } // Fix missing swimlaneId if (!card.swimlaneId) { // Try to get swimlaneId from the card's list if (card.listId || updateFields.listId) { const cardList = allLists.find(l => l._id === (updateFields.listId || card.listId)); if (cardList && cardList.swimlaneId) { updateFields.swimlaneId = cardList.swimlaneId; } else { // Fall back to first available swimlane const defaultSwimlane = allSwimlanes.find(s => !s.archived) || allSwimlanes[0]; updateFields.swimlaneId = defaultSwimlane._id; } } else { // Fall back to first available swimlane const defaultSwimlane = allSwimlanes.find(s => !s.archived) || allSwimlanes[0]; updateFields.swimlaneId = defaultSwimlane._id; } needsFix = true; } if (needsFix) { results.itemsFixed++; if (process.env.DEBUG === 'true') { console.log(`Fixed missing IDs for card: ${card.title}`); } } Cards.update(card._id, { $set: updateFields }); results.cardsRestored++; if (process.env.DEBUG === 'true') { console.log(`Restored card: ${card.title}`); } } return { success: true, changes: [ `Restored ${results.swimlanesRestored} archived swimlanes`, `Restored ${results.listsRestored} archived lists`, `Restored ${results.cardsRestored} archived cards`, `Fixed ${results.itemsFixed} items with missing IDs` ], results }; } catch (error) { console.error('Error executing restoreAllArchived migration:', error); return { success: false, error: error.message }; } } } const restoreAllArchivedMigration = new RestoreAllArchivedMigration(); // Register Meteor methods Meteor.methods({ 'restoreAllArchived.needsMigration'(boardId) { check(boardId, String); if (!this.userId) { throw new Meteor.Error('not-authorized', 'You must be logged in'); } return restoreAllArchivedMigration.needsMigration(boardId); }, 'restoreAllArchived.execute'(boardId) { check(boardId, String); if (!this.userId) { throw new Meteor.Error('not-authorized', 'You must be logged in'); } // Check if user is board admin const board = ReactiveCache.getBoard(boardId); if (!board) { throw new Meteor.Error('board-not-found', 'Board not found'); } const user = ReactiveCache.getUser(this.userId); if (!user) { throw new Meteor.Error('user-not-found', 'User not found'); } // Only board admins can run migrations const isBoardAdmin = board.members && board.members.some( member => member.userId === this.userId && member.isAdmin ); if (!isBoardAdmin && !user.isAdmin) { throw new Meteor.Error('not-authorized', 'Only board administrators can run migrations'); } return restoreAllArchivedMigration.executeMigration(boardId); } }); export default restoreAllArchivedMigration;