mirror of
https://github.com/wekan/wekan.git
synced 2025-12-26 20:28:48 +01:00
Manually merged fixes from seve12.
Thanks to seve12 ! Related https://github.com/wekan/wekan/pull/5967
This commit is contained in:
parent
fc3bb962f7
commit
ecfb0f0fdf
14 changed files with 457 additions and 20 deletions
|
|
@ -66,6 +66,12 @@ and fixes the following bugs:
|
|||
Thanks to brlin-tw.
|
||||
- [Updated Mac docs for Applite](https://github.com/wekan/wekan/commit/400eb81206f346a973d871a8aaa55d4ac5d48753).
|
||||
Thanks to xet7.
|
||||
- [Fix checklist delete action (issue #6020), link-card popup defaults, and stabilize due-cards ordering](https://github.com/wekan/wekan/pull/5967).
|
||||
Thanks to seve12.
|
||||
- [Improve rules UI board dropdowns/loading, rule header titles, and ensure card move updates attachment metadata](https://github.com/wekan/wekan/pull/5967).
|
||||
Thanks to seve12.
|
||||
- [Improve imports: normalize id → _id, add default swimlane fallback, and add regression test](https://github.com/wekan/wekan/pull/5967).
|
||||
Thanks to seve12.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
|
|
|
|||
|
|
@ -305,7 +305,7 @@ BlazeComponent.extendComponent({
|
|||
{
|
||||
'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () {
|
||||
Popup.back(2);
|
||||
const checklist = this.checklist;
|
||||
const checklist = this.data().checklist;
|
||||
if (checklist && checklist._id) {
|
||||
Checklists.remove(checklist._id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,16 +85,19 @@ template(name="linkCardPopup")
|
|||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
option(value="") {{_ 'custom-field-dropdown-none'}}
|
||||
each swimlanes
|
||||
option(value="{{_id}}") {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
option(value="") {{_ 'custom-field-dropdown-none'}}
|
||||
each lists
|
||||
option(value="{{_id}}") {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
option(value="") {{_ 'custom-field-dropdown-none'}}
|
||||
each cards
|
||||
option(value="{{getRealId}}") {{getTitle}}
|
||||
|
||||
|
|
|
|||
|
|
@ -542,8 +542,6 @@ BlazeComponent.extendComponent({
|
|||
{
|
||||
sort: { sort: 1 },
|
||||
});
|
||||
if (swimlanes.length)
|
||||
this.selectedSwimlaneId.set(swimlanes[0]._id);
|
||||
return swimlanes;
|
||||
},
|
||||
|
||||
|
|
@ -558,7 +556,6 @@ BlazeComponent.extendComponent({
|
|||
{
|
||||
sort: { sort: 1 },
|
||||
});
|
||||
if (lists.length) this.selectedListId.set(lists[0]._id);
|
||||
return lists;
|
||||
},
|
||||
|
||||
|
|
@ -567,19 +564,17 @@ BlazeComponent.extendComponent({
|
|||
return [];
|
||||
}
|
||||
const ownCardsIds = this.board.cards().map(card => card.getRealId());
|
||||
const ret = ReactiveCache.getCards(
|
||||
{
|
||||
boardId: this.selectedBoardId.get(),
|
||||
swimlaneId: this.selectedSwimlaneId.get(),
|
||||
listId: this.selectedListId.get(),
|
||||
const selector = {
|
||||
archived: false,
|
||||
linkedId: { $nin: ownCardsIds },
|
||||
_id: { $nin: ownCardsIds },
|
||||
type: { $nin: ['template-card'] },
|
||||
},
|
||||
{
|
||||
sort: { sort: 1 },
|
||||
});
|
||||
};
|
||||
if (this.selectedBoardId.get()) selector.boardId = this.selectedBoardId.get();
|
||||
if (this.selectedSwimlaneId.get()) selector.swimlaneId = this.selectedSwimlaneId.get();
|
||||
if (this.selectedListId.get()) selector.listId = this.selectedListId.get();
|
||||
|
||||
const ret = ReactiveCache.getCards(selector, { sort: { sort: 1 } });
|
||||
return ret;
|
||||
},
|
||||
|
||||
|
|
@ -600,8 +595,12 @@ BlazeComponent.extendComponent({
|
|||
return [
|
||||
{
|
||||
'change .js-select-boards'(evt) {
|
||||
subManager.subscribe('board', $(evt.currentTarget).val(), false);
|
||||
this.selectedBoardId.set($(evt.currentTarget).val());
|
||||
const val = $(evt.currentTarget).val();
|
||||
subManager.subscribe('board', val, false);
|
||||
// Clear selections to allow linking only board or re-choose swimlane/list
|
||||
this.selectedSwimlaneId.set('');
|
||||
this.selectedListId.set('');
|
||||
this.selectedBoardId.set(val);
|
||||
},
|
||||
'change .js-select-swimlanes'(evt) {
|
||||
this.selectedSwimlaneId.set($(evt.currentTarget).val());
|
||||
|
|
|
|||
|
|
@ -232,6 +232,24 @@ class DueCardsComponent extends BlazeComponent {
|
|||
});
|
||||
}
|
||||
|
||||
// Normalize dueAt to timestamps for stable client-side ordering
|
||||
const future = new Date('2100-12-31').getTime();
|
||||
const toTime = v => {
|
||||
if (v === null || v === undefined || v === '') return future;
|
||||
if (v instanceof Date) return v.getTime();
|
||||
const t = new Date(v);
|
||||
if (!isNaN(t.getTime())) return t.getTime();
|
||||
return future;
|
||||
};
|
||||
|
||||
filteredCards.sort((a, b) => {
|
||||
const x = toTime(a.dueAt);
|
||||
const y = toTime(b.dueAt);
|
||||
if (x > y) return 1;
|
||||
if (x < y) return -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards client: filtered to', filteredCards.length, 'cards');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ template(name="boardActions")
|
|||
| {{_'r-the-board'}}
|
||||
div.trigger-dropdown
|
||||
select(id="board-id")
|
||||
option(value="" disabled selected if=not boards.length) {{loadingBoardsLabel}}
|
||||
each boards
|
||||
if $eq _id currentBoard._id
|
||||
option(value="{{_id}}" selected) {{_ 'current'}}
|
||||
|
|
@ -85,6 +86,7 @@ template(name="boardActions")
|
|||
| {{_'r-the-board'}}
|
||||
div.trigger-dropdown
|
||||
select(id="board-id-link")
|
||||
option(value="" disabled selected if=not boards.length) {{loadingBoardsLabel}}
|
||||
each boards
|
||||
if $eq _id currentBoard._id
|
||||
option(value="{{_id}}" selected) {{_ 'current'}}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {},
|
||||
onCreated() {
|
||||
// Ensure boards are available for action dropdowns
|
||||
this.subscribe('boards');
|
||||
},
|
||||
|
||||
boards() {
|
||||
const ret = ReactiveCache.getBoards(
|
||||
|
|
@ -19,6 +23,16 @@ BlazeComponent.extendComponent({
|
|||
return ret;
|
||||
},
|
||||
|
||||
loadingBoardsLabel() {
|
||||
try {
|
||||
const txt = TAPi18n.__('loading-boards');
|
||||
if (txt && !txt.startsWith("key '")) return txt;
|
||||
} catch (e) {
|
||||
// ignore translation lookup errors
|
||||
}
|
||||
return 'Loading boards...';
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ BlazeComponent.extendComponent({
|
|||
this.subscribe('allRules');
|
||||
this.subscribe('allTriggers');
|
||||
this.subscribe('allActions');
|
||||
this.subscribe('boards');
|
||||
},
|
||||
|
||||
trigger() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
template(name="rulesActions")
|
||||
h2
|
||||
| ✨
|
||||
| {{_ 'r-rule' }} "#{data.ruleName.get}" - {{_ 'r-add-action'}}
|
||||
| {{_ 'r-rule' }} "
|
||||
= ruleName.get()
|
||||
| " - {{_ 'r-add-action'}}
|
||||
.triggers-content
|
||||
.triggers-body
|
||||
.triggers-side-menu
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
template(name="rulesTriggers")
|
||||
h2
|
||||
| ✨
|
||||
| {{_ 'r-rule' }} "#{data.ruleName.get}" - {{_ 'r-add-trigger'}}
|
||||
| {{_ 'r-rule' }} "
|
||||
= ruleName.get()
|
||||
| " - {{_ 'r-add-trigger'}}
|
||||
.triggers-content
|
||||
.triggers-body
|
||||
.triggers-side-menu
|
||||
|
|
|
|||
|
|
@ -2107,6 +2107,28 @@ Cards.mutations({
|
|||
Cards.update(this._id, {
|
||||
$set: mutatedFields,
|
||||
});
|
||||
|
||||
// Ensure attachments follow the card to its new board/list/swimlane
|
||||
if (Meteor.isServer) {
|
||||
const updateMeta = {};
|
||||
if (mutatedFields.boardId !== undefined) updateMeta['meta.boardId'] = mutatedFields.boardId;
|
||||
if (mutatedFields.listId !== undefined) updateMeta['meta.listId'] = mutatedFields.listId;
|
||||
if (mutatedFields.swimlaneId !== undefined) updateMeta['meta.swimlaneId'] = mutatedFields.swimlaneId;
|
||||
|
||||
if (Object.keys(updateMeta).length > 0) {
|
||||
try {
|
||||
Attachments.collection.update(
|
||||
{ 'meta.cardId': this._id },
|
||||
{ $set: updateMeta },
|
||||
{ multi: true },
|
||||
);
|
||||
} catch (err) {
|
||||
// Do not block the move if attachment update fails
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to update attachments metadata after moving card', this._id, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addLabel(labelId) {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -348,7 +375,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
|
||||
|
|
@ -588,6 +615,25 @@ export class WekanCreator {
|
|||
}
|
||||
|
||||
createSwimlanes(wekanSwimlanes, boardId) {
|
||||
// If no swimlanes provided, create a default so cards still render
|
||||
if (!wekanSwimlanes || wekanSwimlanes.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;
|
||||
return;
|
||||
}
|
||||
|
||||
wekanSwimlanes.forEach((swimlane, swimlaneIndex) => {
|
||||
const swimlaneToCreate = {
|
||||
archived: swimlane.archived,
|
||||
|
|
@ -611,6 +657,9 @@ export class WekanCreator {
|
|||
},
|
||||
});
|
||||
this.swimlanes[swimlane._id] = swimlaneId;
|
||||
if (!this._defaultSwimlaneId) {
|
||||
this._defaultSwimlaneId = swimlaneId;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ function pause(){
|
|||
|
||||
echo
|
||||
PS3='Please enter your choice: '
|
||||
options=("Install Wekan dependencies" "Build Wekan" "Run Meteor for dev on http://localhost:4000" "Run Meteor for dev on http://localhost:4000 with trace warnings, and warnings using old Meteor API that will not exist in Meteor 3.0" "Run Meteor for dev on http://localhost:4000 with bundle visualizer" "Run Meteor for dev on http://CURRENT-IP-ADDRESS:4000" "Run Meteor for dev on http://CURRENT-IP-ADDRESS:4000 with MONGO_URL=mongodb://127.0.0.1:27019/wekan" "Run Meteor for dev on http://CUSTOM-IP-ADDRESS:PORT" "Save Meteor dependency chain to ../meteor-deps.txt" "Quit")
|
||||
options=("Install Wekan dependencies" "Build Wekan" "Run Meteor for dev on http://localhost:4000" "Run Meteor for dev on http://localhost:4000 with trace warnings, and warnings using old Meteor API that will not exist in Meteor 3.0" "Run Meteor for dev on http://localhost:4000 with bundle visualizer" "Run Meteor for dev on http://CURRENT-IP-ADDRESS:4000" "Run Meteor for dev on http://CURRENT-IP-ADDRESS:4000 with MONGO_URL=mongodb://127.0.0.1:27019/wekan" "Run Meteor for dev on http://CUSTOM-IP-ADDRESS:PORT" "Run tests" "Save Meteor dependency chain to ../meteor-deps.txt" "Quit")
|
||||
|
||||
select opt in "${options[@]}"
|
||||
do
|
||||
|
|
@ -240,6 +240,12 @@ do
|
|||
break
|
||||
;;
|
||||
|
||||
"Run tests")
|
||||
echo "Running tests (import regression)."
|
||||
node tests/wekanCreator.import.test.js
|
||||
break
|
||||
;;
|
||||
|
||||
"Quit")
|
||||
break
|
||||
;;
|
||||
|
|
|
|||
313
tests/wekanCreator.import.test.js
Normal file
313
tests/wekanCreator.import.test.js
Normal file
|
|
@ -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');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue