mirror of
https://github.com/wekan/wekan.git
synced 2026-02-27 10:24:07 +01:00
Merge branch 'main' into feature/reactive-cache-async-migration
This commit is contained in:
commit
5212f3beb3
328 changed files with 15124 additions and 3392 deletions
|
|
@ -1,14 +1,14 @@
|
|||
/**
|
||||
* Comprehensive Board Migration System
|
||||
*
|
||||
*
|
||||
* This migration handles all database structure changes from previous Wekan versions
|
||||
* to the current per-swimlane lists structure. It ensures:
|
||||
*
|
||||
*
|
||||
* 1. All cards are visible with proper swimlaneId and listId
|
||||
* 2. Lists are per-swimlane (no shared lists across swimlanes)
|
||||
* 3. No empty lists are created
|
||||
* 4. Handles various database structure versions from git history
|
||||
*
|
||||
*
|
||||
* Supported versions and their database structures:
|
||||
* - v7.94 and earlier: Shared lists across all swimlanes
|
||||
* - v8.00-v8.02: Transition period with mixed structures
|
||||
|
|
@ -178,7 +178,7 @@ class ComprehensiveBoardMigration {
|
|||
const updateProgress = (stepName, stepProgress, stepStatus, stepDetails = null) => {
|
||||
currentStep++;
|
||||
const overallProgress = Math.round((currentStep / totalSteps) * 100);
|
||||
|
||||
|
||||
const progressData = {
|
||||
overallProgress,
|
||||
currentStep: currentStep,
|
||||
|
|
@ -206,7 +206,7 @@ class ComprehensiveBoardMigration {
|
|||
issuesFound: results.steps.analyze.issueCount,
|
||||
needsMigration: results.steps.analyze.needsMigration
|
||||
});
|
||||
|
||||
|
||||
// Step 2: Fix orphaned cards
|
||||
updateProgress('fix_orphaned_cards', 0, 'Fixing orphaned cards...');
|
||||
results.steps.fixOrphanedCards = await this.fixOrphanedCards(boardId, (progress, status) => {
|
||||
|
|
@ -323,7 +323,7 @@ class ComprehensiveBoardMigration {
|
|||
if (!card.listId) {
|
||||
// Find or create a default list for this swimlane
|
||||
const swimlaneId = updates.swimlaneId || card.swimlaneId;
|
||||
let defaultList = lists.find(list =>
|
||||
let defaultList = lists.find(list =>
|
||||
list.swimlaneId === swimlaneId && list.title === 'Default'
|
||||
);
|
||||
|
||||
|
|
@ -426,7 +426,7 @@ class ComprehensiveBoardMigration {
|
|||
|
||||
// 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 = {
|
||||
|
|
@ -508,12 +508,12 @@ class ComprehensiveBoardMigration {
|
|||
|
||||
for (const list of lists) {
|
||||
const listCards = cards.filter(card => card.listId === list._id);
|
||||
|
||||
|
||||
if (listCards.length === 0) {
|
||||
// Remove empty list
|
||||
Lists.remove(list._id);
|
||||
listsRemoved++;
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Removed empty list: ${list.title} (${list._id})`);
|
||||
}
|
||||
|
|
@ -563,7 +563,7 @@ class ComprehensiveBoardMigration {
|
|||
const avatarUrl = user.profile.avatarUrl;
|
||||
let needsUpdate = false;
|
||||
let cleanUrl = avatarUrl;
|
||||
|
||||
|
||||
// Check if URL has problematic parameters
|
||||
if (avatarUrl.includes('auth=false') || avatarUrl.includes('brokenIsFine=true')) {
|
||||
// Remove problematic parameters
|
||||
|
|
@ -573,13 +573,13 @@ class ComprehensiveBoardMigration {
|
|||
cleanUrl = cleanUrl.replace(/\?$/g, '');
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
// Check if URL is using old CollectionFS format
|
||||
if (avatarUrl.includes('/cfs/files/avatars/')) {
|
||||
cleanUrl = cleanUrl.replace('/cfs/files/avatars/', '/cdn/storage/avatars/');
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
// Check if URL is missing the /cdn/storage/avatars/ prefix
|
||||
if (avatarUrl.includes('avatars/') && !avatarUrl.includes('/cdn/storage/avatars/') && !avatarUrl.includes('/cfs/files/avatars/')) {
|
||||
// This might be a relative URL, make it absolute
|
||||
|
|
@ -588,7 +588,7 @@ class ComprehensiveBoardMigration {
|
|||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (needsUpdate) {
|
||||
// Update user's avatar URL
|
||||
Users.update(user._id, {
|
||||
|
|
@ -597,7 +597,7 @@ class ComprehensiveBoardMigration {
|
|||
modifiedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
avatarsFixed++;
|
||||
}
|
||||
}
|
||||
|
|
@ -619,7 +619,7 @@ class ComprehensiveBoardMigration {
|
|||
const attachmentUrl = attachment.url;
|
||||
let needsUpdate = false;
|
||||
let cleanUrl = attachmentUrl;
|
||||
|
||||
|
||||
// Check if URL has problematic parameters
|
||||
if (attachmentUrl.includes('auth=false') || attachmentUrl.includes('brokenIsFine=true')) {
|
||||
// Remove problematic parameters
|
||||
|
|
@ -629,26 +629,26 @@ class ComprehensiveBoardMigration {
|
|||
cleanUrl = cleanUrl.replace(/\?$/g, '');
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
// Check if URL is using old CollectionFS format
|
||||
if (attachmentUrl.includes('/cfs/files/attachments/')) {
|
||||
cleanUrl = cleanUrl.replace('/cfs/files/attachments/', '/cdn/storage/attachments/');
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
// Check if URL has /original/ path that should be removed
|
||||
if (attachmentUrl.includes('/original/')) {
|
||||
cleanUrl = cleanUrl.replace(/\/original\/[^\/\?#]+/, '');
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
// If we have a file ID, generate a universal URL
|
||||
const fileId = attachment._id;
|
||||
if (fileId && !isUniversalFileUrl(cleanUrl, 'attachment')) {
|
||||
cleanUrl = generateUniversalAttachmentUrl(fileId);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
if (needsUpdate) {
|
||||
// Update attachment URL
|
||||
Attachments.update(attachment._id, {
|
||||
|
|
@ -657,7 +657,7 @@ class ComprehensiveBoardMigration {
|
|||
modifiedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
attachmentsFixed++;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Delete Duplicate Empty Lists Migration
|
||||
*
|
||||
*
|
||||
* Safely deletes empty duplicate lists from a board:
|
||||
* 1. First converts any shared lists to per-swimlane lists
|
||||
* 2. Only deletes per-swimlane lists that:
|
||||
|
|
@ -42,9 +42,9 @@ class DeleteDuplicateEmptyListsMigration {
|
|||
const listCards = cards.filter(card => card.listId === list._id);
|
||||
if (listCards.length === 0) {
|
||||
// Check if there's a duplicate list with the same title that has cards
|
||||
const duplicateListsWithSameTitle = lists.filter(l =>
|
||||
l._id !== list._id &&
|
||||
l.title === list.title &&
|
||||
const duplicateListsWithSameTitle = lists.filter(l =>
|
||||
l._id !== list._id &&
|
||||
l.title === list.title &&
|
||||
l.boardId === boardId
|
||||
);
|
||||
|
||||
|
|
@ -137,8 +137,8 @@ class DeleteDuplicateEmptyListsMigration {
|
|||
|
||||
if (swimlaneCards.length > 0) {
|
||||
// Check if per-swimlane list already exists
|
||||
const existingList = lists.find(l =>
|
||||
l.title === sharedList.title &&
|
||||
const existingList = lists.find(l =>
|
||||
l.title === sharedList.title &&
|
||||
l.swimlaneId === swimlane._id &&
|
||||
l._id !== sharedList._id
|
||||
);
|
||||
|
|
@ -230,9 +230,9 @@ class DeleteDuplicateEmptyListsMigration {
|
|||
}
|
||||
|
||||
// Safety check 3: There must be another list with the same title on the same board that has cards
|
||||
const duplicateListsWithSameTitle = lists.filter(l =>
|
||||
l._id !== list._id &&
|
||||
l.title === list.title &&
|
||||
const duplicateListsWithSameTitle = lists.filter(l =>
|
||||
l._id !== list._id &&
|
||||
l.title === list.title &&
|
||||
l.boardId === boardId
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ function getOrCreateRescuedSwimlane(boardId) {
|
|||
});
|
||||
|
||||
rescuedSwimlane = Swimlanes.findOne(swimlaneId);
|
||||
|
||||
|
||||
Activities.insert({
|
||||
userId: 'migration',
|
||||
type: 'swimlane',
|
||||
|
|
@ -164,7 +164,7 @@ function getOrCreateRescuedSwimlane(boardId) {
|
|||
let rescuedCount = 0;
|
||||
|
||||
const allCards = Cards.find({}).fetch();
|
||||
|
||||
|
||||
allCards.forEach(card => {
|
||||
if (!card.swimlaneId) return; // Handled by fixCardsWithoutSwimlaneId
|
||||
|
||||
|
|
@ -173,7 +173,7 @@ function getOrCreateRescuedSwimlane(boardId) {
|
|||
if (!swimlane) {
|
||||
// Orphaned card - swimlane doesn't exist
|
||||
const rescuedSwimlane = getOrCreateRescuedSwimlane(card.boardId);
|
||||
|
||||
|
||||
if (rescuedSwimlane) {
|
||||
Cards.update(card._id, {
|
||||
$set: { swimlaneId: rescuedSwimlane._id },
|
||||
|
|
@ -290,7 +290,7 @@ function getOrCreateRescuedSwimlane(boardId) {
|
|||
);
|
||||
|
||||
console.log(`Migration ${MIGRATION_NAME} completed successfully`);
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
cardsFixed: cardResults.fixedCount,
|
||||
|
|
@ -306,7 +306,7 @@ function getOrCreateRescuedSwimlane(boardId) {
|
|||
// Install validation hooks on startup (always run these for data integrity)
|
||||
Meteor.startup(() => {
|
||||
if (!Meteor.isServer) return;
|
||||
|
||||
|
||||
try {
|
||||
addSwimlaneIdValidationHooks();
|
||||
console.log('SwimlaneId validation hooks installed');
|
||||
|
|
|
|||
|
|
@ -61,17 +61,17 @@ class FixAllFileUrlsMigration {
|
|||
*/
|
||||
hasProblematicUrl(url) {
|
||||
if (!url) return false;
|
||||
|
||||
|
||||
// Check for auth parameters
|
||||
if (url.includes('auth=false') || url.includes('brokenIsFine=true')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check for absolute URLs with domains
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check for ROOT_URL dependencies
|
||||
if (Meteor.isServer && process.env.ROOT_URL) {
|
||||
try {
|
||||
|
|
@ -83,12 +83,12 @@ class FixAllFileUrlsMigration {
|
|||
// Ignore URL parsing errors
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check for non-universal file URLs
|
||||
if (url.includes('/cfs/files/') && !isUniversalFileUrl(url, 'attachment') && !isUniversalFileUrl(url, 'avatar')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ class FixAllFileUrlsMigration {
|
|||
}
|
||||
|
||||
console.log(`Universal file URL migration completed for board ${boardId}. Fixed ${filesFixed} file URLs.`);
|
||||
|
||||
|
||||
return {
|
||||
success: errors.length === 0,
|
||||
filesFixed,
|
||||
|
|
@ -145,12 +145,12 @@ class FixAllFileUrlsMigration {
|
|||
for (const user of users) {
|
||||
if (user.profile && user.profile.avatarUrl) {
|
||||
const avatarUrl = user.profile.avatarUrl;
|
||||
|
||||
|
||||
if (this.hasProblematicUrl(avatarUrl)) {
|
||||
try {
|
||||
// Extract file ID from URL
|
||||
const fileId = extractFileIdFromUrl(avatarUrl, 'avatar');
|
||||
|
||||
|
||||
let cleanUrl;
|
||||
if (fileId) {
|
||||
// Generate universal URL
|
||||
|
|
@ -159,7 +159,7 @@ class FixAllFileUrlsMigration {
|
|||
// Clean existing URL
|
||||
cleanUrl = cleanFileUrl(avatarUrl, 'avatar');
|
||||
}
|
||||
|
||||
|
||||
if (cleanUrl && cleanUrl !== avatarUrl) {
|
||||
// Update user's avatar URL
|
||||
Users.update(user._id, {
|
||||
|
|
@ -168,9 +168,9 @@ class FixAllFileUrlsMigration {
|
|||
modifiedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
avatarsFixed++;
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Fixed avatar URL for user ${user.username}: ${avatarUrl} -> ${cleanUrl}`);
|
||||
}
|
||||
|
|
@ -200,7 +200,7 @@ class FixAllFileUrlsMigration {
|
|||
try {
|
||||
const fileId = attachment._id;
|
||||
const cleanUrl = generateUniversalAttachmentUrl(fileId);
|
||||
|
||||
|
||||
if (cleanUrl && cleanUrl !== attachment.url) {
|
||||
// Update attachment URL
|
||||
Attachments.update(attachment._id, {
|
||||
|
|
@ -209,9 +209,9 @@ class FixAllFileUrlsMigration {
|
|||
modifiedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
attachmentsFixed++;
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Fixed attachment URL: ${attachment.url} -> ${cleanUrl}`);
|
||||
}
|
||||
|
|
@ -239,7 +239,7 @@ class FixAllFileUrlsMigration {
|
|||
try {
|
||||
const fileId = attachment._id || extractFileIdFromUrl(attachment.url, 'attachment');
|
||||
const cleanUrl = fileId ? generateUniversalAttachmentUrl(fileId) : cleanFileUrl(attachment.url, 'attachment');
|
||||
|
||||
|
||||
if (cleanUrl && cleanUrl !== attachment.url) {
|
||||
// Update attachment with fixed URL
|
||||
Attachments.update(attachment._id, {
|
||||
|
|
@ -248,9 +248,9 @@ class FixAllFileUrlsMigration {
|
|||
modifiedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
attachmentsFixed++;
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Fixed attachment URL ${attachment._id}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class FixAvatarUrlsMigration {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ class FixAvatarUrlsMigration {
|
|||
const avatarUrl = user.profile.avatarUrl;
|
||||
let needsUpdate = false;
|
||||
let cleanUrl = avatarUrl;
|
||||
|
||||
|
||||
// Check if URL has problematic parameters
|
||||
if (avatarUrl.includes('auth=false') || avatarUrl.includes('brokenIsFine=true')) {
|
||||
// Remove problematic parameters
|
||||
|
|
@ -75,13 +75,13 @@ class FixAvatarUrlsMigration {
|
|||
cleanUrl = cleanUrl.replace(/\?$/g, '');
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
// Check if URL is using old CollectionFS format
|
||||
if (avatarUrl.includes('/cfs/files/avatars/')) {
|
||||
cleanUrl = cleanUrl.replace('/cfs/files/avatars/', '/cdn/storage/avatars/');
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
// Check if URL is missing the /cdn/storage/avatars/ prefix
|
||||
if (avatarUrl.includes('avatars/') && !avatarUrl.includes('/cdn/storage/avatars/') && !avatarUrl.includes('/cfs/files/avatars/')) {
|
||||
// This might be a relative URL, make it absolute
|
||||
|
|
@ -90,14 +90,14 @@ class FixAvatarUrlsMigration {
|
|||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we have a file ID, generate a universal URL
|
||||
const fileId = extractFileIdFromUrl(avatarUrl, 'avatar');
|
||||
if (fileId && !isUniversalFileUrl(cleanUrl, 'avatar')) {
|
||||
cleanUrl = generateUniversalAvatarUrl(fileId);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
if (needsUpdate) {
|
||||
// Update user's avatar URL
|
||||
Users.update(user._id, {
|
||||
|
|
@ -106,9 +106,9 @@ class FixAvatarUrlsMigration {
|
|||
modifiedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
avatarsFixed++;
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Fixed avatar URL for user ${user.username}: ${avatarUrl} -> ${cleanUrl}`);
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ class FixAvatarUrlsMigration {
|
|||
}
|
||||
|
||||
console.log(`Avatar URL fix migration completed for board ${boardId}. Fixed ${avatarsFixed} avatar URLs.`);
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
avatarsFixed,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ class FixMissingListsMigration {
|
|||
// 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 || '')) {
|
||||
|
|
@ -142,7 +142,7 @@ class FixMissingListsMigration {
|
|||
|
||||
// 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 = {
|
||||
|
|
@ -168,7 +168,7 @@ class FixMissingListsMigration {
|
|||
const newListId = Lists.insert(newListData);
|
||||
targetList = { _id: newListId, ...newListData };
|
||||
createdLists++;
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Created new list "${originalList.title}" for swimlane ${swimlaneId}`);
|
||||
}
|
||||
|
|
@ -198,7 +198,7 @@ class FixMissingListsMigration {
|
|||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Fix missing lists migration completed for board ${boardId}: created ${createdLists} lists, updated ${updatedCards} cards`);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
createdLists,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* Restore All Archived Migration
|
||||
*
|
||||
*
|
||||
* Restores all archived swimlanes, lists, and cards.
|
||||
* If any restored items are missing swimlaneId, listId, or cardId,
|
||||
* If any restored items are missing swimlaneId, listId, or cardId,
|
||||
* creates/assigns proper IDs to make them visible.
|
||||
*/
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ class RestoreAllArchivedMigration {
|
|||
if (!list.swimlaneId) {
|
||||
// Try to find a suitable swimlane or use default
|
||||
let targetSwimlane = activeSwimlanes.find(s => !s.archived);
|
||||
|
||||
|
||||
if (!targetSwimlane) {
|
||||
// No active swimlane found, create default
|
||||
const swimlaneId = Swimlanes.insert({
|
||||
|
|
@ -139,11 +139,11 @@ class RestoreAllArchivedMigration {
|
|||
if (!card.listId) {
|
||||
// Find or create a default list
|
||||
let targetList = allLists.find(l => !l.archived);
|
||||
|
||||
|
||||
if (!targetList) {
|
||||
// No active list found, create one
|
||||
const defaultSwimlane = allSwimlanes.find(s => !s.archived) || allSwimlanes[0];
|
||||
|
||||
|
||||
const listId = Lists.insert({
|
||||
title: TAPi18n.__('default'),
|
||||
boardId: boardId,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* 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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue