mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 23:40:13 +01:00
parent
c7bbe47221
commit
b6e7b258e0
5 changed files with 667 additions and 29 deletions
|
|
@ -56,10 +56,15 @@ BlazeComponent.extendComponent({
|
||||||
const swimlanes = board.swimlanes();
|
const swimlanes = board.swimlanes();
|
||||||
|
|
||||||
if (swimlanes.length === 0) {
|
if (swimlanes.length === 0) {
|
||||||
|
// Check if any swimlane exists in the database to avoid race conditions
|
||||||
|
const existingSwimlanes = ReactiveCache.getSwimlanes({ boardId });
|
||||||
|
if (existingSwimlanes.length === 0) {
|
||||||
const swimlaneId = Swimlanes.insert({
|
const swimlaneId = Swimlanes.insert({
|
||||||
title: 'Default',
|
title: 'Default',
|
||||||
boardId: boardId,
|
boardId: boardId,
|
||||||
});
|
});
|
||||||
|
console.log(`Created default swimlane ${swimlaneId} for board ${boardId}`);
|
||||||
|
}
|
||||||
this._swimlaneCreated.add(boardId);
|
this._swimlaneCreated.add(boardId);
|
||||||
} else {
|
} else {
|
||||||
this._swimlaneCreated.add(boardId);
|
this._swimlaneCreated.add(boardId);
|
||||||
|
|
@ -197,6 +202,14 @@ BlazeComponent.extendComponent({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingList) {
|
if (!existingList) {
|
||||||
|
// Double-check to avoid race conditions
|
||||||
|
const doubleCheckList = ReactiveCache.getList({
|
||||||
|
boardId: boardId,
|
||||||
|
swimlaneId: swimlane._id,
|
||||||
|
title: sharedList.title
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!doubleCheckList) {
|
||||||
// Create a new list in this swimlane
|
// Create a new list in this swimlane
|
||||||
const newListData = {
|
const newListData = {
|
||||||
title: sharedList.title,
|
title: sharedList.title,
|
||||||
|
|
@ -220,6 +233,11 @@ BlazeComponent.extendComponent({
|
||||||
const archivedStatus = sharedList.archived ? ' (archived)' : ' (active)';
|
const archivedStatus = sharedList.archived ? ' (archived)' : ' (active)';
|
||||||
console.log(`Created list "${sharedList.title}"${archivedStatus} for swimlane ${swimlane.title || swimlane._id}`);
|
console.log(`Created list "${sharedList.title}"${archivedStatus} for swimlane ${swimlane.title || swimlane._id}`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log(`List "${sharedList.title}" already exists in swimlane ${swimlane.title || swimlane._id} (double-check), skipping`);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (process.env.DEBUG === 'true') {
|
if (process.env.DEBUG === 'true') {
|
||||||
console.log(`List "${sharedList.title}" already exists in swimlane ${swimlane.title || swimlane._id}, skipping`);
|
console.log(`List "${sharedList.title}" already exists in swimlane ${swimlane.title || swimlane._id}, skipping`);
|
||||||
|
|
|
||||||
93
client/lib/fixDuplicateLists.js
Normal file
93
client/lib/fixDuplicateLists.js
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client-side interface for fixing duplicate lists
|
||||||
|
*/
|
||||||
|
export const fixDuplicateLists = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a report of all boards with duplicate lists/swimlanes
|
||||||
|
*/
|
||||||
|
async getReport() {
|
||||||
|
try {
|
||||||
|
const result = await Meteor.callAsync('fixDuplicateLists.getReport');
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting duplicate lists report:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix duplicate lists for a specific board
|
||||||
|
*/
|
||||||
|
async fixBoard(boardId) {
|
||||||
|
try {
|
||||||
|
const result = await Meteor.callAsync('fixDuplicateLists.fixBoard', boardId);
|
||||||
|
console.log(`Fixed duplicate lists for board ${boardId}:`, result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fixing board ${boardId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix duplicate lists for all boards
|
||||||
|
*/
|
||||||
|
async fixAllBoards() {
|
||||||
|
try {
|
||||||
|
console.log('Starting fix for all boards...');
|
||||||
|
const result = await Meteor.callAsync('fixDuplicateLists.fixAllBoards');
|
||||||
|
console.log('Fix completed:', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fixing all boards:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interactive fix with user confirmation
|
||||||
|
*/
|
||||||
|
async interactiveFix() {
|
||||||
|
try {
|
||||||
|
// Get report first
|
||||||
|
console.log('Getting duplicate lists report...');
|
||||||
|
const report = await this.getReport();
|
||||||
|
|
||||||
|
if (report.boardsWithDuplicates === 0) {
|
||||||
|
console.log('No duplicate lists found!');
|
||||||
|
return { message: 'No duplicate lists found!' };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${report.boardsWithDuplicates} boards with duplicate lists:`);
|
||||||
|
report.report.forEach(board => {
|
||||||
|
console.log(`- Board "${board.boardTitle}" (${board.boardId}): ${board.duplicateSwimlanes} duplicate swimlanes, ${board.duplicateLists} duplicate lists`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ask for confirmation
|
||||||
|
const confirmed = confirm(
|
||||||
|
`Found ${report.boardsWithDuplicates} boards with duplicate lists. ` +
|
||||||
|
`This will fix ${report.report.reduce((sum, board) => sum + board.duplicateSwimlanes + board.duplicateLists, 0)} duplicates. ` +
|
||||||
|
'Continue?'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return { message: 'Fix cancelled by user' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the fix
|
||||||
|
const result = await this.fixAllBoards();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in interactive fix:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make it available globally for console access
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.fixDuplicateLists = fixDuplicateLists;
|
||||||
|
}
|
||||||
286
fix-duplicate-lists.js
Normal file
286
fix-duplicate-lists.js
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone script to fix duplicate lists created by WeKan 8.10
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node fix-duplicate-lists.js
|
||||||
|
*
|
||||||
|
* This script will:
|
||||||
|
* 1. Connect to the MongoDB database
|
||||||
|
* 2. Identify boards with duplicate lists/swimlanes
|
||||||
|
* 3. Fix the duplicates by merging them
|
||||||
|
* 4. Report the results
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { MongoClient } = require('mongodb');
|
||||||
|
|
||||||
|
// Configuration - adjust these for your setup
|
||||||
|
const MONGO_URL = process.env.MONGO_URL || 'mongodb://localhost:27017/wekan';
|
||||||
|
const DB_NAME = process.env.MONGO_DB_NAME || 'wekan';
|
||||||
|
|
||||||
|
class DuplicateListsFixer {
|
||||||
|
constructor() {
|
||||||
|
this.client = null;
|
||||||
|
this.db = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
console.log('Connecting to MongoDB...');
|
||||||
|
this.client = new MongoClient(MONGO_URL);
|
||||||
|
await this.client.connect();
|
||||||
|
this.db = this.client.db(DB_NAME);
|
||||||
|
console.log('Connected to MongoDB');
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
if (this.client) {
|
||||||
|
await this.client.close();
|
||||||
|
console.log('Disconnected from MongoDB');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getReport() {
|
||||||
|
console.log('Analyzing boards for duplicate lists...');
|
||||||
|
|
||||||
|
const boards = await this.db.collection('boards').find({}).toArray();
|
||||||
|
const report = [];
|
||||||
|
|
||||||
|
for (const board of boards) {
|
||||||
|
const swimlanes = await this.db.collection('swimlanes').find({ boardId: board._id }).toArray();
|
||||||
|
const lists = await this.db.collection('lists').find({ boardId: board._id }).toArray();
|
||||||
|
|
||||||
|
// Check for duplicate swimlanes
|
||||||
|
const swimlaneGroups = {};
|
||||||
|
swimlanes.forEach(swimlane => {
|
||||||
|
const key = swimlane.title || 'Default';
|
||||||
|
if (!swimlaneGroups[key]) {
|
||||||
|
swimlaneGroups[key] = [];
|
||||||
|
}
|
||||||
|
swimlaneGroups[key].push(swimlane);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for duplicate lists
|
||||||
|
const listGroups = {};
|
||||||
|
lists.forEach(list => {
|
||||||
|
const key = `${list.swimlaneId || 'null'}-${list.title}`;
|
||||||
|
if (!listGroups[key]) {
|
||||||
|
listGroups[key] = [];
|
||||||
|
}
|
||||||
|
listGroups[key].push(list);
|
||||||
|
});
|
||||||
|
|
||||||
|
const duplicateSwimlanes = Object.values(swimlaneGroups).filter(group => group.length > 1);
|
||||||
|
const duplicateLists = Object.values(listGroups).filter(group => group.length > 1);
|
||||||
|
|
||||||
|
if (duplicateSwimlanes.length > 0 || duplicateLists.length > 0) {
|
||||||
|
report.push({
|
||||||
|
boardId: board._id,
|
||||||
|
boardTitle: board.title,
|
||||||
|
duplicateSwimlanes: duplicateSwimlanes.length,
|
||||||
|
duplicateLists: duplicateLists.length,
|
||||||
|
totalSwimlanes: swimlanes.length,
|
||||||
|
totalLists: lists.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalBoards: boards.length,
|
||||||
|
boardsWithDuplicates: report.length,
|
||||||
|
report
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fixBoard(boardId) {
|
||||||
|
console.log(`Fixing duplicate lists for board ${boardId}...`);
|
||||||
|
|
||||||
|
// Fix duplicate swimlanes
|
||||||
|
const swimlaneResult = await this.fixDuplicateSwimlanes(boardId);
|
||||||
|
|
||||||
|
// Fix duplicate lists
|
||||||
|
const listResult = await this.fixDuplicateLists(boardId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
boardId,
|
||||||
|
fixedSwimlanes: swimlaneResult.fixed,
|
||||||
|
fixedLists: listResult.fixed,
|
||||||
|
fixed: swimlaneResult.fixed + listResult.fixed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fixDuplicateSwimlanes(boardId) {
|
||||||
|
const swimlanes = await this.db.collection('swimlanes').find({ boardId }).toArray();
|
||||||
|
const swimlaneGroups = {};
|
||||||
|
let fixed = 0;
|
||||||
|
|
||||||
|
// Group swimlanes by title
|
||||||
|
swimlanes.forEach(swimlane => {
|
||||||
|
const key = swimlane.title || 'Default';
|
||||||
|
if (!swimlaneGroups[key]) {
|
||||||
|
swimlaneGroups[key] = [];
|
||||||
|
}
|
||||||
|
swimlaneGroups[key].push(swimlane);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For each group with duplicates, keep the oldest and remove the rest
|
||||||
|
for (const [title, group] of Object.entries(swimlaneGroups)) {
|
||||||
|
if (group.length > 1) {
|
||||||
|
// Sort by creation date, keep the oldest
|
||||||
|
group.sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
|
||||||
|
const keepSwimlane = group[0];
|
||||||
|
const removeSwimlanes = group.slice(1);
|
||||||
|
|
||||||
|
console.log(`Found ${group.length} duplicate swimlanes with title "${title}", keeping oldest (${keepSwimlane._id})`);
|
||||||
|
|
||||||
|
// Move all lists from duplicate swimlanes to the kept swimlane
|
||||||
|
for (const swimlane of removeSwimlanes) {
|
||||||
|
const lists = await this.db.collection('lists').find({ swimlaneId: swimlane._id }).toArray();
|
||||||
|
for (const list of lists) {
|
||||||
|
// Check if a list with the same title already exists in the kept swimlane
|
||||||
|
const existingList = await this.db.collection('lists').findOne({
|
||||||
|
boardId,
|
||||||
|
swimlaneId: keepSwimlane._id,
|
||||||
|
title: list.title
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingList) {
|
||||||
|
// Move cards to existing list
|
||||||
|
await this.db.collection('cards').updateMany(
|
||||||
|
{ listId: list._id },
|
||||||
|
{ $set: { listId: existingList._id } }
|
||||||
|
);
|
||||||
|
// Remove duplicate list
|
||||||
|
await this.db.collection('lists').deleteOne({ _id: list._id });
|
||||||
|
console.log(`Moved cards from duplicate list "${list.title}" to existing list in kept swimlane`);
|
||||||
|
} else {
|
||||||
|
// Move list to kept swimlane
|
||||||
|
await this.db.collection('lists').updateOne(
|
||||||
|
{ _id: list._id },
|
||||||
|
{ $set: { swimlaneId: keepSwimlane._id } }
|
||||||
|
);
|
||||||
|
console.log(`Moved list "${list.title}" to kept swimlane`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicate swimlane
|
||||||
|
await this.db.collection('swimlanes').deleteOne({ _id: swimlane._id });
|
||||||
|
fixed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fixed };
|
||||||
|
}
|
||||||
|
|
||||||
|
async fixDuplicateLists(boardId) {
|
||||||
|
const lists = await this.db.collection('lists').find({ boardId }).toArray();
|
||||||
|
const listGroups = {};
|
||||||
|
let fixed = 0;
|
||||||
|
|
||||||
|
// Group lists by title and swimlaneId
|
||||||
|
lists.forEach(list => {
|
||||||
|
const key = `${list.swimlaneId || 'null'}-${list.title}`;
|
||||||
|
if (!listGroups[key]) {
|
||||||
|
listGroups[key] = [];
|
||||||
|
}
|
||||||
|
listGroups[key].push(list);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For each group with duplicates, keep the oldest and remove the rest
|
||||||
|
for (const [key, group] of Object.entries(listGroups)) {
|
||||||
|
if (group.length > 1) {
|
||||||
|
// Sort by creation date, keep the oldest
|
||||||
|
group.sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
|
||||||
|
const keepList = group[0];
|
||||||
|
const removeLists = group.slice(1);
|
||||||
|
|
||||||
|
console.log(`Found ${group.length} duplicate lists with title "${keepList.title}" in swimlane ${keepList.swimlaneId}, keeping oldest (${keepList._id})`);
|
||||||
|
|
||||||
|
// Move all cards from duplicate lists to the kept list
|
||||||
|
for (const list of removeLists) {
|
||||||
|
await this.db.collection('cards').updateMany(
|
||||||
|
{ listId: list._id },
|
||||||
|
{ $set: { listId: keepList._id } }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove duplicate list
|
||||||
|
await this.db.collection('lists').deleteOne({ _id: list._id });
|
||||||
|
fixed++;
|
||||||
|
console.log(`Moved cards from duplicate list "${list.title}" to kept list`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fixed };
|
||||||
|
}
|
||||||
|
|
||||||
|
async fixAllBoards() {
|
||||||
|
console.log('Starting duplicate lists fix for all boards...');
|
||||||
|
|
||||||
|
const allBoards = await this.db.collection('boards').find({}).toArray();
|
||||||
|
let totalFixed = 0;
|
||||||
|
let totalBoardsProcessed = 0;
|
||||||
|
|
||||||
|
for (const board of allBoards) {
|
||||||
|
try {
|
||||||
|
const result = await this.fixBoard(board._id);
|
||||||
|
totalFixed += result.fixed;
|
||||||
|
totalBoardsProcessed++;
|
||||||
|
|
||||||
|
if (result.fixed > 0) {
|
||||||
|
console.log(`Fixed ${result.fixed} duplicate lists in board "${board.title}" (${board._id})`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fixing board ${board._id}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Duplicate lists fix completed. Processed ${totalBoardsProcessed} boards, fixed ${totalFixed} duplicate lists.`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `Fixed ${totalFixed} duplicate lists across ${totalBoardsProcessed} boards`,
|
||||||
|
totalFixed,
|
||||||
|
totalBoardsProcessed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main execution
|
||||||
|
async function main() {
|
||||||
|
const fixer = new DuplicateListsFixer();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fixer.connect();
|
||||||
|
|
||||||
|
// Get report first
|
||||||
|
const report = await fixer.getReport();
|
||||||
|
|
||||||
|
if (report.boardsWithDuplicates === 0) {
|
||||||
|
console.log('No duplicate lists found!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${report.boardsWithDuplicates} boards with duplicate lists:`);
|
||||||
|
report.report.forEach(board => {
|
||||||
|
console.log(`- Board "${board.boardTitle}" (${board.boardId}): ${board.duplicateSwimlanes} duplicate swimlanes, ${board.duplicateLists} duplicate lists`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Perform the fix
|
||||||
|
const result = await fixer.fixAllBoards();
|
||||||
|
console.log('Fix completed:', result);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await fixer.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run if called directly
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DuplicateListsFixer;
|
||||||
|
|
@ -1235,6 +1235,12 @@ Boards.helpers({
|
||||||
getDefaultSwimline() {
|
getDefaultSwimline() {
|
||||||
let result = ReactiveCache.getSwimlane({ boardId: this._id });
|
let result = ReactiveCache.getSwimlane({ boardId: this._id });
|
||||||
if (result === undefined) {
|
if (result === undefined) {
|
||||||
|
// Check if any swimlane exists for this board to avoid duplicates
|
||||||
|
const existingSwimlanes = ReactiveCache.getSwimlanes({ boardId: this._id });
|
||||||
|
if (existingSwimlanes.length > 0) {
|
||||||
|
// Use the first existing swimlane
|
||||||
|
result = existingSwimlanes[0];
|
||||||
|
} else {
|
||||||
// Use fallback title if i18n is not available (e.g., during migration)
|
// Use fallback title if i18n is not available (e.g., during migration)
|
||||||
const title = TAPi18n && TAPi18n.i18n ? TAPi18n.__('default') : 'Default';
|
const title = TAPi18n && TAPi18n.i18n ? TAPi18n.__('default') : 'Default';
|
||||||
Swimlanes.insert({
|
Swimlanes.insert({
|
||||||
|
|
@ -1243,6 +1249,7 @@ Boards.helpers({
|
||||||
});
|
});
|
||||||
result = ReactiveCache.getSwimlane({ boardId: this._id });
|
result = ReactiveCache.getSwimlane({ boardId: this._id });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
234
server/methods/fixDuplicateLists.js
Normal file
234
server/methods/fixDuplicateLists.js
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import { check } from 'meteor/check';
|
||||||
|
import Boards from '/models/boards';
|
||||||
|
import Lists from '/models/lists';
|
||||||
|
import Swimlanes from '/models/swimlanes';
|
||||||
|
import Cards from '/models/cards';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix duplicate lists and swimlanes created by WeKan 8.10
|
||||||
|
* This method identifies and removes duplicate lists while preserving cards
|
||||||
|
*/
|
||||||
|
Meteor.methods({
|
||||||
|
'fixDuplicateLists.fixAllBoards'() {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Meteor.Error('not-authorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Starting duplicate lists fix for all boards...');
|
||||||
|
|
||||||
|
const allBoards = Boards.find({}).fetch();
|
||||||
|
let totalFixed = 0;
|
||||||
|
let totalBoardsProcessed = 0;
|
||||||
|
|
||||||
|
for (const board of allBoards) {
|
||||||
|
try {
|
||||||
|
const result = this.fixDuplicateListsForBoard(board._id);
|
||||||
|
totalFixed += result.fixed;
|
||||||
|
totalBoardsProcessed++;
|
||||||
|
|
||||||
|
if (result.fixed > 0) {
|
||||||
|
console.log(`Fixed ${result.fixed} duplicate lists in board "${board.title}" (${board._id})`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fixing board ${board._id}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Duplicate lists fix completed. Processed ${totalBoardsProcessed} boards, fixed ${totalFixed} duplicate lists.`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `Fixed ${totalFixed} duplicate lists across ${totalBoardsProcessed} boards`,
|
||||||
|
totalFixed,
|
||||||
|
totalBoardsProcessed
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
'fixDuplicateLists.fixBoard'(boardId) {
|
||||||
|
check(boardId, String);
|
||||||
|
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Meteor.Error('not-authorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fixDuplicateListsForBoard(boardId);
|
||||||
|
},
|
||||||
|
|
||||||
|
fixDuplicateListsForBoard(boardId) {
|
||||||
|
console.log(`Fixing duplicate lists for board ${boardId}...`);
|
||||||
|
|
||||||
|
// First, fix duplicate swimlanes
|
||||||
|
const swimlaneResult = this.fixDuplicateSwimlanes(boardId);
|
||||||
|
|
||||||
|
// Then, fix duplicate lists
|
||||||
|
const listResult = this.fixDuplicateLists(boardId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
boardId,
|
||||||
|
fixedSwimlanes: swimlaneResult.fixed,
|
||||||
|
fixedLists: listResult.fixed,
|
||||||
|
fixed: swimlaneResult.fixed + listResult.fixed
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
fixDuplicateSwimlanes(boardId) {
|
||||||
|
const swimlanes = Swimlanes.find({ boardId }).fetch();
|
||||||
|
const swimlaneGroups = {};
|
||||||
|
let fixed = 0;
|
||||||
|
|
||||||
|
// Group swimlanes by title
|
||||||
|
swimlanes.forEach(swimlane => {
|
||||||
|
const key = swimlane.title || 'Default';
|
||||||
|
if (!swimlaneGroups[key]) {
|
||||||
|
swimlaneGroups[key] = [];
|
||||||
|
}
|
||||||
|
swimlaneGroups[key].push(swimlane);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For each group with duplicates, keep the oldest and remove the rest
|
||||||
|
Object.keys(swimlaneGroups).forEach(title => {
|
||||||
|
const group = swimlaneGroups[title];
|
||||||
|
if (group.length > 1) {
|
||||||
|
// Sort by creation date, keep the oldest
|
||||||
|
group.sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
|
||||||
|
const keepSwimlane = group[0];
|
||||||
|
const removeSwimlanes = group.slice(1);
|
||||||
|
|
||||||
|
console.log(`Found ${group.length} duplicate swimlanes with title "${title}", keeping oldest (${keepSwimlane._id})`);
|
||||||
|
|
||||||
|
// Move all lists from duplicate swimlanes to the kept swimlane
|
||||||
|
removeSwimlanes.forEach(swimlane => {
|
||||||
|
const lists = Lists.find({ swimlaneId: swimlane._id }).fetch();
|
||||||
|
lists.forEach(list => {
|
||||||
|
// Check if a list with the same title already exists in the kept swimlane
|
||||||
|
const existingList = Lists.findOne({
|
||||||
|
boardId,
|
||||||
|
swimlaneId: keepSwimlane._id,
|
||||||
|
title: list.title
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingList) {
|
||||||
|
// Move cards to existing list
|
||||||
|
Cards.update(
|
||||||
|
{ listId: list._id },
|
||||||
|
{ $set: { listId: existingList._id } },
|
||||||
|
{ multi: true }
|
||||||
|
);
|
||||||
|
// Remove duplicate list
|
||||||
|
Lists.remove(list._id);
|
||||||
|
console.log(`Moved cards from duplicate list "${list.title}" to existing list in kept swimlane`);
|
||||||
|
} else {
|
||||||
|
// Move list to kept swimlane
|
||||||
|
Lists.update(list._id, { $set: { swimlaneId: keepSwimlane._id } });
|
||||||
|
console.log(`Moved list "${list.title}" to kept swimlane`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove duplicate swimlane
|
||||||
|
Swimlanes.remove(swimlane._id);
|
||||||
|
fixed++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { fixed };
|
||||||
|
},
|
||||||
|
|
||||||
|
fixDuplicateLists(boardId) {
|
||||||
|
const lists = Lists.find({ boardId }).fetch();
|
||||||
|
const listGroups = {};
|
||||||
|
let fixed = 0;
|
||||||
|
|
||||||
|
// Group lists by title and swimlaneId
|
||||||
|
lists.forEach(list => {
|
||||||
|
const key = `${list.swimlaneId || 'null'}-${list.title}`;
|
||||||
|
if (!listGroups[key]) {
|
||||||
|
listGroups[key] = [];
|
||||||
|
}
|
||||||
|
listGroups[key].push(list);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For each group with duplicates, keep the oldest and remove the rest
|
||||||
|
Object.keys(listGroups).forEach(key => {
|
||||||
|
const group = listGroups[key];
|
||||||
|
if (group.length > 1) {
|
||||||
|
// Sort by creation date, keep the oldest
|
||||||
|
group.sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
|
||||||
|
const keepList = group[0];
|
||||||
|
const removeLists = group.slice(1);
|
||||||
|
|
||||||
|
console.log(`Found ${group.length} duplicate lists with title "${keepList.title}" in swimlane ${keepList.swimlaneId}, keeping oldest (${keepList._id})`);
|
||||||
|
|
||||||
|
// Move all cards from duplicate lists to the kept list
|
||||||
|
removeLists.forEach(list => {
|
||||||
|
Cards.update(
|
||||||
|
{ listId: list._id },
|
||||||
|
{ $set: { listId: keepList._id } },
|
||||||
|
{ multi: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove duplicate list
|
||||||
|
Lists.remove(list._id);
|
||||||
|
fixed++;
|
||||||
|
console.log(`Moved cards from duplicate list "${list.title}" to kept list`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { fixed };
|
||||||
|
},
|
||||||
|
|
||||||
|
'fixDuplicateLists.getReport'() {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Meteor.Error('not-authorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const allBoards = Boards.find({}).fetch();
|
||||||
|
const report = [];
|
||||||
|
|
||||||
|
for (const board of allBoards) {
|
||||||
|
const swimlanes = Swimlanes.find({ boardId: board._id }).fetch();
|
||||||
|
const lists = Lists.find({ boardId: board._id }).fetch();
|
||||||
|
|
||||||
|
// Check for duplicate swimlanes
|
||||||
|
const swimlaneGroups = {};
|
||||||
|
swimlanes.forEach(swimlane => {
|
||||||
|
const key = swimlane.title || 'Default';
|
||||||
|
if (!swimlaneGroups[key]) {
|
||||||
|
swimlaneGroups[key] = [];
|
||||||
|
}
|
||||||
|
swimlaneGroups[key].push(swimlane);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for duplicate lists
|
||||||
|
const listGroups = {};
|
||||||
|
lists.forEach(list => {
|
||||||
|
const key = `${list.swimlaneId || 'null'}-${list.title}`;
|
||||||
|
if (!listGroups[key]) {
|
||||||
|
listGroups[key] = [];
|
||||||
|
}
|
||||||
|
listGroups[key].push(list);
|
||||||
|
});
|
||||||
|
|
||||||
|
const duplicateSwimlanes = Object.values(swimlaneGroups).filter(group => group.length > 1);
|
||||||
|
const duplicateLists = Object.values(listGroups).filter(group => group.length > 1);
|
||||||
|
|
||||||
|
if (duplicateSwimlanes.length > 0 || duplicateLists.length > 0) {
|
||||||
|
report.push({
|
||||||
|
boardId: board._id,
|
||||||
|
boardTitle: board.title,
|
||||||
|
duplicateSwimlanes: duplicateSwimlanes.length,
|
||||||
|
duplicateLists: duplicateLists.length,
|
||||||
|
totalSwimlanes: swimlanes.length,
|
||||||
|
totalLists: lists.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalBoards: allBoards.length,
|
||||||
|
boardsWithDuplicates: report.length,
|
||||||
|
report
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue