mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 07:20:12 +01:00
203 lines
5.8 KiB
JavaScript
203 lines
5.8 KiB
JavaScript
/**
|
|
* Board Conversion Service
|
|
* Handles conversion of boards from old database structure to new structure
|
|
* without running migrations that could cause downtime
|
|
*/
|
|
|
|
import { ReactiveVar } from 'meteor/reactive-var';
|
|
import { ReactiveCache } from '/imports/reactiveCache';
|
|
|
|
// Reactive variables for conversion progress
|
|
export const conversionProgress = new ReactiveVar(0);
|
|
export const conversionStatus = new ReactiveVar('');
|
|
export const conversionEstimatedTime = new ReactiveVar('');
|
|
export const isConverting = new ReactiveVar(false);
|
|
|
|
class BoardConverter {
|
|
constructor() {
|
|
this.conversionCache = new Map(); // Cache converted board IDs
|
|
}
|
|
|
|
/**
|
|
* Check if a board needs conversion
|
|
* @param {string} boardId - The board ID to check
|
|
* @returns {boolean} - True if board needs conversion
|
|
*/
|
|
needsConversion(boardId) {
|
|
if (this.conversionCache.has(boardId)) {
|
|
return false; // Already converted
|
|
}
|
|
|
|
try {
|
|
const board = ReactiveCache.getBoard(boardId);
|
|
if (!board) return false;
|
|
|
|
// Check if any lists in this board don't have swimlaneId
|
|
const lists = ReactiveCache.getLists({
|
|
boardId: boardId,
|
|
$or: [
|
|
{ swimlaneId: { $exists: false } },
|
|
{ swimlaneId: '' },
|
|
{ swimlaneId: null }
|
|
]
|
|
});
|
|
|
|
return lists.length > 0;
|
|
} catch (error) {
|
|
console.error('Error checking if board needs conversion:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert a board from old structure to new structure
|
|
* @param {string} boardId - The board ID to convert
|
|
* @returns {Promise<boolean>} - True if conversion was successful
|
|
*/
|
|
async convertBoard(boardId) {
|
|
if (this.conversionCache.has(boardId)) {
|
|
return true; // Already converted
|
|
}
|
|
|
|
isConverting.set(true);
|
|
conversionProgress.set(0);
|
|
conversionStatus.set('Starting board conversion...');
|
|
|
|
try {
|
|
const board = ReactiveCache.getBoard(boardId);
|
|
if (!board) {
|
|
throw new Error('Board not found');
|
|
}
|
|
|
|
// Get the default swimlane for this board
|
|
const defaultSwimlane = board.getDefaultSwimline();
|
|
if (!defaultSwimlane) {
|
|
throw new Error('No default swimlane found for board');
|
|
}
|
|
|
|
// Get all lists that need conversion
|
|
const listsToConvert = ReactiveCache.getLists({
|
|
boardId: boardId,
|
|
$or: [
|
|
{ swimlaneId: { $exists: false } },
|
|
{ swimlaneId: '' },
|
|
{ swimlaneId: null }
|
|
]
|
|
});
|
|
|
|
if (listsToConvert.length === 0) {
|
|
this.conversionCache.set(boardId, true);
|
|
isConverting.set(false);
|
|
return true;
|
|
}
|
|
|
|
conversionStatus.set(`Converting ${listsToConvert.length} lists...`);
|
|
|
|
const startTime = Date.now();
|
|
const totalLists = listsToConvert.length;
|
|
let convertedLists = 0;
|
|
|
|
// Convert lists in batches to avoid blocking the UI
|
|
const batchSize = 10;
|
|
for (let i = 0; i < listsToConvert.length; i += batchSize) {
|
|
const batch = listsToConvert.slice(i, i + batchSize);
|
|
|
|
// Process batch
|
|
await this.processBatch(batch, defaultSwimlane._id);
|
|
|
|
convertedLists += batch.length;
|
|
const progress = Math.round((convertedLists / totalLists) * 100);
|
|
conversionProgress.set(progress);
|
|
|
|
// Calculate estimated time remaining
|
|
const elapsed = Date.now() - startTime;
|
|
const rate = convertedLists / elapsed; // lists per millisecond
|
|
const remaining = totalLists - convertedLists;
|
|
const estimatedMs = remaining / rate;
|
|
|
|
conversionStatus.set(`Converting list ${convertedLists} of ${totalLists}...`);
|
|
conversionEstimatedTime.set(this.formatTime(estimatedMs));
|
|
|
|
// Allow UI to update
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
}
|
|
|
|
// Mark as converted
|
|
this.conversionCache.set(boardId, true);
|
|
|
|
conversionStatus.set('Board conversion completed!');
|
|
conversionProgress.set(100);
|
|
|
|
// Clear status after a delay
|
|
setTimeout(() => {
|
|
isConverting.set(false);
|
|
conversionStatus.set('');
|
|
conversionProgress.set(0);
|
|
conversionEstimatedTime.set('');
|
|
}, 2000);
|
|
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.error('Error converting board:', error);
|
|
conversionStatus.set(`Conversion failed: ${error.message}`);
|
|
isConverting.set(false);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process a batch of lists for conversion
|
|
* @param {Array} batch - Array of lists to convert
|
|
* @param {string} defaultSwimlaneId - Default swimlane ID
|
|
*/
|
|
async processBatch(batch, defaultSwimlaneId) {
|
|
const updates = batch.map(list => ({
|
|
_id: list._id,
|
|
swimlaneId: defaultSwimlaneId
|
|
}));
|
|
|
|
// Update lists in batch
|
|
updates.forEach(update => {
|
|
ReactiveCache.getCollection('lists').update(update._id, {
|
|
$set: { swimlaneId: update.swimlaneId }
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Format time in milliseconds to human readable format
|
|
* @param {number} ms - Time in milliseconds
|
|
* @returns {string} - Formatted time string
|
|
*/
|
|
formatTime(ms) {
|
|
if (ms < 1000) {
|
|
return `${Math.round(ms)}ms`;
|
|
}
|
|
|
|
const seconds = Math.floor(ms / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const hours = Math.floor(minutes / 60);
|
|
|
|
if (hours > 0) {
|
|
const remainingMinutes = minutes % 60;
|
|
const remainingSeconds = seconds % 60;
|
|
return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`;
|
|
} else if (minutes > 0) {
|
|
const remainingSeconds = seconds % 60;
|
|
return `${minutes}m ${remainingSeconds}s`;
|
|
} else {
|
|
return `${seconds}s`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear conversion cache (useful for testing)
|
|
*/
|
|
clearCache() {
|
|
this.conversionCache.clear();
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const boardConverter = new BoardConverter();
|