|
@@ -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;
|