mirror of
https://github.com/wekan/wekan.git
synced 2026-01-17 06:55:29 +01:00
Per-User and Board-level data save fixes. Per-User is collapse, width, height. Per-Board is Swimlanes, Lists, Cards etc.
Thanks to xet7 ! Fixes #5997
This commit is contained in:
parent
2e0e1e56b5
commit
414b8dbf41
11 changed files with 2273 additions and 57 deletions
283
server/migrations/ensureValidSwimlaneIds.js
Normal file
283
server/migrations/ensureValidSwimlaneIds.js
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
/**
|
||||
* Migration: Ensure all entities have valid swimlaneId
|
||||
*
|
||||
* This migration ensures that:
|
||||
* 1. All cards have a valid swimlaneId
|
||||
* 2. All lists have a valid swimlaneId (if applicable)
|
||||
* 3. Orphaned entities (without valid swimlaneId) are moved to a "Rescued Data" swimlane
|
||||
*
|
||||
* This is similar to the existing rescue migration but specifically for swimlaneId validation
|
||||
*/
|
||||
|
||||
Meteor.startup(() => {
|
||||
// Only run on server
|
||||
if (!Meteor.isServer) return;
|
||||
|
||||
const MIGRATION_NAME = 'ensure-valid-swimlane-ids';
|
||||
const MIGRATION_VERSION = 1;
|
||||
|
||||
// Check if migration already ran
|
||||
const existingMigration = Migrations.findOne({ name: MIGRATION_NAME });
|
||||
if (existingMigration && existingMigration.version >= MIGRATION_VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Running migration: ${MIGRATION_NAME} v${MIGRATION_VERSION}`);
|
||||
|
||||
/**
|
||||
* Get or create a "Rescued Data" swimlane for a board
|
||||
*/
|
||||
function getOrCreateRescuedSwimlane(boardId) {
|
||||
const board = Boards.findOne(boardId);
|
||||
if (!board) return null;
|
||||
|
||||
// Look for existing rescued data swimlane
|
||||
let rescuedSwimlane = Swimlanes.findOne({
|
||||
boardId,
|
||||
title: { $regex: /rescued.*data/i },
|
||||
});
|
||||
|
||||
if (!rescuedSwimlane) {
|
||||
// Create a new rescued data swimlane
|
||||
const swimlaneId = Swimlanes.insert({
|
||||
title: 'Rescued Data (Missing Swimlane)',
|
||||
boardId,
|
||||
archived: false,
|
||||
sort: 9999999, // Put at the end
|
||||
type: 'swimlane',
|
||||
color: 'red',
|
||||
});
|
||||
|
||||
rescuedSwimlane = Swimlanes.findOne(swimlaneId);
|
||||
|
||||
Activities.insert({
|
||||
userId: 'migration',
|
||||
type: 'swimlane',
|
||||
activityType: 'createSwimlane',
|
||||
boardId,
|
||||
swimlaneId,
|
||||
title: 'Created rescued data swimlane during migration',
|
||||
});
|
||||
}
|
||||
|
||||
return rescuedSwimlane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and fix cards without valid swimlaneId
|
||||
*/
|
||||
function fixCardsWithoutSwimlaneId() {
|
||||
let fixedCount = 0;
|
||||
let rescuedCount = 0;
|
||||
|
||||
const cardsWithoutSwimlane = Cards.find({
|
||||
$or: [
|
||||
{ swimlaneId: { $exists: false } },
|
||||
{ swimlaneId: null },
|
||||
{ swimlaneId: '' },
|
||||
],
|
||||
}).fetch();
|
||||
|
||||
console.log(`Found ${cardsWithoutSwimlane.length} cards without swimlaneId`);
|
||||
|
||||
cardsWithoutSwimlane.forEach(card => {
|
||||
const board = Boards.findOne(card.boardId);
|
||||
if (!board) {
|
||||
console.warn(`Card ${card._id} has invalid boardId: ${card.boardId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to get default swimlane
|
||||
let defaultSwimlane = Swimlanes.findOne({
|
||||
boardId: card.boardId,
|
||||
type: { $ne: 'template-swimlane' },
|
||||
archived: false,
|
||||
}, { sort: { sort: 1 } });
|
||||
|
||||
if (!defaultSwimlane) {
|
||||
// No swimlanes at all - create default
|
||||
const swimlaneId = Swimlanes.insert({
|
||||
title: 'Default',
|
||||
boardId: card.boardId,
|
||||
archived: false,
|
||||
sort: 0,
|
||||
type: 'swimlane',
|
||||
});
|
||||
defaultSwimlane = Swimlanes.findOne(swimlaneId);
|
||||
}
|
||||
|
||||
if (defaultSwimlane) {
|
||||
Cards.update(card._id, {
|
||||
$set: { swimlaneId: defaultSwimlane._id },
|
||||
});
|
||||
fixedCount++;
|
||||
} else {
|
||||
console.warn(`Could not find or create default swimlane for card ${card._id}`);
|
||||
}
|
||||
});
|
||||
|
||||
return { fixedCount, rescuedCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and fix lists without valid swimlaneId
|
||||
*/
|
||||
function fixListsWithoutSwimlaneId() {
|
||||
let fixedCount = 0;
|
||||
|
||||
const listsWithoutSwimlane = Lists.find({
|
||||
$or: [
|
||||
{ swimlaneId: { $exists: false } },
|
||||
{ swimlaneId: null },
|
||||
],
|
||||
}).fetch();
|
||||
|
||||
console.log(`Found ${listsWithoutSwimlane.length} lists without swimlaneId`);
|
||||
|
||||
listsWithoutSwimlane.forEach(list => {
|
||||
// Set to empty string for backward compatibility
|
||||
// (lists can be shared across swimlanes)
|
||||
Lists.update(list._id, {
|
||||
$set: { swimlaneId: '' },
|
||||
});
|
||||
fixedCount++;
|
||||
});
|
||||
|
||||
return { fixedCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and rescue orphaned cards (swimlaneId points to non-existent swimlane)
|
||||
*/
|
||||
function rescueOrphanedCards() {
|
||||
let rescuedCount = 0;
|
||||
|
||||
const allCards = Cards.find({}).fetch();
|
||||
|
||||
allCards.forEach(card => {
|
||||
if (!card.swimlaneId) return; // Handled by fixCardsWithoutSwimlaneId
|
||||
|
||||
// Check if swimlane exists
|
||||
const swimlane = Swimlanes.findOne(card.swimlaneId);
|
||||
if (!swimlane) {
|
||||
// Orphaned card - swimlane doesn't exist
|
||||
const rescuedSwimlane = getOrCreateRescuedSwimlane(card.boardId);
|
||||
|
||||
if (rescuedSwimlane) {
|
||||
Cards.update(card._id, {
|
||||
$set: { swimlaneId: rescuedSwimlane._id },
|
||||
});
|
||||
rescuedCount++;
|
||||
|
||||
Activities.insert({
|
||||
userId: 'migration',
|
||||
type: 'card',
|
||||
activityType: 'moveCard',
|
||||
boardId: card.boardId,
|
||||
cardId: card._id,
|
||||
swimlaneId: rescuedSwimlane._id,
|
||||
listId: card.listId,
|
||||
title: `Rescued card from deleted swimlane`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { rescuedCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure all swimlaneId references are always saved in all operations
|
||||
* This adds a global hook to validate swimlaneId before insert/update
|
||||
*/
|
||||
function addSwimlaneIdValidationHooks() {
|
||||
// Card insert hook
|
||||
Cards.before.insert(function(userId, doc) {
|
||||
if (!doc.swimlaneId) {
|
||||
const board = Boards.findOne(doc.boardId);
|
||||
if (board) {
|
||||
const defaultSwimlane = Swimlanes.findOne({
|
||||
boardId: doc.boardId,
|
||||
type: { $ne: 'template-swimlane' },
|
||||
archived: false,
|
||||
}, { sort: { sort: 1 } });
|
||||
|
||||
if (defaultSwimlane) {
|
||||
doc.swimlaneId = defaultSwimlane._id;
|
||||
} else {
|
||||
console.warn('No default swimlane found for new card, creating one');
|
||||
const swimlaneId = Swimlanes.insert({
|
||||
title: 'Default',
|
||||
boardId: doc.boardId,
|
||||
archived: false,
|
||||
sort: 0,
|
||||
type: 'swimlane',
|
||||
});
|
||||
doc.swimlaneId = swimlaneId;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Card update hook - ensure swimlaneId is never removed
|
||||
Cards.before.update(function(userId, doc, fieldNames, modifier) {
|
||||
if (modifier.$unset && modifier.$unset.swimlaneId) {
|
||||
delete modifier.$unset.swimlaneId;
|
||||
console.warn('Prevented removal of swimlaneId from card', doc._id);
|
||||
}
|
||||
|
||||
if (modifier.$set && modifier.$set.swimlaneId === null) {
|
||||
const defaultSwimlane = Swimlanes.findOne({
|
||||
boardId: doc.boardId,
|
||||
type: { $ne: 'template-swimlane' },
|
||||
archived: false,
|
||||
}, { sort: { sort: 1 } });
|
||||
|
||||
if (defaultSwimlane) {
|
||||
modifier.$set.swimlaneId = defaultSwimlane._id;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Run all fix operations
|
||||
const cardResults = fixCardsWithoutSwimlaneId();
|
||||
const listResults = fixListsWithoutSwimlaneId();
|
||||
const rescueResults = rescueOrphanedCards();
|
||||
|
||||
console.log('Migration results:');
|
||||
console.log(`- Fixed ${cardResults.fixedCount} cards without swimlaneId`);
|
||||
console.log(`- Fixed ${listResults.fixedCount} lists without swimlaneId`);
|
||||
console.log(`- Rescued ${rescueResults.rescuedCount} orphaned cards`);
|
||||
|
||||
// Add validation hooks
|
||||
addSwimlaneIdValidationHooks();
|
||||
|
||||
// Record migration completion
|
||||
Migrations.upsert(
|
||||
{ name: MIGRATION_NAME },
|
||||
{
|
||||
$set: {
|
||||
name: MIGRATION_NAME,
|
||||
version: MIGRATION_VERSION,
|
||||
completedAt: new Date(),
|
||||
results: {
|
||||
cardsFixed: cardResults.fixedCount,
|
||||
listsFixed: listResults.fixedCount,
|
||||
cardsRescued: rescueResults.rescuedCount,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Migration ${MIGRATION_NAME} completed successfully`);
|
||||
} catch (error) {
|
||||
console.error(`Migration ${MIGRATION_NAME} failed:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
// Helper collection to track migrations
|
||||
if (typeof Migrations === 'undefined') {
|
||||
Migrations = new Mongo.Collection('migrations');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue