fixDuplicateLists.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import { Meteor } from 'meteor/meteor';
  2. import { check } from 'meteor/check';
  3. import Boards from '/models/boards';
  4. import Lists from '/models/lists';
  5. import Swimlanes from '/models/swimlanes';
  6. import Cards from '/models/cards';
  7. /**
  8. * Fix duplicate lists and swimlanes created by WeKan 8.10
  9. * This method identifies and removes duplicate lists while preserving cards
  10. */
  11. Meteor.methods({
  12. 'fixDuplicateLists.fixAllBoards'() {
  13. if (!this.userId) {
  14. throw new Meteor.Error('not-authorized');
  15. }
  16. console.log('Starting duplicate lists fix for all boards...');
  17. const allBoards = Boards.find({}).fetch();
  18. let totalFixed = 0;
  19. let totalBoardsProcessed = 0;
  20. for (const board of allBoards) {
  21. try {
  22. const result = this.fixDuplicateListsForBoard(board._id);
  23. totalFixed += result.fixed;
  24. totalBoardsProcessed++;
  25. if (result.fixed > 0) {
  26. console.log(`Fixed ${result.fixed} duplicate lists in board "${board.title}" (${board._id})`);
  27. }
  28. } catch (error) {
  29. console.error(`Error fixing board ${board._id}:`, error);
  30. }
  31. }
  32. console.log(`Duplicate lists fix completed. Processed ${totalBoardsProcessed} boards, fixed ${totalFixed} duplicate lists.`);
  33. return {
  34. message: `Fixed ${totalFixed} duplicate lists across ${totalBoardsProcessed} boards`,
  35. totalFixed,
  36. totalBoardsProcessed
  37. };
  38. },
  39. 'fixDuplicateLists.fixBoard'(boardId) {
  40. check(boardId, String);
  41. if (!this.userId) {
  42. throw new Meteor.Error('not-authorized');
  43. }
  44. return this.fixDuplicateListsForBoard(boardId);
  45. },
  46. fixDuplicateListsForBoard(boardId) {
  47. console.log(`Fixing duplicate lists for board ${boardId}...`);
  48. // First, fix duplicate swimlanes
  49. const swimlaneResult = this.fixDuplicateSwimlanes(boardId);
  50. // Then, fix duplicate lists
  51. const listResult = this.fixDuplicateLists(boardId);
  52. return {
  53. boardId,
  54. fixedSwimlanes: swimlaneResult.fixed,
  55. fixedLists: listResult.fixed,
  56. fixed: swimlaneResult.fixed + listResult.fixed
  57. };
  58. },
  59. fixDuplicateSwimlanes(boardId) {
  60. const swimlanes = Swimlanes.find({ boardId }).fetch();
  61. const swimlaneGroups = {};
  62. let fixed = 0;
  63. // Group swimlanes by title
  64. swimlanes.forEach(swimlane => {
  65. const key = swimlane.title || 'Default';
  66. if (!swimlaneGroups[key]) {
  67. swimlaneGroups[key] = [];
  68. }
  69. swimlaneGroups[key].push(swimlane);
  70. });
  71. // For each group with duplicates, keep the oldest and remove the rest
  72. Object.keys(swimlaneGroups).forEach(title => {
  73. const group = swimlaneGroups[title];
  74. if (group.length > 1) {
  75. // Sort by creation date, keep the oldest
  76. group.sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
  77. const keepSwimlane = group[0];
  78. const removeSwimlanes = group.slice(1);
  79. console.log(`Found ${group.length} duplicate swimlanes with title "${title}", keeping oldest (${keepSwimlane._id})`);
  80. // Move all lists from duplicate swimlanes to the kept swimlane
  81. removeSwimlanes.forEach(swimlane => {
  82. const lists = Lists.find({ swimlaneId: swimlane._id }).fetch();
  83. lists.forEach(list => {
  84. // Check if a list with the same title already exists in the kept swimlane
  85. const existingList = Lists.findOne({
  86. boardId,
  87. swimlaneId: keepSwimlane._id,
  88. title: list.title
  89. });
  90. if (existingList) {
  91. // Move cards to existing list
  92. Cards.update(
  93. { listId: list._id },
  94. { $set: { listId: existingList._id } },
  95. { multi: true }
  96. );
  97. // Remove duplicate list
  98. Lists.remove(list._id);
  99. console.log(`Moved cards from duplicate list "${list.title}" to existing list in kept swimlane`);
  100. } else {
  101. // Move list to kept swimlane
  102. Lists.update(list._id, { $set: { swimlaneId: keepSwimlane._id } });
  103. console.log(`Moved list "${list.title}" to kept swimlane`);
  104. }
  105. });
  106. // Remove duplicate swimlane
  107. Swimlanes.remove(swimlane._id);
  108. fixed++;
  109. });
  110. }
  111. });
  112. return { fixed };
  113. },
  114. fixDuplicateLists(boardId) {
  115. const lists = Lists.find({ boardId }).fetch();
  116. const listGroups = {};
  117. let fixed = 0;
  118. // Group lists by title and swimlaneId
  119. lists.forEach(list => {
  120. const key = `${list.swimlaneId || 'null'}-${list.title}`;
  121. if (!listGroups[key]) {
  122. listGroups[key] = [];
  123. }
  124. listGroups[key].push(list);
  125. });
  126. // For each group with duplicates, keep the oldest and remove the rest
  127. Object.keys(listGroups).forEach(key => {
  128. const group = listGroups[key];
  129. if (group.length > 1) {
  130. // Sort by creation date, keep the oldest
  131. group.sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
  132. const keepList = group[0];
  133. const removeLists = group.slice(1);
  134. console.log(`Found ${group.length} duplicate lists with title "${keepList.title}" in swimlane ${keepList.swimlaneId}, keeping oldest (${keepList._id})`);
  135. // Move all cards from duplicate lists to the kept list
  136. removeLists.forEach(list => {
  137. Cards.update(
  138. { listId: list._id },
  139. { $set: { listId: keepList._id } },
  140. { multi: true }
  141. );
  142. // Remove duplicate list
  143. Lists.remove(list._id);
  144. fixed++;
  145. console.log(`Moved cards from duplicate list "${list.title}" to kept list`);
  146. });
  147. }
  148. });
  149. return { fixed };
  150. },
  151. 'fixDuplicateLists.getReport'() {
  152. if (!this.userId) {
  153. throw new Meteor.Error('not-authorized');
  154. }
  155. const allBoards = Boards.find({}).fetch();
  156. const report = [];
  157. for (const board of allBoards) {
  158. const swimlanes = Swimlanes.find({ boardId: board._id }).fetch();
  159. const lists = Lists.find({ boardId: board._id }).fetch();
  160. // Check for duplicate swimlanes
  161. const swimlaneGroups = {};
  162. swimlanes.forEach(swimlane => {
  163. const key = swimlane.title || 'Default';
  164. if (!swimlaneGroups[key]) {
  165. swimlaneGroups[key] = [];
  166. }
  167. swimlaneGroups[key].push(swimlane);
  168. });
  169. // Check for duplicate lists
  170. const listGroups = {};
  171. lists.forEach(list => {
  172. const key = `${list.swimlaneId || 'null'}-${list.title}`;
  173. if (!listGroups[key]) {
  174. listGroups[key] = [];
  175. }
  176. listGroups[key].push(list);
  177. });
  178. const duplicateSwimlanes = Object.values(swimlaneGroups).filter(group => group.length > 1);
  179. const duplicateLists = Object.values(listGroups).filter(group => group.length > 1);
  180. if (duplicateSwimlanes.length > 0 || duplicateLists.length > 0) {
  181. report.push({
  182. boardId: board._id,
  183. boardTitle: board.title,
  184. duplicateSwimlanes: duplicateSwimlanes.length,
  185. duplicateLists: duplicateLists.length,
  186. totalSwimlanes: swimlanes.length,
  187. totalLists: lists.length
  188. });
  189. }
  190. }
  191. return {
  192. totalBoards: allBoards.length,
  193. boardsWithDuplicates: report.length,
  194. report
  195. };
  196. }
  197. });