mirror of
https://github.com/wekan/wekan.git
synced 2026-03-04 04:40:16 +01:00
Reverted New UI Design of WeKan v8.29 and added more fixes and performance improvements.
Thanks to xet7 !
This commit is contained in:
parent
d152d8fc1b
commit
1b8b8d2eef
196 changed files with 17659 additions and 10028 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
|
||||
|
|
@ -66,7 +66,7 @@ class ComprehensiveBoardMigration {
|
|||
*/
|
||||
detectMigrationIssues(boardId) {
|
||||
const issues = [];
|
||||
|
||||
|
||||
try {
|
||||
const cards = ReactiveCache.getCards({ boardId });
|
||||
const lists = ReactiveCache.getLists({ boardId });
|
||||
|
|
@ -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++;
|
||||
}
|
||||
}
|
||||
|
|
@ -677,7 +677,7 @@ class ComprehensiveBoardMigration {
|
|||
}
|
||||
|
||||
if (board.comprehensiveMigrationCompleted) {
|
||||
return {
|
||||
return {
|
||||
status: 'completed',
|
||||
completedAt: board.comprehensiveMigrationCompletedAt,
|
||||
results: board.comprehensiveMigrationResults
|
||||
|
|
@ -686,7 +686,7 @@ class ComprehensiveBoardMigration {
|
|||
|
||||
const needsMigration = this.needsMigration(boardId);
|
||||
const issues = this.detectMigrationIssues(boardId);
|
||||
|
||||
|
||||
return {
|
||||
status: needsMigration ? 'needed' : 'not_needed',
|
||||
issues,
|
||||
|
|
@ -707,54 +707,54 @@ export const comprehensiveBoardMigration = new ComprehensiveBoardMigration();
|
|||
Meteor.methods({
|
||||
'comprehensiveBoardMigration.check'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
return comprehensiveBoardMigration.getMigrationStatus(boardId);
|
||||
},
|
||||
|
||||
'comprehensiveBoardMigration.execute'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) {
|
||||
throw new Meteor.Error('board-not-found');
|
||||
}
|
||||
|
||||
|
||||
const isBoardAdmin = board.hasAdmin(this.userId);
|
||||
const isInstanceAdmin = user && user.isAdmin;
|
||||
|
||||
|
||||
if (!isBoardAdmin && !isInstanceAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be a board admin or instance admin to perform this action.');
|
||||
}
|
||||
|
||||
|
||||
return comprehensiveBoardMigration.executeMigration(boardId);
|
||||
},
|
||||
|
||||
'comprehensiveBoardMigration.needsMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
return comprehensiveBoardMigration.needsMigration(boardId);
|
||||
},
|
||||
|
||||
'comprehensiveBoardMigration.detectIssues'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
return comprehensiveBoardMigration.detectMigrationIssues(boardId);
|
||||
},
|
||||
|
||||
|
|
@ -762,12 +762,12 @@ Meteor.methods({
|
|||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Only instance admins can perform this action.');
|
||||
}
|
||||
|
||||
|
||||
return comprehensiveBoardMigration.fixAvatarUrls();
|
||||
},
|
||||
|
||||
|
|
@ -775,12 +775,12 @@ Meteor.methods({
|
|||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Only instance admins can perform this action.');
|
||||
}
|
||||
|
||||
|
||||
return comprehensiveBoardMigration.fixAttachmentUrls();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ class DeleteDuplicateEmptyListsMigration {
|
|||
const lists = ReactiveCache.getLists({ boardId });
|
||||
const swimlanes = ReactiveCache.getSwimlanes({ boardId, archived: false });
|
||||
const cards = ReactiveCache.getCards({ boardId });
|
||||
|
||||
|
||||
let listsConverted = 0;
|
||||
|
||||
// Find shared lists (lists without swimlaneId)
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -208,7 +208,7 @@ class DeleteDuplicateEmptyListsMigration {
|
|||
async deleteEmptyPerSwimlaneLists(boardId) {
|
||||
const lists = ReactiveCache.getLists({ boardId });
|
||||
const cards = ReactiveCache.getCards({ boardId });
|
||||
|
||||
|
||||
let listsDeleted = 0;
|
||||
|
||||
for (const list of lists) {
|
||||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
@ -321,7 +321,7 @@ const deleteDuplicateEmptyListsMigration = new DeleteDuplicateEmptyListsMigratio
|
|||
Meteor.methods({
|
||||
'deleteDuplicateEmptyLists.needsMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
@ -331,7 +331,7 @@ Meteor.methods({
|
|||
|
||||
'deleteDuplicateEmptyLists.execute'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
@ -361,7 +361,7 @@ Meteor.methods({
|
|||
|
||||
'deleteDuplicateEmptyLists.getStatus'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ class FixAllFileUrlsMigration {
|
|||
if (!board || !board.members) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const memberIds = board.members.map(m => m.userId);
|
||||
|
||||
|
||||
// Check for problematic avatar URLs for board members
|
||||
const users = ReactiveCache.getUsers({ _id: { $in: memberIds } });
|
||||
for (const user of users) {
|
||||
|
|
@ -46,7 +46,7 @@ class FixAllFileUrlsMigration {
|
|||
const cards = ReactiveCache.getCards({ boardId });
|
||||
const cardIds = cards.map(c => c._id);
|
||||
const attachments = ReactiveCache.getAttachments({ cardId: { $in: cardIds } });
|
||||
|
||||
|
||||
for (const attachment of attachments) {
|
||||
if (attachment.url && this.hasProblematicUrl(attachment.url)) {
|
||||
return true;
|
||||
|
|
@ -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,
|
||||
|
|
@ -137,7 +137,7 @@ class FixAllFileUrlsMigration {
|
|||
if (!board || !board.members) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
const memberIds = board.members.map(m => m.userId);
|
||||
const users = ReactiveCache.getUsers({ _id: { $in: memberIds } });
|
||||
let avatarsFixed = 0;
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
@ -272,7 +272,7 @@ export const fixAllFileUrlsMigration = new FixAllFileUrlsMigration();
|
|||
Meteor.methods({
|
||||
'fixAllFileUrls.execute'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
@ -296,17 +296,17 @@ Meteor.methods({
|
|||
if (!isBoardAdmin && !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Only board administrators can run migrations');
|
||||
}
|
||||
|
||||
|
||||
return fixAllFileUrlsMigration.execute(boardId);
|
||||
},
|
||||
|
||||
'fixAllFileUrls.needsMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
||||
|
||||
return fixAllFileUrlsMigration.needsMigration(boardId);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ class FixAvatarUrlsMigration {
|
|||
if (!board || !board.members) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const memberIds = board.members.map(m => m.userId);
|
||||
const users = ReactiveCache.getUsers({ _id: { $in: memberIds } });
|
||||
|
||||
|
||||
for (const user of users) {
|
||||
if (user.profile && user.profile.avatarUrl) {
|
||||
const avatarUrl = user.profile.avatarUrl;
|
||||
|
|
@ -37,7 +37,7 @@ class FixAvatarUrlsMigration {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ class FixAvatarUrlsMigration {
|
|||
error: 'Board not found or has no members'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const memberIds = board.members.map(m => m.userId);
|
||||
const users = ReactiveCache.getUsers({ _id: { $in: memberIds } });
|
||||
let avatarsFixed = 0;
|
||||
|
|
@ -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,
|
||||
|
|
@ -133,7 +133,7 @@ export const fixAvatarUrlsMigration = new FixAvatarUrlsMigration();
|
|||
Meteor.methods({
|
||||
'fixAvatarUrls.execute'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
@ -157,17 +157,17 @@ Meteor.methods({
|
|||
if (!isBoardAdmin && !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Only board administrators can run migrations');
|
||||
}
|
||||
|
||||
|
||||
return fixAvatarUrlsMigration.execute(boardId);
|
||||
},
|
||||
|
||||
'fixAvatarUrls.needsMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
||||
|
||||
return fixAvatarUrlsMigration.needsMigration(boardId);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ class FixMissingListsMigration {
|
|||
// 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 => {
|
||||
|
|
@ -77,7 +77,7 @@ class FixMissingListsMigration {
|
|||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Starting fix missing lists migration for board ${boardId}`);
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) {
|
||||
throw new Error(`Board ${boardId} not found`);
|
||||
|
|
@ -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,
|
||||
|
|
@ -222,7 +222,7 @@ class FixMissingListsMigration {
|
|||
}
|
||||
|
||||
if (board.fixMissingListsCompleted) {
|
||||
return {
|
||||
return {
|
||||
status: 'completed',
|
||||
completedAt: board.fixMissingListsCompletedAt
|
||||
};
|
||||
|
|
@ -247,31 +247,31 @@ export const fixMissingListsMigration = new FixMissingListsMigration();
|
|||
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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -224,7 +224,7 @@ const restoreAllArchivedMigration = new RestoreAllArchivedMigration();
|
|||
Meteor.methods({
|
||||
'restoreAllArchived.needsMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
@ -234,7 +234,7 @@ Meteor.methods({
|
|||
|
||||
'restoreAllArchived.execute'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -217,7 +217,7 @@ const restoreLostCardsMigration = new RestoreLostCardsMigration();
|
|||
Meteor.methods({
|
||||
'restoreLostCards.needsMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
@ -227,7 +227,7 @@ Meteor.methods({
|
|||
|
||||
'restoreLostCards.execute'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue