sidebarMigrations.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. import { TAPi18n } from '/imports/i18n';
  3. import { migrationProgressManager } from '/client/components/migrationProgress';
  4. BlazeComponent.extendComponent({
  5. onCreated() {
  6. this.migrationStatuses = new ReactiveVar({});
  7. this.loadMigrationStatuses();
  8. },
  9. loadMigrationStatuses() {
  10. const boardId = Session.get('currentBoard');
  11. if (!boardId) return;
  12. // Check comprehensive migration
  13. Meteor.call('comprehensiveBoardMigration.needsMigration', boardId, (err, res) => {
  14. if (!err) {
  15. const statuses = this.migrationStatuses.get();
  16. statuses.comprehensive = res;
  17. this.migrationStatuses.set(statuses);
  18. }
  19. });
  20. // Check fix missing lists migration
  21. Meteor.call('fixMissingListsMigration.needsMigration', boardId, (err, res) => {
  22. if (!err) {
  23. const statuses = this.migrationStatuses.get();
  24. statuses.fixMissingLists = res;
  25. this.migrationStatuses.set(statuses);
  26. }
  27. });
  28. // Check delete duplicate empty lists migration
  29. Meteor.call('deleteDuplicateEmptyLists.needsMigration', boardId, (err, res) => {
  30. if (!err) {
  31. const statuses = this.migrationStatuses.get();
  32. statuses.deleteDuplicateEmptyLists = res;
  33. this.migrationStatuses.set(statuses);
  34. }
  35. });
  36. // Check restore lost cards migration
  37. Meteor.call('restoreLostCards.needsMigration', boardId, (err, res) => {
  38. if (!err) {
  39. const statuses = this.migrationStatuses.get();
  40. statuses.restoreLostCards = res;
  41. this.migrationStatuses.set(statuses);
  42. }
  43. });
  44. // Check restore all archived migration
  45. Meteor.call('restoreAllArchived.needsMigration', boardId, (err, res) => {
  46. if (!err) {
  47. const statuses = this.migrationStatuses.get();
  48. statuses.restoreAllArchived = res;
  49. this.migrationStatuses.set(statuses);
  50. }
  51. });
  52. // Check fix avatar URLs migration (board-specific)
  53. Meteor.call('fixAvatarUrls.needsMigration', boardId, (err, res) => {
  54. if (!err) {
  55. const statuses = this.migrationStatuses.get();
  56. statuses.fixAvatarUrls = res;
  57. this.migrationStatuses.set(statuses);
  58. }
  59. });
  60. // Check fix all file URLs migration (board-specific)
  61. Meteor.call('fixAllFileUrls.needsMigration', boardId, (err, res) => {
  62. if (!err) {
  63. const statuses = this.migrationStatuses.get();
  64. statuses.fixAllFileUrls = res;
  65. this.migrationStatuses.set(statuses);
  66. }
  67. });
  68. },
  69. comprehensiveMigrationNeeded() {
  70. return this.migrationStatuses.get().comprehensive === true;
  71. },
  72. fixMissingListsNeeded() {
  73. return this.migrationStatuses.get().fixMissingLists === true;
  74. },
  75. deleteDuplicateEmptyListsNeeded() {
  76. return this.migrationStatuses.get().deleteDuplicateEmptyLists === true;
  77. },
  78. restoreLostCardsNeeded() {
  79. return this.migrationStatuses.get().restoreLostCards === true;
  80. },
  81. restoreAllArchivedNeeded() {
  82. return this.migrationStatuses.get().restoreAllArchived === true;
  83. },
  84. fixAvatarUrlsNeeded() {
  85. return this.migrationStatuses.get().fixAvatarUrls === true;
  86. },
  87. fixAllFileUrlsNeeded() {
  88. return this.migrationStatuses.get().fixAllFileUrls === true;
  89. },
  90. // Simulate migration progress updates using the global progress popup
  91. async simulateMigrationProgress(progressSteps) {
  92. const totalSteps = progressSteps.length;
  93. for (let i = 0; i < progressSteps.length; i++) {
  94. const step = progressSteps[i];
  95. const overall = Math.round(((i + 1) / totalSteps) * 100);
  96. // Start step
  97. migrationProgressManager.updateProgress({
  98. overallProgress: overall,
  99. currentStep: i + 1,
  100. totalSteps,
  101. stepName: step.step,
  102. stepProgress: 0,
  103. stepStatus: `Starting ${step.name}...`,
  104. stepDetails: null,
  105. boardId: Session.get('currentBoard'),
  106. });
  107. const stepDuration = step.duration;
  108. const updateInterval = 100;
  109. const totalUpdates = Math.max(1, Math.floor(stepDuration / updateInterval));
  110. for (let j = 0; j < totalUpdates; j++) {
  111. const per = Math.round(((j + 1) / totalUpdates) * 100);
  112. migrationProgressManager.updateProgress({
  113. overallProgress: overall,
  114. currentStep: i + 1,
  115. totalSteps,
  116. stepName: step.step,
  117. stepProgress: per,
  118. stepStatus: `Processing ${step.name}...`,
  119. stepDetails: { progress: `${per}%` },
  120. boardId: Session.get('currentBoard'),
  121. });
  122. // eslint-disable-next-line no-await-in-loop
  123. await new Promise((r) => setTimeout(r, updateInterval));
  124. }
  125. // Complete step
  126. migrationProgressManager.updateProgress({
  127. overallProgress: overall,
  128. currentStep: i + 1,
  129. totalSteps,
  130. stepName: step.step,
  131. stepProgress: 100,
  132. stepStatus: `${step.name} completed`,
  133. stepDetails: { status: 'completed' },
  134. boardId: Session.get('currentBoard'),
  135. });
  136. }
  137. },
  138. runMigration(migrationType) {
  139. const boardId = Session.get('currentBoard');
  140. let methodName;
  141. let methodArgs = [];
  142. switch (migrationType) {
  143. case 'comprehensive':
  144. methodName = 'comprehensiveBoardMigration.execute';
  145. methodArgs = [boardId];
  146. break;
  147. case 'fixMissingLists':
  148. methodName = 'fixMissingListsMigration.execute';
  149. methodArgs = [boardId];
  150. break;
  151. case 'deleteDuplicateEmptyLists':
  152. methodName = 'deleteDuplicateEmptyLists.execute';
  153. methodArgs = [boardId];
  154. break;
  155. case 'restoreLostCards':
  156. methodName = 'restoreLostCards.execute';
  157. methodArgs = [boardId];
  158. break;
  159. case 'restoreAllArchived':
  160. methodName = 'restoreAllArchived.execute';
  161. methodArgs = [boardId];
  162. break;
  163. case 'fixAvatarUrls':
  164. methodName = 'fixAvatarUrls.execute';
  165. methodArgs = [boardId];
  166. break;
  167. case 'fixAllFileUrls':
  168. methodName = 'fixAllFileUrls.execute';
  169. methodArgs = [boardId];
  170. break;
  171. }
  172. if (methodName) {
  173. // Define simulated steps per migration type
  174. const stepsByType = {
  175. comprehensive: [
  176. { step: 'analyze_board_structure', name: 'Analyze Board Structure', duration: 800 },
  177. { step: 'fix_orphaned_cards', name: 'Fix Orphaned Cards', duration: 1200 },
  178. { step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 1000 },
  179. { step: 'ensure_per_swimlane_lists', name: 'Ensure Per-Swimlane Lists', duration: 800 },
  180. { step: 'validate_migration', name: 'Validate Migration', duration: 800 },
  181. { step: 'fix_avatar_urls', name: 'Fix Avatar URLs', duration: 600 },
  182. { step: 'fix_attachment_urls', name: 'Fix Attachment URLs', duration: 600 },
  183. ],
  184. fixMissingLists: [
  185. { step: 'analyze_lists', name: 'Analyze Lists', duration: 600 },
  186. { step: 'create_missing_lists', name: 'Create Missing Lists', duration: 900 },
  187. { step: 'update_cards', name: 'Update Cards', duration: 900 },
  188. { step: 'finalize', name: 'Finalize', duration: 400 },
  189. ],
  190. deleteDuplicateEmptyLists: [
  191. { step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 700 },
  192. { step: 'delete_duplicate_empty_lists', name: 'Delete Duplicate Empty Lists', duration: 800 },
  193. ],
  194. restoreLostCards: [
  195. { step: 'ensure_lost_cards_swimlane', name: 'Ensure Lost Cards Swimlane', duration: 600 },
  196. { step: 'restore_lists', name: 'Restore Lists', duration: 800 },
  197. { step: 'restore_cards', name: 'Restore Cards', duration: 1000 },
  198. ],
  199. restoreAllArchived: [
  200. { step: 'restore_swimlanes', name: 'Restore Swimlanes', duration: 800 },
  201. { step: 'restore_lists', name: 'Restore Lists', duration: 900 },
  202. { step: 'restore_cards', name: 'Restore Cards', duration: 1000 },
  203. { step: 'fix_missing_ids', name: 'Fix Missing IDs', duration: 600 },
  204. ],
  205. fixAvatarUrls: [
  206. { step: 'scan_users', name: 'Checking board member avatars', duration: 500 },
  207. { step: 'fix_urls', name: 'Fixing avatar URLs', duration: 900 },
  208. ],
  209. fixAllFileUrls: [
  210. { step: 'scan_files', name: 'Checking board file attachments', duration: 600 },
  211. { step: 'fix_urls', name: 'Fixing file URLs', duration: 1000 },
  212. ],
  213. };
  214. const steps = stepsByType[migrationType] || [
  215. { step: 'running', name: 'Running Migration', duration: 1000 },
  216. ];
  217. // Kick off popup and simulated progress
  218. migrationProgressManager.startMigration();
  219. const progressPromise = this.simulateMigrationProgress(steps);
  220. // Start migration call
  221. const callPromise = new Promise((resolve, reject) => {
  222. Meteor.call(methodName, ...methodArgs, (err, result) => {
  223. if (err) return reject(err);
  224. return resolve(result);
  225. });
  226. });
  227. Promise.allSettled([callPromise, progressPromise]).then(([callRes]) => {
  228. if (callRes.status === 'rejected') {
  229. migrationProgressManager.failMigration(callRes.reason);
  230. } else {
  231. const result = callRes.value;
  232. // Summarize result details in the popup
  233. let summary = {};
  234. if (result && result.results) {
  235. // Comprehensive returns {success, results}
  236. const r = result.results;
  237. summary = {
  238. totalCardsProcessed: r.totalCardsProcessed,
  239. totalListsProcessed: r.totalListsProcessed,
  240. totalListsCreated: r.totalListsCreated,
  241. };
  242. } else if (result && result.changes) {
  243. // Many migrations return a changes string array
  244. summary = { changes: result.changes.join(' | ') };
  245. } else if (result && typeof result === 'object') {
  246. summary = result;
  247. }
  248. migrationProgressManager.updateProgress({
  249. overallProgress: 100,
  250. currentStep: steps.length,
  251. totalSteps: steps.length,
  252. stepName: 'completed',
  253. stepProgress: 100,
  254. stepStatus: 'Migration completed',
  255. stepDetails: summary,
  256. boardId: Session.get('currentBoard'),
  257. });
  258. migrationProgressManager.completeMigration();
  259. // Refresh status badges slightly after
  260. Meteor.setTimeout(() => {
  261. this.loadMigrationStatuses();
  262. }, 1000);
  263. }
  264. });
  265. }
  266. },
  267. events() {
  268. const self = this; // Capture component reference
  269. return [
  270. {
  271. 'click .js-run-migration[data-migration="comprehensive"]': Popup.afterConfirm('runComprehensiveMigration', function() {
  272. self.runMigration('comprehensive');
  273. Popup.back();
  274. }),
  275. 'click .js-run-migration[data-migration="fixMissingLists"]': Popup.afterConfirm('runFixMissingListsMigration', function() {
  276. self.runMigration('fixMissingLists');
  277. Popup.back();
  278. }),
  279. 'click .js-run-migration[data-migration="deleteDuplicateEmptyLists"]': Popup.afterConfirm('runDeleteDuplicateEmptyListsMigration', function() {
  280. self.runMigration('deleteDuplicateEmptyLists');
  281. Popup.back();
  282. }),
  283. 'click .js-run-migration[data-migration="restoreLostCards"]': Popup.afterConfirm('runRestoreLostCardsMigration', function() {
  284. self.runMigration('restoreLostCards');
  285. Popup.back();
  286. }),
  287. 'click .js-run-migration[data-migration="restoreAllArchived"]': Popup.afterConfirm('runRestoreAllArchivedMigration', function() {
  288. self.runMigration('restoreAllArchived');
  289. Popup.back();
  290. }),
  291. 'click .js-run-migration[data-migration="fixAvatarUrls"]': Popup.afterConfirm('runFixAvatarUrlsMigration', function() {
  292. self.runMigration('fixAvatarUrls');
  293. Popup.back();
  294. }),
  295. 'click .js-run-migration[data-migration="fixAllFileUrls"]': Popup.afterConfirm('runFixAllFileUrlsMigration', function() {
  296. self.runMigration('fixAllFileUrls');
  297. Popup.back();
  298. }),
  299. },
  300. ];
  301. },
  302. }).register('migrationsSidebar');