mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 15:30:13 +01:00
259 lines
7.9 KiB
JavaScript
259 lines
7.9 KiB
JavaScript
/**
|
|
* Restore Lost Cards Migration
|
|
*
|
|
* Finds and restores cards and lists that have missing swimlaneId, listId, or are orphaned.
|
|
* Creates a "Lost Cards" swimlane and restores visibility of lost items.
|
|
* Only processes non-archived items.
|
|
*/
|
|
|
|
import { Meteor } from 'meteor/meteor';
|
|
import { check } from 'meteor/check';
|
|
import { ReactiveCache } from '/imports/reactiveCache';
|
|
import { TAPi18n } from '/imports/i18n';
|
|
import Boards from '/models/boards';
|
|
import Lists from '/models/lists';
|
|
import Cards from '/models/cards';
|
|
import Swimlanes from '/models/swimlanes';
|
|
|
|
class RestoreLostCardsMigration {
|
|
constructor() {
|
|
this.name = 'restoreLostCards';
|
|
this.version = 1;
|
|
}
|
|
|
|
/**
|
|
* Check if migration is needed for a board
|
|
*/
|
|
needsMigration(boardId) {
|
|
try {
|
|
const cards = ReactiveCache.getCards({ boardId, archived: false });
|
|
const lists = ReactiveCache.getLists({ boardId, archived: false });
|
|
|
|
// Check for cards missing swimlaneId or listId
|
|
const lostCards = cards.filter(card => !card.swimlaneId || !card.listId);
|
|
if (lostCards.length > 0) {
|
|
return true;
|
|
}
|
|
|
|
// Check for lists missing swimlaneId
|
|
const lostLists = lists.filter(list => !list.swimlaneId);
|
|
if (lostLists.length > 0) {
|
|
return true;
|
|
}
|
|
|
|
// Check for orphaned cards (cards whose list doesn't exist)
|
|
for (const card of cards) {
|
|
if (card.listId) {
|
|
const listExists = lists.some(list => list._id === card.listId);
|
|
if (!listExists) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
} catch (error) {
|
|
console.error('Error checking if restoreLostCards migration is needed:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute the migration
|
|
*/
|
|
async executeMigration(boardId) {
|
|
try {
|
|
const results = {
|
|
lostCardsSwimlaneCreated: false,
|
|
cardsRestored: 0,
|
|
listsRestored: 0,
|
|
errors: []
|
|
};
|
|
|
|
const board = ReactiveCache.getBoard(boardId);
|
|
if (!board) {
|
|
throw new Error('Board not found');
|
|
}
|
|
|
|
// Get all non-archived items
|
|
const cards = ReactiveCache.getCards({ boardId, archived: false });
|
|
const lists = ReactiveCache.getLists({ boardId, archived: false });
|
|
const swimlanes = ReactiveCache.getSwimlanes({ boardId, archived: false });
|
|
|
|
// Detect items to restore BEFORE creating anything
|
|
const lostLists = lists.filter(list => !list.swimlaneId);
|
|
const lostCards = cards.filter(card => !card.swimlaneId || !card.listId);
|
|
const orphanedCards = cards.filter(card => card.listId && !lists.some(list => list._id === card.listId));
|
|
|
|
const hasCardsWork = lostCards.length > 0 || orphanedCards.length > 0;
|
|
const hasListsWork = lostLists.length > 0;
|
|
const hasAnyWork = hasCardsWork || hasListsWork;
|
|
|
|
if (!hasAnyWork) {
|
|
// Nothing to restore; do not create swimlane or list
|
|
return {
|
|
success: true,
|
|
changes: [
|
|
'No lost swimlanes, lists, or cards to restore'
|
|
],
|
|
results: {
|
|
lostCardsSwimlaneCreated: false,
|
|
cardsRestored: 0,
|
|
listsRestored: 0
|
|
}
|
|
};
|
|
}
|
|
|
|
// Find or create "Lost Cards" swimlane (only if there is actual work)
|
|
let lostCardsSwimlane = swimlanes.find(s => s.title === TAPi18n.__('lost-cards'));
|
|
if (!lostCardsSwimlane) {
|
|
const swimlaneId = Swimlanes.insert({
|
|
title: TAPi18n.__('lost-cards'),
|
|
boardId: boardId,
|
|
sort: 999999, // Put at the end
|
|
color: 'red',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
archived: false
|
|
});
|
|
lostCardsSwimlane = ReactiveCache.getSwimlane(swimlaneId);
|
|
results.lostCardsSwimlaneCreated = true;
|
|
if (process.env.DEBUG === 'true') {
|
|
console.log(`Created "Lost Cards" swimlane for board ${boardId}`);
|
|
}
|
|
}
|
|
|
|
// Restore lost lists (lists without swimlaneId)
|
|
if (hasListsWork) {
|
|
for (const list of lostLists) {
|
|
Lists.update(list._id, {
|
|
$set: {
|
|
swimlaneId: lostCardsSwimlane._id,
|
|
updatedAt: new Date()
|
|
}
|
|
});
|
|
results.listsRestored++;
|
|
if (process.env.DEBUG === 'true') {
|
|
console.log(`Restored lost list: ${list.title}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create default list only if we need to move cards
|
|
let defaultList = null;
|
|
if (hasCardsWork) {
|
|
defaultList = lists.find(l =>
|
|
l.swimlaneId === lostCardsSwimlane._id &&
|
|
l.title === TAPi18n.__('lost-cards-list')
|
|
);
|
|
if (!defaultList) {
|
|
const listId = Lists.insert({
|
|
title: TAPi18n.__('lost-cards-list'),
|
|
boardId: boardId,
|
|
swimlaneId: lostCardsSwimlane._id,
|
|
sort: 0,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
archived: false
|
|
});
|
|
defaultList = ReactiveCache.getList(listId);
|
|
if (process.env.DEBUG === 'true') {
|
|
console.log(`Created default list in Lost Cards swimlane`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Restore cards missing swimlaneId or listId
|
|
if (hasCardsWork) {
|
|
for (const card of lostCards) {
|
|
const updateFields = { updatedAt: new Date() };
|
|
if (!card.swimlaneId) updateFields.swimlaneId = lostCardsSwimlane._id;
|
|
if (!card.listId) updateFields.listId = defaultList._id;
|
|
Cards.update(card._id, { $set: updateFields });
|
|
results.cardsRestored++;
|
|
if (process.env.DEBUG === 'true') {
|
|
console.log(`Restored lost card: ${card.title}`);
|
|
}
|
|
}
|
|
|
|
// Restore orphaned cards (cards whose list doesn't exist)
|
|
for (const card of orphanedCards) {
|
|
Cards.update(card._id, {
|
|
$set: {
|
|
listId: defaultList._id,
|
|
swimlaneId: lostCardsSwimlane._id,
|
|
updatedAt: new Date()
|
|
}
|
|
});
|
|
results.cardsRestored++;
|
|
if (process.env.DEBUG === 'true') {
|
|
console.log(`Restored orphaned card: ${card.title}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
changes: [
|
|
results.lostCardsSwimlaneCreated ? 'Created "Lost Cards" swimlane' : 'Using existing "Lost Cards" swimlane',
|
|
`Restored ${results.listsRestored} lost lists`,
|
|
`Restored ${results.cardsRestored} lost cards`
|
|
],
|
|
results
|
|
};
|
|
} catch (error) {
|
|
console.error('Error executing restoreLostCards migration:', error);
|
|
return {
|
|
success: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
const restoreLostCardsMigration = new RestoreLostCardsMigration();
|
|
|
|
// Register Meteor methods
|
|
Meteor.methods({
|
|
'restoreLostCards.needsMigration'(boardId) {
|
|
check(boardId, String);
|
|
|
|
if (!this.userId) {
|
|
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
|
}
|
|
|
|
return restoreLostCardsMigration.needsMigration(boardId);
|
|
},
|
|
|
|
'restoreLostCards.execute'(boardId) {
|
|
check(boardId, String);
|
|
|
|
if (!this.userId) {
|
|
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
|
}
|
|
|
|
// Check if user is board admin
|
|
const board = ReactiveCache.getBoard(boardId);
|
|
if (!board) {
|
|
throw new Meteor.Error('board-not-found', 'Board not found');
|
|
}
|
|
|
|
const user = ReactiveCache.getUser(this.userId);
|
|
if (!user) {
|
|
throw new Meteor.Error('user-not-found', 'User not found');
|
|
}
|
|
|
|
// Only board admins can run migrations
|
|
const isBoardAdmin = board.members && board.members.some(
|
|
member => member.userId === this.userId && member.isAdmin
|
|
);
|
|
|
|
if (!isBoardAdmin && !user.isAdmin) {
|
|
throw new Meteor.Error('not-authorized', 'Only board administrators can run migrations');
|
|
}
|
|
|
|
return restoreLostCardsMigration.executeMigration(boardId);
|
|
}
|
|
});
|
|
|
|
export default restoreLostCardsMigration;
|