sidebarMigrations.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  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 (global)
  53. Meteor.call('fixAvatarUrls.needsMigration', (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 (global)
  61. Meteor.call('fixAllFileUrls.needsMigration', (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. break;
  166. case 'fixAllFileUrls':
  167. methodName = 'fixAllFileUrls.execute';
  168. break;
  169. }
  170. if (methodName) {
  171. // Define simulated steps per migration type
  172. const stepsByType = {
  173. comprehensive: [
  174. { step: 'analyze_board_structure', name: 'Analyze Board Structure', duration: 800 },
  175. { step: 'fix_orphaned_cards', name: 'Fix Orphaned Cards', duration: 1200 },
  176. { step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 1000 },
  177. { step: 'ensure_per_swimlane_lists', name: 'Ensure Per-Swimlane Lists', duration: 800 },
  178. { step: 'validate_migration', name: 'Validate Migration', duration: 800 },
  179. { step: 'fix_avatar_urls', name: 'Fix Avatar URLs', duration: 600 },
  180. { step: 'fix_attachment_urls', name: 'Fix Attachment URLs', duration: 600 },
  181. ],
  182. fixMissingLists: [
  183. { step: 'analyze_lists', name: 'Analyze Lists', duration: 600 },
  184. { step: 'create_missing_lists', name: 'Create Missing Lists', duration: 900 },
  185. { step: 'update_cards', name: 'Update Cards', duration: 900 },
  186. { step: 'finalize', name: 'Finalize', duration: 400 },
  187. ],
  188. deleteDuplicateEmptyLists: [
  189. { step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 700 },
  190. { step: 'delete_duplicate_empty_lists', name: 'Delete Duplicate Empty Lists', duration: 800 },
  191. ],
  192. restoreLostCards: [
  193. { step: 'ensure_lost_cards_swimlane', name: 'Ensure Lost Cards Swimlane', duration: 600 },
  194. { step: 'restore_lists', name: 'Restore Lists', duration: 800 },
  195. { step: 'restore_cards', name: 'Restore Cards', duration: 1000 },
  196. ],
  197. restoreAllArchived: [
  198. { step: 'restore_swimlanes', name: 'Restore Swimlanes', duration: 800 },
  199. { step: 'restore_lists', name: 'Restore Lists', duration: 900 },
  200. { step: 'restore_cards', name: 'Restore Cards', duration: 1000 },
  201. { step: 'fix_missing_ids', name: 'Fix Missing IDs', duration: 600 },
  202. ],
  203. fixAvatarUrls: [
  204. { step: 'scan_users', name: 'Scan Users', duration: 500 },
  205. { step: 'fix_urls', name: 'Fix Avatar URLs', duration: 900 },
  206. ],
  207. fixAllFileUrls: [
  208. { step: 'scan_files', name: 'Scan Files', duration: 600 },
  209. { step: 'fix_urls', name: 'Fix File URLs', duration: 1000 },
  210. ],
  211. };
  212. const steps = stepsByType[migrationType] || [
  213. { step: 'running', name: 'Running Migration', duration: 1000 },
  214. ];
  215. // Kick off popup and simulated progress
  216. migrationProgressManager.startMigration();
  217. const progressPromise = this.simulateMigrationProgress(steps);
  218. // Start migration call
  219. const callPromise = new Promise((resolve, reject) => {
  220. Meteor.call(methodName, ...methodArgs, (err, result) => {
  221. if (err) return reject(err);
  222. return resolve(result);
  223. });
  224. });
  225. Promise.allSettled([callPromise, progressPromise]).then(([callRes]) => {
  226. if (callRes.status === 'rejected') {
  227. migrationProgressManager.failMigration(callRes.reason);
  228. } else {
  229. const result = callRes.value;
  230. // Summarize result details in the popup
  231. let summary = {};
  232. if (result && result.results) {
  233. // Comprehensive returns {success, results}
  234. const r = result.results;
  235. summary = {
  236. totalCardsProcessed: r.totalCardsProcessed,
  237. totalListsProcessed: r.totalListsProcessed,
  238. totalListsCreated: r.totalListsCreated,
  239. };
  240. } else if (result && result.changes) {
  241. // Many migrations return a changes string array
  242. summary = { changes: result.changes.join(' | ') };
  243. } else if (result && typeof result === 'object') {
  244. summary = result;
  245. }
  246. migrationProgressManager.updateProgress({
  247. overallProgress: 100,
  248. currentStep: steps.length,
  249. totalSteps: steps.length,
  250. stepName: 'completed',
  251. stepProgress: 100,
  252. stepStatus: 'Migration completed',
  253. stepDetails: summary,
  254. boardId: Session.get('currentBoard'),
  255. });
  256. migrationProgressManager.completeMigration();
  257. // Refresh status badges slightly after
  258. Meteor.setTimeout(() => {
  259. this.loadMigrationStatuses();
  260. }, 1000);
  261. }
  262. });
  263. }
  264. },
  265. events() {
  266. const self = this; // Capture component reference
  267. return [
  268. {
  269. 'click .js-run-migration[data-migration="comprehensive"]': Popup.afterConfirm('runComprehensiveMigration', function() {
  270. self.runMigration('comprehensive');
  271. Popup.back();
  272. }),
  273. 'click .js-run-migration[data-migration="fixMissingLists"]': Popup.afterConfirm('runFixMissingListsMigration', function() {
  274. self.runMigration('fixMissingLists');
  275. Popup.back();
  276. }),
  277. 'click .js-run-migration[data-migration="deleteDuplicateEmptyLists"]': Popup.afterConfirm('runDeleteDuplicateEmptyListsMigration', function() {
  278. self.runMigration('deleteDuplicateEmptyLists');
  279. Popup.back();
  280. }),
  281. 'click .js-run-migration[data-migration="restoreLostCards"]': Popup.afterConfirm('runRestoreLostCardsMigration', function() {
  282. self.runMigration('restoreLostCards');
  283. Popup.back();
  284. }),
  285. 'click .js-run-migration[data-migration="restoreAllArchived"]': Popup.afterConfirm('runRestoreAllArchivedMigration', function() {
  286. self.runMigration('restoreAllArchived');
  287. Popup.back();
  288. }),
  289. 'click .js-run-migration[data-migration="fixAvatarUrls"]': Popup.afterConfirm('runFixAvatarUrlsMigration', function() {
  290. self.runMigration('fixAvatarUrls');
  291. Popup.back();
  292. }),
  293. 'click .js-run-migration[data-migration="fixAllFileUrls"]': Popup.afterConfirm('runFixAllFileUrlsMigration', function() {
  294. self.runMigration('fixAllFileUrls');
  295. Popup.back();
  296. }),
  297. },
  298. ];
  299. },
  300. }).register('migrationsSidebar');