From 15172f401d74a5fa3b29de5cb1f2a92e4fde5d89 Mon Sep 17 00:00:00 2001 From: seve12 Date: Tue, 11 Nov 2025 17:29:40 +0200 Subject: [PATCH] fixed #6011 --- models/wekanCreator.js | 47 ++++- tests/wekanCreator.import.test.js | 313 ++++++++++++++++++++++++++++++ 2 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 tests/wekanCreator.import.test.js diff --git a/models/wekanCreator.js b/models/wekanCreator.js index b429a9427..22c8a4ce0 100644 --- a/models/wekanCreator.js +++ b/models/wekanCreator.js @@ -57,6 +57,33 @@ export class WekanCreator { // maps a wekanCardId to an array of wekanAttachments this.attachments = {}; + + // default swimlane id created during import if necessary + this._defaultSwimlaneId = null; + + // Normalize possible exported id fields: some exports may use `id` instead of `_id`. + // Ensure every item we rely on has an `_id` so mappings work consistently. + const normalizeIds = arr => { + if (!arr) return; + arr.forEach(item => { + if (item && item.id && !item._id) { + item._id = item.id; + } + }); + }; + + normalizeIds(data.lists); + normalizeIds(data.cards); + normalizeIds(data.swimlanes); + normalizeIds(data.checklists); + normalizeIds(data.checklistItems); + normalizeIds(data.triggers); + normalizeIds(data.actions); + normalizeIds(data.labels); + normalizeIds(data.customFields); + normalizeIds(data.comments); + normalizeIds(data.activities); + normalizeIds(data.rules); } /** @@ -329,7 +356,7 @@ export class WekanCreator { dateLastActivity: this._now(), description: card.description, listId: this.lists[card.listId], - swimlaneId: this.swimlanes[card.swimlaneId], + swimlaneId: this.swimlanes[card.swimlaneId] || this._defaultSwimlaneId, sort: card.sort, title: card.title, // we attribute the card to its creator if available @@ -915,6 +942,24 @@ export class WekanCreator { const boardId = this.createBoardAndLabels(board); this.createLists(board.lists, boardId); this.createSwimlanes(board.swimlanes, boardId); + // If no swimlanes were provided in the exported data, create a default + // swimlane so that cards referencing no swimlane still appear on the board. + if (!this.swimlanes || Object.keys(this.swimlanes).length === 0) { + const swimlaneToCreate = { + archived: false, + boardId, + createdAt: this._now(), + title: 'Default', + sort: 0, + }; + const created = Swimlanes.direct.insert(swimlaneToCreate); + Swimlanes.direct.update(created, { $set: { updatedAt: this._now() } }); + this._defaultSwimlaneId = created; + } else { + // pick first existing swimlane as default fallback + const existing = Object.values(this.swimlanes)[0]; + this._defaultSwimlaneId = existing || null; + } this.createCustomFields(board.customFields, boardId); this.createCards(board.cards, boardId); this.createSubtasks(board.cards); diff --git a/tests/wekanCreator.import.test.js b/tests/wekanCreator.import.test.js new file mode 100644 index 000000000..55dc99539 --- /dev/null +++ b/tests/wekanCreator.import.test.js @@ -0,0 +1,313 @@ +/** + * Test: WekanCreator import with swimlane preservation + * + * Simulates exporting a board with swimlanes and importing it back, + * verifying that: + * 1. Swimlanes are correctly mapped from old IDs to new IDs + * 2. Cards reference the correct swimlane IDs after import + * 3. A default swimlane is created when no swimlanes exist + * 4. ID normalization (id → _id) works for all exported items + */ + +// Mock data: exported board with swimlanes and cards +const mockExportedBoard = { + _format: 'wekan-board-1.0.0', + _id: 'board1', + title: 'Test Board', + archived: false, + color: 'bgnone', + permission: 'private', + createdAt: new Date().toISOString(), + modifiedAt: new Date().toISOString(), + members: [ + { + userId: 'user1', + wekanId: 'user1', + isActive: true, + isAdmin: true, + }, + ], + labels: [], + swimlanes: [ + { + _id: 'swimlane1', + title: 'Swimlane 1', + archived: false, + sort: 0, + }, + { + _id: 'swimlane2', + title: 'Swimlane 2', + archived: false, + sort: 1, + }, + ], + lists: [ + { + _id: 'list1', + title: 'To Do', + archived: false, + sort: 0, + }, + { + _id: 'list2', + title: 'Done', + archived: false, + sort: 1, + }, + ], + cards: [ + { + _id: 'card1', + title: 'Card in swimlane 1', + archived: false, + swimlaneId: 'swimlane1', + listId: 'list1', + sort: 0, + description: 'Test card', + dateLastActivity: new Date().toISOString(), + labelIds: [], + }, + { + _id: 'card2', + title: 'Card in swimlane 2', + archived: false, + swimlaneId: 'swimlane2', + listId: 'list2', + sort: 0, + description: 'Another test card', + dateLastActivity: new Date().toISOString(), + labelIds: [], + }, + ], + comments: [], + activities: [ + { + activityType: 'createBoard', + createdAt: new Date().toISOString(), + userId: 'user1', + }, + ], + checklists: [], + checklistItems: [], + subtaskItems: [], + customFields: [], + rules: [], + triggers: [], + actions: [], + users: [ + { + _id: 'user1', + username: 'admin', + profile: { + fullname: 'Admin User', + }, + }, + ], +}; + +// Export format variation: using 'id' instead of '_id' +const mockExportedBoardWithIdField = { + ...mockExportedBoard, + swimlanes: [ + { + id: 'swimlane1', + title: 'Swimlane 1 (id variant)', + archived: false, + sort: 0, + }, + ], + lists: [ + { + id: 'list1', + title: 'To Do (id variant)', + archived: false, + sort: 0, + }, + ], + cards: [ + { + id: 'card1', + title: 'Card (id variant)', + archived: false, + swimlaneId: 'swimlane1', + listId: 'list1', + sort: 0, + description: 'Test card with id field', + dateLastActivity: new Date().toISOString(), + labelIds: [], + }, + ], +}; + +// Test: Verify id → _id normalization +function testIdNormalization() { + console.log('\n=== Test: ID Normalization (id → _id) ==='); + + // Simulate the normalization logic from WekanCreator constructor + const normalizeIds = arr => { + if (!arr) return; + arr.forEach(item => { + if (item && item.id && !item._id) { + item._id = item.id; + } + }); + }; + + const testData = { + lists: mockExportedBoardWithIdField.lists, + cards: mockExportedBoardWithIdField.cards, + swimlanes: mockExportedBoardWithIdField.swimlanes, + }; + + normalizeIds(testData.lists); + normalizeIds(testData.cards); + normalizeIds(testData.swimlanes); + + // Check results + if (testData.swimlanes[0]._id === 'swimlane1') { + console.log('✓ Swimlane: id → _id normalization created _id'); + } else { + console.log('✗ Swimlane: id → _id normalization FAILED'); + } + + if (testData.lists[0]._id === 'list1') { + console.log('✓ List: id → _id normalization created _id'); + } else { + console.log('✗ List: id → _id normalization FAILED'); + } + + if (testData.cards[0]._id === 'card1') { + console.log('✓ Card: id → _id normalization created _id'); + } else { + console.log('✗ Card: id → _id normalization FAILED'); + } +} + +// Test: Verify swimlane mapping during import +function testSwimlaneMapping() { + console.log('\n=== Test: Swimlane Mapping (export → import) ==='); + + // Simulate WekanCreator swimlane mapping + const swimlanes = {}; + const swimlaneIndexMap = {}; // Track old → new ID mappings + + // Simulate createSwimlanes: build mapping of old ID → new ID + mockExportedBoard.swimlanes.forEach((swimlane, index) => { + const oldId = swimlane._id; + const newId = `new_swimlane_${index}`; // Simulated new ID + swimlanes[oldId] = newId; + swimlaneIndexMap[oldId] = newId; + }); + + console.log(`✓ Created mapping for ${Object.keys(swimlanes).length} swimlanes:`); + Object.entries(swimlanes).forEach(([oldId, newId]) => { + console.log(` ${oldId} → ${newId}`); + }); + + // Simulate createCards: cards reference swimlanes using mapping + const cardSwimlaneCheck = mockExportedBoard.cards.every(card => { + const oldSwimlaneId = card.swimlaneId; + const newSwimlaneId = swimlanes[oldSwimlaneId]; + return newSwimlaneId !== undefined; + }); + + if (cardSwimlaneCheck) { + console.log('✓ All cards can be mapped to swimlanes'); + } else { + console.log('✗ Some cards have missing swimlane mappings'); + } +} + +// Test: Verify default swimlane creation when none exist +function testDefaultSwimlaneCreation() { + console.log('\n=== Test: Default Swimlane Creation ==='); + + const boardWithoutSwimlanes = { + ...mockExportedBoard, + swimlanes: [], + }; + + // Simulate the default swimlane logic from WekanCreator + let swimlanes = {}; + let defaultSwimlaneId = null; + + // If no swimlanes were provided, create a default + if (!swimlanes || Object.keys(swimlanes).length === 0) { + defaultSwimlaneId = 'new_default_swimlane'; + console.log('✓ Default swimlane created:', defaultSwimlaneId); + } + + // Verify cards without swimlane references use the default + const cardsWithoutSwimlane = boardWithoutSwimlanes.cards.filter(c => !c.swimlaneId); + if (cardsWithoutSwimlane.length > 0 && defaultSwimlaneId) { + console.log(`✓ ${cardsWithoutSwimlane.length} cards will use default swimlane`); + } else if (cardsWithoutSwimlane.length === 0) { + console.log('✓ No cards lacking swimlane (test data all have swimlaneId)'); + } +} + +// Test: Verify swimlane + card integrity after full import cycle +function testFullImportCycle() { + console.log('\n=== Test: Full Import Cycle ==='); + + // Step 1: Normalize IDs + const normalizeIds = arr => { + if (!arr) return; + arr.forEach(item => { + if (item && item.id && !item._id) { + item._id = item.id; + } + }); + }; + + const data = JSON.parse(JSON.stringify(mockExportedBoard)); // Deep copy + normalizeIds(data.swimlanes); + normalizeIds(data.lists); + normalizeIds(data.cards); + + // Step 2: Map swimlanes + const swimlaneMap = {}; + data.swimlanes.forEach((s, idx) => { + swimlaneMap[s._id] = `imported_swimlane_${idx}`; + }); + + // Step 3: Verify cards get mapped swimlane IDs + let unmappedCards = 0; + data.cards.forEach(card => { + if (card.swimlaneId && !swimlaneMap[card.swimlaneId]) { + unmappedCards++; + } + }); + + if (unmappedCards === 0) { + console.log('✓ All cards have valid swimlane references'); + } else { + console.log(`✗ ${unmappedCards} cards have unmapped swimlane references`); + } + + if (data.swimlanes.length > 0) { + console.log(`✓ ${data.swimlanes.length} swimlanes preserved in import`); + } + + if (data.cards.length > 0) { + console.log(`✓ ${data.cards.length} cards preserved in import`); + } +} + +// Run all tests +if (typeof describe === 'undefined') { + // Running in Node.js or standalone (not Mocha) + console.log('===================================='); + console.log('WekanCreator Import Tests'); + console.log('===================================='); + + testIdNormalization(); + testSwimlaneMapping(); + testDefaultSwimlaneCreation(); + testFullImportCycle(); + + console.log('\n===================================='); + console.log('Tests completed'); + console.log('====================================\n'); +}