| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 | /** * 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';import Boards from '/models/boards';import Lists from '/models/lists';import Cards from '/models/cards';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) {          if (process.env.DEBUG === 'true') {            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 {      if (process.env.DEBUG === 'true') {        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 = Lists.insert(newListData);            targetList = { _id: newListId, ...newListData };            createdLists++;                        if (process.env.DEBUG === 'true') {              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) {            Cards.update(card._id, {              $set: {                listId: targetList._id,                modifiedAt: new Date()              }            });            updatedCards++;          }        }      }      // Mark board as processed      Boards.update(boardId, {        $set: {          fixMissingListsCompleted: true,          fixMissingListsCompletedAt: new Date()        }      });      if (process.env.DEBUG === 'true') {        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 instanceexport const fixMissingListsMigration = new FixMissingListsMigration();// Meteor methodsMeteor.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);  }});
 |