mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 15:30:13 +01:00
parent
9473c1fe41
commit
80777b4663
5 changed files with 331 additions and 30 deletions
|
|
@ -115,6 +115,9 @@ BlazeComponent.extendComponent({
|
|||
// Convert shared lists to per-swimlane lists if needed
|
||||
await this.convertSharedListsToPerSwimlane(boardId);
|
||||
|
||||
// Fix missing lists migration (for cards with wrong listId references)
|
||||
await this.fixMissingLists(boardId);
|
||||
|
||||
// Start attachment migration in background if needed
|
||||
this.startAttachmentMigrationIfNeeded(boardId);
|
||||
} catch (error) {
|
||||
|
|
@ -236,6 +239,61 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
},
|
||||
|
||||
async fixMissingLists(boardId) {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) return;
|
||||
|
||||
// Check if board has already been processed for missing lists fix
|
||||
if (board.fixMissingListsCompleted) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has already been processed for missing lists fix`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if migration is needed
|
||||
const needsMigration = await new Promise((resolve, reject) => {
|
||||
Meteor.call('fixMissingListsMigration.needsMigration', boardId, (error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!needsMigration) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} does not need missing lists fix`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Starting fix missing lists migration for board ${boardId}`);
|
||||
}
|
||||
|
||||
// Execute the migration
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
Meteor.call('fixMissingListsMigration.execute', boardId, (error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (result && result.success) {
|
||||
console.log(`Successfully fixed missing lists for board ${boardId}: created ${result.createdLists} lists, updated ${result.updatedCards} cards`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fixing missing lists:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async startAttachmentMigrationIfNeeded(boardId) {
|
||||
try {
|
||||
// Check if board has already been migrated
|
||||
|
|
|
|||
|
|
@ -803,13 +803,6 @@ Boards.helpers({
|
|||
{
|
||||
boardId: this._id,
|
||||
archived: false,
|
||||
// Get lists for all swimlanes in this board, plus lists without swimlaneId for backward compatibility
|
||||
$or: [
|
||||
{ swimlaneId: { $in: this.swimlanes().map(s => s._id) } },
|
||||
{ swimlaneId: { $exists: false } },
|
||||
{ swimlaneId: '' },
|
||||
{ swimlaneId: null }
|
||||
],
|
||||
},
|
||||
{ sort: sortKey },
|
||||
);
|
||||
|
|
@ -819,13 +812,6 @@ Boards.helpers({
|
|||
return ReactiveCache.getLists(
|
||||
{
|
||||
boardId: this._id,
|
||||
// Get lists for all swimlanes in this board, plus lists without swimlaneId for backward compatibility
|
||||
$or: [
|
||||
{ swimlaneId: { $in: this.swimlanes().map(s => s._id) } },
|
||||
{ swimlaneId: { $exists: false } },
|
||||
{ swimlaneId: '' },
|
||||
{ swimlaneId: null }
|
||||
]
|
||||
},
|
||||
{ sort: { sort: 1 } }
|
||||
);
|
||||
|
|
|
|||
|
|
@ -211,33 +211,20 @@ Swimlanes.helpers({
|
|||
return this.draggableLists();
|
||||
},
|
||||
newestLists() {
|
||||
// sorted lists from newest to the oldest, by its creation date or its cards' last modification date
|
||||
// Include lists without swimlaneId for backward compatibility (they belong to default swimlane)
|
||||
// Revert to shared lists across swimlanes: filter by board only
|
||||
return ReactiveCache.getLists(
|
||||
{
|
||||
boardId: this.boardId,
|
||||
$or: [
|
||||
{ swimlaneId: this._id },
|
||||
{ swimlaneId: { $exists: false } },
|
||||
{ swimlaneId: '' },
|
||||
{ swimlaneId: null }
|
||||
],
|
||||
archived: false,
|
||||
},
|
||||
{ sort: { modifiedAt: -1 } },
|
||||
);
|
||||
},
|
||||
draggableLists() {
|
||||
// Include lists without swimlaneId for backward compatibility (they belong to default swimlane)
|
||||
// Revert to shared lists across swimlanes: filter by board only
|
||||
return ReactiveCache.getLists(
|
||||
{
|
||||
boardId: this.boardId,
|
||||
$or: [
|
||||
{ swimlaneId: this._id },
|
||||
{ swimlaneId: { $exists: false } },
|
||||
{ swimlaneId: '' },
|
||||
{ swimlaneId: null }
|
||||
],
|
||||
//archived: false,
|
||||
},
|
||||
{ sort: ['sort'] },
|
||||
|
|
@ -245,7 +232,8 @@ Swimlanes.helpers({
|
|||
},
|
||||
|
||||
myLists() {
|
||||
return ReactiveCache.getLists({ swimlaneId: this._id });
|
||||
// Revert to shared lists: provide lists by board for this swimlane's board
|
||||
return ReactiveCache.getLists({ boardId: this.boardId });
|
||||
},
|
||||
|
||||
allCards() {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ if (errors.length > 0) {
|
|||
// Import cron job storage for persistent job tracking
|
||||
import './cronJobStorage';
|
||||
|
||||
// Import migrations
|
||||
import './migrations/fixMissingListsMigration';
|
||||
|
||||
// Note: Automatic migrations are disabled - migrations only run when opening boards
|
||||
// import './boardMigrationDetector';
|
||||
// import './cronMigrationManager';
|
||||
|
|
|
|||
266
server/migrations/fixMissingListsMigration.js
Normal file
266
server/migrations/fixMissingListsMigration.js
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
/**
|
||||
* Fix Missing Lists Migration
|
||||
*
|
||||
* This migration fixes the issue where cards have incorrect listId references
|
||||
* due to the per-swimlane lists change. It detects cards with mismatched
|
||||
* listId/swimlaneId and creates the missing lists.
|
||||
*
|
||||
* Issue: When upgrading from v7.94 to v8.02, cards that were in different
|
||||
* swimlanes but shared the same list now have wrong listId references.
|
||||
*
|
||||
* Example:
|
||||
* - Card1: listId: 'HB93dWNnY5bgYdtxc', swimlaneId: 'sK69SseWkh3tMbJvg'
|
||||
* - Card2: listId: 'HB93dWNnY5bgYdtxc', swimlaneId: 'XeecF9nZxGph4zcT4'
|
||||
*
|
||||
* Card2 should have a different listId that corresponds to its swimlane.
|
||||
*/
|
||||
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
|
||||
class FixMissingListsMigration {
|
||||
constructor() {
|
||||
this.name = 'fix-missing-lists';
|
||||
this.version = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if migration is needed for a board
|
||||
*/
|
||||
needsMigration(boardId) {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) return false;
|
||||
|
||||
// Check if board has already been processed
|
||||
if (board.fixMissingListsCompleted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if there are cards with mismatched listId/swimlaneId
|
||||
const cards = ReactiveCache.getCards({ boardId });
|
||||
const lists = ReactiveCache.getLists({ boardId });
|
||||
|
||||
// Create a map of listId -> swimlaneId for existing lists
|
||||
const listSwimlaneMap = new Map();
|
||||
lists.forEach(list => {
|
||||
listSwimlaneMap.set(list._id, list.swimlaneId || '');
|
||||
});
|
||||
|
||||
// Check for cards with mismatched listId/swimlaneId
|
||||
for (const card of cards) {
|
||||
const expectedSwimlaneId = listSwimlaneMap.get(card.listId);
|
||||
if (expectedSwimlaneId && expectedSwimlaneId !== card.swimlaneId) {
|
||||
console.log(`Found mismatched card: ${card._id}, listId: ${card.listId}, card swimlaneId: ${card.swimlaneId}, list swimlaneId: ${expectedSwimlaneId}`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error checking if migration is needed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the migration for a board
|
||||
*/
|
||||
async executeMigration(boardId) {
|
||||
try {
|
||||
console.log(`Starting fix missing lists migration for board ${boardId}`);
|
||||
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) {
|
||||
throw new Error(`Board ${boardId} not found`);
|
||||
}
|
||||
|
||||
const cards = ReactiveCache.getCards({ boardId });
|
||||
const lists = ReactiveCache.getLists({ boardId });
|
||||
const swimlanes = ReactiveCache.getSwimlanes({ boardId });
|
||||
|
||||
// Create maps for efficient lookup
|
||||
const listSwimlaneMap = new Map();
|
||||
const swimlaneListsMap = new Map();
|
||||
|
||||
lists.forEach(list => {
|
||||
listSwimlaneMap.set(list._id, list.swimlaneId || '');
|
||||
if (!swimlaneListsMap.has(list.swimlaneId || '')) {
|
||||
swimlaneListsMap.set(list.swimlaneId || '', []);
|
||||
}
|
||||
swimlaneListsMap.get(list.swimlaneId || '').push(list);
|
||||
});
|
||||
|
||||
// Group cards by swimlaneId
|
||||
const cardsBySwimlane = new Map();
|
||||
cards.forEach(card => {
|
||||
if (!cardsBySwimlane.has(card.swimlaneId)) {
|
||||
cardsBySwimlane.set(card.swimlaneId, []);
|
||||
}
|
||||
cardsBySwimlane.get(card.swimlaneId).push(card);
|
||||
});
|
||||
|
||||
let createdLists = 0;
|
||||
let updatedCards = 0;
|
||||
|
||||
// Process each swimlane
|
||||
for (const [swimlaneId, swimlaneCards] of cardsBySwimlane) {
|
||||
if (!swimlaneId) continue;
|
||||
|
||||
// Get existing lists for this swimlane
|
||||
const existingLists = swimlaneListsMap.get(swimlaneId) || [];
|
||||
const existingListTitles = new Set(existingLists.map(list => list.title));
|
||||
|
||||
// Group cards by their current listId
|
||||
const cardsByListId = new Map();
|
||||
swimlaneCards.forEach(card => {
|
||||
if (!cardsByListId.has(card.listId)) {
|
||||
cardsByListId.set(card.listId, []);
|
||||
}
|
||||
cardsByListId.get(card.listId).push(card);
|
||||
});
|
||||
|
||||
// For each listId used by cards in this swimlane
|
||||
for (const [listId, cardsInList] of cardsByListId) {
|
||||
const originalList = lists.find(l => l._id === listId);
|
||||
if (!originalList) continue;
|
||||
|
||||
// Check if this list's swimlaneId matches the card's swimlaneId
|
||||
const listSwimlaneId = listSwimlaneMap.get(listId);
|
||||
if (listSwimlaneId === swimlaneId) {
|
||||
// List is already correctly assigned to this swimlane
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we already have a list with the same title in this swimlane
|
||||
let targetList = existingLists.find(list => list.title === originalList.title);
|
||||
|
||||
if (!targetList) {
|
||||
// Create a new list for this swimlane
|
||||
const newListData = {
|
||||
title: originalList.title,
|
||||
boardId: boardId,
|
||||
swimlaneId: swimlaneId,
|
||||
sort: originalList.sort || 0,
|
||||
archived: originalList.archived || false,
|
||||
createdAt: new Date(),
|
||||
modifiedAt: new Date(),
|
||||
type: originalList.type || 'list'
|
||||
};
|
||||
|
||||
// Copy other properties if they exist
|
||||
if (originalList.color) newListData.color = originalList.color;
|
||||
if (originalList.wipLimit) newListData.wipLimit = originalList.wipLimit;
|
||||
if (originalList.wipLimitEnabled) newListData.wipLimitEnabled = originalList.wipLimitEnabled;
|
||||
if (originalList.wipLimitSoft) newListData.wipLimitSoft = originalList.wipLimitSoft;
|
||||
if (originalList.starred) newListData.starred = originalList.starred;
|
||||
if (originalList.collapsed) newListData.collapsed = originalList.collapsed;
|
||||
|
||||
// Insert the new list
|
||||
const newListId = ReactiveCache.getCollection('lists').insert(newListData);
|
||||
targetList = { _id: newListId, ...newListData };
|
||||
createdLists++;
|
||||
|
||||
console.log(`Created new list "${originalList.title}" for swimlane ${swimlaneId}`);
|
||||
}
|
||||
|
||||
// Update all cards in this group to use the correct listId
|
||||
for (const card of cardsInList) {
|
||||
ReactiveCache.getCollection('cards').update(card._id, {
|
||||
$set: {
|
||||
listId: targetList._id,
|
||||
modifiedAt: new Date()
|
||||
}
|
||||
});
|
||||
updatedCards++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark board as processed
|
||||
ReactiveCache.getCollection('boards').update(boardId, {
|
||||
$set: {
|
||||
fixMissingListsCompleted: true,
|
||||
fixMissingListsCompletedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Fix missing lists migration completed for board ${boardId}: created ${createdLists} lists, updated ${updatedCards} cards`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
createdLists,
|
||||
updatedCards
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error executing fix missing lists migration for board ${boardId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get migration status for a board
|
||||
*/
|
||||
getMigrationStatus(boardId) {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) {
|
||||
return { status: 'board_not_found' };
|
||||
}
|
||||
|
||||
if (board.fixMissingListsCompleted) {
|
||||
return {
|
||||
status: 'completed',
|
||||
completedAt: board.fixMissingListsCompletedAt
|
||||
};
|
||||
}
|
||||
|
||||
const needsMigration = this.needsMigration(boardId);
|
||||
return {
|
||||
status: needsMigration ? 'needed' : 'not_needed'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error getting migration status:', error);
|
||||
return { status: 'error', error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const fixMissingListsMigration = new FixMissingListsMigration();
|
||||
|
||||
// Meteor methods
|
||||
Meteor.methods({
|
||||
'fixMissingListsMigration.check'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
return fixMissingListsMigration.getMigrationStatus(boardId);
|
||||
},
|
||||
|
||||
'fixMissingListsMigration.execute'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
return fixMissingListsMigration.executeMigration(boardId);
|
||||
},
|
||||
|
||||
'fixMissingListsMigration.needsMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
return fixMissingListsMigration.needsMigration(boardId);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue