123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- /**
- * Fix Missing Lists Migration
- *
- * This migration fixes the issue where cards have incorrect listId references
- * due to the per-swimlane lists change. It detects cards with mismatched
- * listId/swimlaneId and creates the missing lists.
- *
- * Issue: When upgrading from v7.94 to v8.02, cards that were in different
- * swimlanes but shared the same list now have wrong listId references.
- *
- * Example:
- * - Card1: listId: 'HB93dWNnY5bgYdtxc', swimlaneId: 'sK69SseWkh3tMbJvg'
- * - Card2: listId: 'HB93dWNnY5bgYdtxc', swimlaneId: 'XeecF9nZxGph4zcT4'
- *
- * Card2 should have a different listId that corresponds to its swimlane.
- */
- import { Meteor } from 'meteor/meteor';
- import { check } from 'meteor/check';
- import { ReactiveCache } from '/imports/reactiveCache';
- class FixMissingListsMigration {
- constructor() {
- this.name = 'fix-missing-lists';
- this.version = 1;
- }
- /**
- * Check if migration is needed for a board
- */
- needsMigration(boardId) {
- try {
- const board = ReactiveCache.getBoard(boardId);
- if (!board) return false;
- // Check if board has already been processed
- if (board.fixMissingListsCompleted) {
- return false;
- }
- // Check if there are cards with mismatched listId/swimlaneId
- const cards = ReactiveCache.getCards({ boardId });
- const lists = ReactiveCache.getLists({ boardId });
-
- // Create a map of listId -> swimlaneId for existing lists
- const listSwimlaneMap = new Map();
- lists.forEach(list => {
- listSwimlaneMap.set(list._id, list.swimlaneId || '');
- });
- // Check for cards with mismatched listId/swimlaneId
- for (const card of cards) {
- const expectedSwimlaneId = listSwimlaneMap.get(card.listId);
- if (expectedSwimlaneId && expectedSwimlaneId !== card.swimlaneId) {
- console.log(`Found mismatched card: ${card._id}, listId: ${card.listId}, card swimlaneId: ${card.swimlaneId}, list swimlaneId: ${expectedSwimlaneId}`);
- return true;
- }
- }
- return false;
- } catch (error) {
- console.error('Error checking if migration is needed:', error);
- return false;
- }
- }
- /**
- * Execute the migration for a board
- */
- async executeMigration(boardId) {
- try {
- console.log(`Starting fix missing lists migration for board ${boardId}`);
-
- const board = ReactiveCache.getBoard(boardId);
- if (!board) {
- throw new Error(`Board ${boardId} not found`);
- }
- const cards = ReactiveCache.getCards({ boardId });
- const lists = ReactiveCache.getLists({ boardId });
- const swimlanes = ReactiveCache.getSwimlanes({ boardId });
- // Create maps for efficient lookup
- const listSwimlaneMap = new Map();
- const swimlaneListsMap = new Map();
-
- lists.forEach(list => {
- listSwimlaneMap.set(list._id, list.swimlaneId || '');
- if (!swimlaneListsMap.has(list.swimlaneId || '')) {
- swimlaneListsMap.set(list.swimlaneId || '', []);
- }
- swimlaneListsMap.get(list.swimlaneId || '').push(list);
- });
- // Group cards by swimlaneId
- const cardsBySwimlane = new Map();
- cards.forEach(card => {
- if (!cardsBySwimlane.has(card.swimlaneId)) {
- cardsBySwimlane.set(card.swimlaneId, []);
- }
- cardsBySwimlane.get(card.swimlaneId).push(card);
- });
- let createdLists = 0;
- let updatedCards = 0;
- // Process each swimlane
- for (const [swimlaneId, swimlaneCards] of cardsBySwimlane) {
- if (!swimlaneId) continue;
- // Get existing lists for this swimlane
- const existingLists = swimlaneListsMap.get(swimlaneId) || [];
- const existingListTitles = new Set(existingLists.map(list => list.title));
- // Group cards by their current listId
- const cardsByListId = new Map();
- swimlaneCards.forEach(card => {
- if (!cardsByListId.has(card.listId)) {
- cardsByListId.set(card.listId, []);
- }
- cardsByListId.get(card.listId).push(card);
- });
- // For each listId used by cards in this swimlane
- for (const [listId, cardsInList] of cardsByListId) {
- const originalList = lists.find(l => l._id === listId);
- if (!originalList) continue;
- // Check if this list's swimlaneId matches the card's swimlaneId
- const listSwimlaneId = listSwimlaneMap.get(listId);
- if (listSwimlaneId === swimlaneId) {
- // List is already correctly assigned to this swimlane
- continue;
- }
- // Check if we already have a list with the same title in this swimlane
- let targetList = existingLists.find(list => list.title === originalList.title);
-
- if (!targetList) {
- // Create a new list for this swimlane
- const newListData = {
- title: originalList.title,
- boardId: boardId,
- swimlaneId: swimlaneId,
- sort: originalList.sort || 0,
- archived: originalList.archived || false,
- createdAt: new Date(),
- modifiedAt: new Date(),
- type: originalList.type || 'list'
- };
- // Copy other properties if they exist
- if (originalList.color) newListData.color = originalList.color;
- if (originalList.wipLimit) newListData.wipLimit = originalList.wipLimit;
- if (originalList.wipLimitEnabled) newListData.wipLimitEnabled = originalList.wipLimitEnabled;
- if (originalList.wipLimitSoft) newListData.wipLimitSoft = originalList.wipLimitSoft;
- if (originalList.starred) newListData.starred = originalList.starred;
- if (originalList.collapsed) newListData.collapsed = originalList.collapsed;
- // Insert the new list
- const newListId = ReactiveCache.getCollection('lists').insert(newListData);
- targetList = { _id: newListId, ...newListData };
- createdLists++;
-
- console.log(`Created new list "${originalList.title}" for swimlane ${swimlaneId}`);
- }
- // Update all cards in this group to use the correct listId
- for (const card of cardsInList) {
- ReactiveCache.getCollection('cards').update(card._id, {
- $set: {
- listId: targetList._id,
- modifiedAt: new Date()
- }
- });
- updatedCards++;
- }
- }
- }
- // Mark board as processed
- ReactiveCache.getCollection('boards').update(boardId, {
- $set: {
- fixMissingListsCompleted: true,
- fixMissingListsCompletedAt: new Date()
- }
- });
- console.log(`Fix missing lists migration completed for board ${boardId}: created ${createdLists} lists, updated ${updatedCards} cards`);
-
- return {
- success: true,
- createdLists,
- updatedCards
- };
- } catch (error) {
- console.error(`Error executing fix missing lists migration for board ${boardId}:`, error);
- throw error;
- }
- }
- /**
- * Get migration status for a board
- */
- getMigrationStatus(boardId) {
- try {
- const board = ReactiveCache.getBoard(boardId);
- if (!board) {
- return { status: 'board_not_found' };
- }
- if (board.fixMissingListsCompleted) {
- return {
- status: 'completed',
- completedAt: board.fixMissingListsCompletedAt
- };
- }
- const needsMigration = this.needsMigration(boardId);
- return {
- status: needsMigration ? 'needed' : 'not_needed'
- };
- } catch (error) {
- console.error('Error getting migration status:', error);
- return { status: 'error', error: error.message };
- }
- }
- }
- // Export singleton instance
- export const fixMissingListsMigration = new FixMissingListsMigration();
- // Meteor methods
- Meteor.methods({
- 'fixMissingListsMigration.check'(boardId) {
- check(boardId, String);
-
- if (!this.userId) {
- throw new Meteor.Error('not-authorized');
- }
-
- return fixMissingListsMigration.getMigrationStatus(boardId);
- },
- 'fixMissingListsMigration.execute'(boardId) {
- check(boardId, String);
-
- if (!this.userId) {
- throw new Meteor.Error('not-authorized');
- }
-
- return fixMissingListsMigration.executeMigration(boardId);
- },
- 'fixMissingListsMigration.needsMigration'(boardId) {
- check(boardId, String);
-
- if (!this.userId) {
- throw new Meteor.Error('not-authorized');
- }
-
- return fixMissingListsMigration.needsMigration(boardId);
- }
- });
|