sidebarMigrations.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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. deleteEmptyListsNeeded() {
  76. return this.migrationStatuses.get().deleteEmptyLists === true;
  77. },
  78. deleteDuplicateEmptyListsNeeded() {
  79. return this.migrationStatuses.get().deleteDuplicateEmptyLists === true;
  80. },
  81. restoreLostCardsNeeded() {
  82. return this.migrationStatuses.get().restoreLostCards === true;
  83. },
  84. restoreAllArchivedNeeded() {
  85. return this.migrationStatuses.get().restoreAllArchived === true;
  86. },
  87. fixAvatarUrlsNeeded() {
  88. return this.migrationStatuses.get().fixAvatarUrls === true;
  89. },
  90. fixAllFileUrlsNeeded() {
  91. return this.migrationStatuses.get().fixAllFileUrls === true;
  92. },
  93. // Simulate migration progress updates using the global progress popup
  94. async simulateMigrationProgress(progressSteps) {
  95. const totalSteps = progressSteps.length;
  96. for (let i = 0; i < progressSteps.length; i++) {
  97. const step = progressSteps[i];
  98. const overall = Math.round(((i + 1) / totalSteps) * 100);
  99. // Start step
  100. migrationProgressManager.updateProgress({
  101. overallProgress: overall,
  102. currentStep: i + 1,
  103. totalSteps,
  104. stepName: step.step,
  105. stepProgress: 0,
  106. stepStatus: `Starting ${step.name}...`,
  107. stepDetails: null,
  108. boardId: Session.get('currentBoard'),
  109. });
  110. const stepDuration = step.duration;
  111. const updateInterval = 100;
  112. const totalUpdates = Math.max(1, Math.floor(stepDuration / updateInterval));
  113. for (let j = 0; j < totalUpdates; j++) {
  114. const per = Math.round(((j + 1) / totalUpdates) * 100);
  115. migrationProgressManager.updateProgress({
  116. overallProgress: overall,
  117. currentStep: i + 1,
  118. totalSteps,
  119. stepName: step.step,
  120. stepProgress: per,
  121. stepStatus: `Processing ${step.name}...`,
  122. stepDetails: { progress: `${per}%` },
  123. boardId: Session.get('currentBoard'),
  124. });
  125. // eslint-disable-next-line no-await-in-loop
  126. await new Promise((r) => setTimeout(r, updateInterval));
  127. }
  128. // Complete step
  129. migrationProgressManager.updateProgress({
  130. overallProgress: overall,
  131. currentStep: i + 1,
  132. totalSteps,
  133. stepName: step.step,
  134. stepProgress: 100,
  135. stepStatus: `${step.name} completed`,
  136. stepDetails: { status: 'completed' },
  137. boardId: Session.get('currentBoard'),
  138. });
  139. }
  140. },
  141. runMigration(migrationType) {
  142. const boardId = Session.get('currentBoard');
  143. let methodName;
  144. let methodArgs = [];
  145. switch (migrationType) {
  146. case 'comprehensive':
  147. methodName = 'comprehensiveBoardMigration.execute';
  148. methodArgs = [boardId];
  149. break;
  150. case 'fixMissingLists':
  151. methodName = 'fixMissingListsMigration.execute';
  152. methodArgs = [boardId];
  153. break;
  154. case 'deleteEmptyLists':
  155. methodName = 'deleteEmptyLists.execute';
  156. methodArgs = [boardId];
  157. break;
  158. case 'deleteDuplicateEmptyLists':
  159. methodName = 'deleteDuplicateEmptyLists.execute';
  160. methodArgs = [boardId];
  161. break;
  162. case 'restoreLostCards':
  163. methodName = 'restoreLostCards.execute';
  164. methodArgs = [boardId];
  165. break;
  166. case 'restoreAllArchived':
  167. methodName = 'restoreAllArchived.execute';
  168. methodArgs = [boardId];
  169. break;
  170. case 'fixAvatarUrls':
  171. methodName = 'fixAvatarUrls.execute';
  172. break;
  173. case 'fixAllFileUrls':
  174. methodName = 'fixAllFileUrls.execute';
  175. break;
  176. }
  177. if (methodName) {
  178. // Define simulated steps per migration type
  179. const stepsByType = {
  180. comprehensive: [
  181. { step: 'analyze_board_structure', name: 'Analyze Board Structure', duration: 800 },
  182. { step: 'fix_orphaned_cards', name: 'Fix Orphaned Cards', duration: 1200 },
  183. { step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 1000 },
  184. { step: 'ensure_per_swimlane_lists', name: 'Ensure Per-Swimlane Lists', duration: 800 },
  185. { step: 'validate_migration', name: 'Validate Migration', duration: 800 },
  186. { step: 'fix_avatar_urls', name: 'Fix Avatar URLs', duration: 600 },
  187. { step: 'fix_attachment_urls', name: 'Fix Attachment URLs', duration: 600 },
  188. ],
  189. fixMissingLists: [
  190. { step: 'analyze_lists', name: 'Analyze Lists', duration: 600 },
  191. { step: 'create_missing_lists', name: 'Create Missing Lists', duration: 900 },
  192. { step: 'update_cards', name: 'Update Cards', duration: 900 },
  193. { step: 'finalize', name: 'Finalize', duration: 400 },
  194. ],
  195. deleteEmptyLists: [
  196. { step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 700 },
  197. { step: 'delete_empty_lists', name: 'Delete Empty Lists', duration: 800 },
  198. ],
  199. deleteDuplicateEmptyLists: [
  200. { step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 700 },
  201. { step: 'delete_duplicate_empty_lists', name: 'Delete Duplicate Empty Lists', duration: 800 },
  202. ],
  203. restoreLostCards: [
  204. { step: 'ensure_lost_cards_swimlane', name: 'Ensure Lost Cards Swimlane', duration: 600 },
  205. { step: 'restore_lists', name: 'Restore Lists', duration: 800 },
  206. { step: 'restore_cards', name: 'Restore Cards', duration: 1000 },
  207. ],
  208. restoreAllArchived: [
  209. { step: 'restore_swimlanes', name: 'Restore Swimlanes', duration: 800 },
  210. { step: 'restore_lists', name: 'Restore Lists', duration: 900 },
  211. { step: 'restore_cards', name: 'Restore Cards', duration: 1000 },
  212. { step: 'fix_missing_ids', name: 'Fix Missing IDs', duration: 600 },
  213. ],
  214. fixAvatarUrls: [
  215. { step: 'scan_users', name: 'Scan Users', duration: 500 },
  216. { step: 'fix_urls', name: 'Fix Avatar URLs', duration: 900 },
  217. ],
  218. fixAllFileUrls: [
  219. { step: 'scan_files', name: 'Scan Files', duration: 600 },
  220. { step: 'fix_urls', name: 'Fix File URLs', duration: 1000 },
  221. ],
  222. };
  223. const steps = stepsByType[migrationType] || [
  224. { step: 'running', name: 'Running Migration', duration: 1000 },
  225. ];
  226. // Kick off popup and simulated progress
  227. migrationProgressManager.startMigration();
  228. const progressPromise = this.simulateMigrationProgress(steps);
  229. // Start migration call
  230. const callPromise = new Promise((resolve, reject) => {
  231. Meteor.call(methodName, ...methodArgs, (err, result) => {
  232. if (err) return reject(err);
  233. return resolve(result);
  234. });
  235. });
  236. Promise.allSettled([callPromise, progressPromise]).then(([callRes]) => {
  237. if (callRes.status === 'rejected') {
  238. migrationProgressManager.failMigration(callRes.reason);
  239. } else {
  240. const result = callRes.value;
  241. // Summarize result details in the popup
  242. let summary = {};
  243. if (result && result.results) {
  244. // Comprehensive returns {success, results}
  245. const r = result.results;
  246. summary = {
  247. totalCardsProcessed: r.totalCardsProcessed,
  248. totalListsProcessed: r.totalListsProcessed,
  249. totalListsCreated: r.totalListsCreated,
  250. };
  251. } else if (result && result.changes) {
  252. // Many migrations return a changes string array
  253. summary = { changes: result.changes.join(' | ') };
  254. } else if (result && typeof result === 'object') {
  255. summary = result;
  256. }
  257. migrationProgressManager.updateProgress({
  258. overallProgress: 100,
  259. currentStep: steps.length,
  260. totalSteps: steps.length,
  261. stepName: 'completed',
  262. stepProgress: 100,
  263. stepStatus: 'Migration completed',
  264. stepDetails: summary,
  265. boardId: Session.get('currentBoard'),
  266. });
  267. migrationProgressManager.completeMigration();
  268. // Refresh status badges slightly after
  269. Meteor.setTimeout(() => {
  270. this.loadMigrationStatuses();
  271. }, 1000);
  272. }
  273. });
  274. }
  275. },
  276. events() {
  277. const self = this; // Capture component reference
  278. return [
  279. {
  280. 'click .js-run-migration[data-migration="comprehensive"]': Popup.afterConfirm('runComprehensiveMigration', function() {
  281. self.runMigration('comprehensive');
  282. Popup.back();
  283. }),
  284. 'click .js-run-migration[data-migration="fixMissingLists"]': Popup.afterConfirm('runFixMissingListsMigration', function() {
  285. self.runMigration('fixMissingLists');
  286. Popup.back();
  287. }),
  288. 'click .js-run-migration[data-migration="deleteEmptyLists"]': Popup.afterConfirm('runDeleteEmptyListsMigration', function() {
  289. self.runMigration('deleteEmptyLists');
  290. Popup.back();
  291. }),
  292. 'click .js-run-migration[data-migration="deleteDuplicateEmptyLists"]': Popup.afterConfirm('runDeleteDuplicateEmptyListsMigration', function() {
  293. self.runMigration('deleteDuplicateEmptyLists');
  294. Popup.back();
  295. }),
  296. 'click .js-run-migration[data-migration="restoreLostCards"]': Popup.afterConfirm('runRestoreLostCardsMigration', function() {
  297. self.runMigration('restoreLostCards');
  298. Popup.back();
  299. }),
  300. 'click .js-run-migration[data-migration="restoreAllArchived"]': Popup.afterConfirm('runRestoreAllArchivedMigration', function() {
  301. self.runMigration('restoreAllArchived');
  302. Popup.back();
  303. }),
  304. 'click .js-run-migration[data-migration="fixAvatarUrls"]': Popup.afterConfirm('runFixAvatarUrlsMigration', function() {
  305. self.runMigration('fixAvatarUrls');
  306. Popup.back();
  307. }),
  308. 'click .js-run-migration[data-migration="fixAllFileUrls"]': Popup.afterConfirm('runFixAllFileUrlsMigration', function() {
  309. self.runMigration('fixAllFileUrls');
  310. Popup.back();
  311. }),
  312. },
  313. ];
  314. },
  315. }).register('migrationsSidebar');