restoreLostCards.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. /**
  2. * Restore Lost Cards Migration
  3. *
  4. * Finds and restores cards and lists that have missing swimlaneId, listId, or are orphaned.
  5. * Creates a "Lost Cards" swimlane and restores visibility of lost items.
  6. * Only processes non-archived items.
  7. */
  8. import { Meteor } from 'meteor/meteor';
  9. import { check } from 'meteor/check';
  10. import { ReactiveCache } from '/imports/reactiveCache';
  11. import { TAPi18n } from '/imports/i18n';
  12. import Boards from '/models/boards';
  13. import Lists from '/models/lists';
  14. import Cards from '/models/cards';
  15. import Swimlanes from '/models/swimlanes';
  16. class RestoreLostCardsMigration {
  17. constructor() {
  18. this.name = 'restoreLostCards';
  19. this.version = 1;
  20. }
  21. /**
  22. * Check if migration is needed for a board
  23. */
  24. needsMigration(boardId) {
  25. try {
  26. const cards = ReactiveCache.getCards({ boardId, archived: false });
  27. const lists = ReactiveCache.getLists({ boardId, archived: false });
  28. // Check for cards missing swimlaneId or listId
  29. const lostCards = cards.filter(card => !card.swimlaneId || !card.listId);
  30. if (lostCards.length > 0) {
  31. return true;
  32. }
  33. // Check for lists missing swimlaneId
  34. const lostLists = lists.filter(list => !list.swimlaneId);
  35. if (lostLists.length > 0) {
  36. return true;
  37. }
  38. // Check for orphaned cards (cards whose list doesn't exist)
  39. for (const card of cards) {
  40. if (card.listId) {
  41. const listExists = lists.some(list => list._id === card.listId);
  42. if (!listExists) {
  43. return true;
  44. }
  45. }
  46. }
  47. return false;
  48. } catch (error) {
  49. console.error('Error checking if restoreLostCards migration is needed:', error);
  50. return false;
  51. }
  52. }
  53. /**
  54. * Execute the migration
  55. */
  56. async executeMigration(boardId) {
  57. try {
  58. const results = {
  59. lostCardsSwimlaneCreated: false,
  60. cardsRestored: 0,
  61. listsRestored: 0,
  62. errors: []
  63. };
  64. const board = ReactiveCache.getBoard(boardId);
  65. if (!board) {
  66. throw new Error('Board not found');
  67. }
  68. // Get all non-archived items
  69. const cards = ReactiveCache.getCards({ boardId, archived: false });
  70. const lists = ReactiveCache.getLists({ boardId, archived: false });
  71. const swimlanes = ReactiveCache.getSwimlanes({ boardId, archived: false });
  72. // Detect items to restore BEFORE creating anything
  73. const lostLists = lists.filter(list => !list.swimlaneId);
  74. const lostCards = cards.filter(card => !card.swimlaneId || !card.listId);
  75. const orphanedCards = cards.filter(card => card.listId && !lists.some(list => list._id === card.listId));
  76. const hasCardsWork = lostCards.length > 0 || orphanedCards.length > 0;
  77. const hasListsWork = lostLists.length > 0;
  78. const hasAnyWork = hasCardsWork || hasListsWork;
  79. if (!hasAnyWork) {
  80. // Nothing to restore; do not create swimlane or list
  81. return {
  82. success: true,
  83. changes: [
  84. 'No lost swimlanes, lists, or cards to restore'
  85. ],
  86. results: {
  87. lostCardsSwimlaneCreated: false,
  88. cardsRestored: 0,
  89. listsRestored: 0
  90. }
  91. };
  92. }
  93. // Find or create "Lost Cards" swimlane (only if there is actual work)
  94. let lostCardsSwimlane = swimlanes.find(s => s.title === TAPi18n.__('lost-cards'));
  95. if (!lostCardsSwimlane) {
  96. const swimlaneId = Swimlanes.insert({
  97. title: TAPi18n.__('lost-cards'),
  98. boardId: boardId,
  99. sort: 999999, // Put at the end
  100. color: 'red',
  101. createdAt: new Date(),
  102. updatedAt: new Date(),
  103. archived: false
  104. });
  105. lostCardsSwimlane = ReactiveCache.getSwimlane(swimlaneId);
  106. results.lostCardsSwimlaneCreated = true;
  107. if (process.env.DEBUG === 'true') {
  108. console.log(`Created "Lost Cards" swimlane for board ${boardId}`);
  109. }
  110. }
  111. // Restore lost lists (lists without swimlaneId)
  112. if (hasListsWork) {
  113. for (const list of lostLists) {
  114. Lists.update(list._id, {
  115. $set: {
  116. swimlaneId: lostCardsSwimlane._id,
  117. updatedAt: new Date()
  118. }
  119. });
  120. results.listsRestored++;
  121. if (process.env.DEBUG === 'true') {
  122. console.log(`Restored lost list: ${list.title}`);
  123. }
  124. }
  125. }
  126. // Create default list only if we need to move cards
  127. let defaultList = null;
  128. if (hasCardsWork) {
  129. defaultList = lists.find(l =>
  130. l.swimlaneId === lostCardsSwimlane._id &&
  131. l.title === TAPi18n.__('lost-cards-list')
  132. );
  133. if (!defaultList) {
  134. const listId = Lists.insert({
  135. title: TAPi18n.__('lost-cards-list'),
  136. boardId: boardId,
  137. swimlaneId: lostCardsSwimlane._id,
  138. sort: 0,
  139. createdAt: new Date(),
  140. updatedAt: new Date(),
  141. archived: false
  142. });
  143. defaultList = ReactiveCache.getList(listId);
  144. if (process.env.DEBUG === 'true') {
  145. console.log(`Created default list in Lost Cards swimlane`);
  146. }
  147. }
  148. }
  149. // Restore cards missing swimlaneId or listId
  150. if (hasCardsWork) {
  151. for (const card of lostCards) {
  152. const updateFields = { updatedAt: new Date() };
  153. if (!card.swimlaneId) updateFields.swimlaneId = lostCardsSwimlane._id;
  154. if (!card.listId) updateFields.listId = defaultList._id;
  155. Cards.update(card._id, { $set: updateFields });
  156. results.cardsRestored++;
  157. if (process.env.DEBUG === 'true') {
  158. console.log(`Restored lost card: ${card.title}`);
  159. }
  160. }
  161. // Restore orphaned cards (cards whose list doesn't exist)
  162. for (const card of orphanedCards) {
  163. Cards.update(card._id, {
  164. $set: {
  165. listId: defaultList._id,
  166. swimlaneId: lostCardsSwimlane._id,
  167. updatedAt: new Date()
  168. }
  169. });
  170. results.cardsRestored++;
  171. if (process.env.DEBUG === 'true') {
  172. console.log(`Restored orphaned card: ${card.title}`);
  173. }
  174. }
  175. }
  176. return {
  177. success: true,
  178. changes: [
  179. results.lostCardsSwimlaneCreated ? 'Created "Lost Cards" swimlane' : 'Using existing "Lost Cards" swimlane',
  180. `Restored ${results.listsRestored} lost lists`,
  181. `Restored ${results.cardsRestored} lost cards`
  182. ],
  183. results
  184. };
  185. } catch (error) {
  186. console.error('Error executing restoreLostCards migration:', error);
  187. return {
  188. success: false,
  189. error: error.message
  190. };
  191. }
  192. }
  193. }
  194. const restoreLostCardsMigration = new RestoreLostCardsMigration();
  195. // Register Meteor methods
  196. Meteor.methods({
  197. 'restoreLostCards.needsMigration'(boardId) {
  198. check(boardId, String);
  199. if (!this.userId) {
  200. throw new Meteor.Error('not-authorized', 'You must be logged in');
  201. }
  202. return restoreLostCardsMigration.needsMigration(boardId);
  203. },
  204. 'restoreLostCards.execute'(boardId) {
  205. check(boardId, String);
  206. if (!this.userId) {
  207. throw new Meteor.Error('not-authorized', 'You must be logged in');
  208. }
  209. // Check if user is board admin
  210. const board = ReactiveCache.getBoard(boardId);
  211. if (!board) {
  212. throw new Meteor.Error('board-not-found', 'Board not found');
  213. }
  214. const user = ReactiveCache.getUser(this.userId);
  215. if (!user) {
  216. throw new Meteor.Error('user-not-found', 'User not found');
  217. }
  218. // Only board admins can run migrations
  219. const isBoardAdmin = board.members && board.members.some(
  220. member => member.userId === this.userId && member.isAdmin
  221. );
  222. if (!isBoardAdmin && !user.isAdmin) {
  223. throw new Meteor.Error('not-authorized', 'Only board administrators can run migrations');
  224. }
  225. return restoreLostCardsMigration.executeMigration(boardId);
  226. }
  227. });
  228. export default restoreLostCardsMigration;