fixDuplicateLists.js 7.9 KB

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