From b4b598f542d0cefc5f2d5d6c7286f0a312cf6a55 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Thu, 23 Oct 2025 04:03:52 +0300 Subject: [PATCH 01/17] Fix board reloading page every second. Thanks to xet7 ! --- client/components/boards/boardBody.js | 81 ++++++++++++++++++--------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index f1f6b9a5ae..5eb317b9bf 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -17,6 +17,8 @@ BlazeComponent.extendComponent({ this.isConverting = new ReactiveVar(false); this.isMigrating = new ReactiveVar(false); this._swimlaneCreated = new Set(); // Track boards where we've created swimlanes + this._boardProcessed = false; // Track if board has been processed + this._lastProcessedBoardId = null; // Track last processed board ID // The pattern we use to manually handle data loading is described here: // https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager @@ -28,21 +30,33 @@ BlazeComponent.extendComponent({ const handle = subManager.subscribe('board', currentBoardId, false); - Tracker.nonreactive(() => { - Tracker.autorun(() => { - if (handle.ready()) { + // Use a separate autorun for subscription ready state to avoid reactive loops + this.subscriptionReadyAutorun = Tracker.autorun(() => { + if (handle.ready()) { + // Only run conversion/migration logic once per board + if (!this._boardProcessed || this._lastProcessedBoardId !== currentBoardId) { + this._boardProcessed = true; + this._lastProcessedBoardId = currentBoardId; + // Ensure default swimlane exists (only once per board) this.ensureDefaultSwimlane(currentBoardId); // Check if board needs conversion this.checkAndConvertBoard(currentBoardId); - } else { - this.isBoardReady.set(false); } - }); + } else { + this.isBoardReady.set(false); + } }); }); }, + onDestroyed() { + // Clean up the subscription ready autorun to prevent memory leaks + if (this.subscriptionReadyAutorun) { + this.subscriptionReadyAutorun.stop(); + } + }, + ensureDefaultSwimlane(boardId) { // Only create swimlane once per board if (this._swimlaneCreated.has(boardId)) { @@ -441,39 +455,50 @@ BlazeComponent.extendComponent({ this._isDragging = false; // Used to set the overlay this.mouseHasEnterCardDetails = false; + this._sortFieldsFixed = new Set(); // Track which boards have had sort fields fixed // fix swimlanes sort field if there are null values const currentBoardData = Utils.getCurrentBoard(); if (currentBoardData && Swimlanes) { - const nullSortSwimlanes = currentBoardData.nullSortSwimlanes(); - if (nullSortSwimlanes.length > 0) { - const swimlanes = currentBoardData.swimlanes(); - let count = 0; - swimlanes.forEach(s => { - Swimlanes.update(s._id, { - $set: { - sort: count, - }, + const boardId = currentBoardData._id; + // Only fix sort fields once per board to prevent reactive loops + if (!this._sortFieldsFixed.has(`swimlanes-${boardId}`)) { + const nullSortSwimlanes = currentBoardData.nullSortSwimlanes(); + if (nullSortSwimlanes.length > 0) { + const swimlanes = currentBoardData.swimlanes(); + let count = 0; + swimlanes.forEach(s => { + Swimlanes.update(s._id, { + $set: { + sort: count, + }, + }); + count += 1; }); - count += 1; - }); + } + this._sortFieldsFixed.add(`swimlanes-${boardId}`); } } // fix lists sort field if there are null values if (currentBoardData && Lists) { - const nullSortLists = currentBoardData.nullSortLists(); - if (nullSortLists.length > 0) { - const lists = currentBoardData.lists(); - let count = 0; - lists.forEach(l => { - Lists.update(l._id, { - $set: { - sort: count, - }, + const boardId = currentBoardData._id; + // Only fix sort fields once per board to prevent reactive loops + if (!this._sortFieldsFixed.has(`lists-${boardId}`)) { + const nullSortLists = currentBoardData.nullSortLists(); + if (nullSortLists.length > 0) { + const lists = currentBoardData.lists(); + let count = 0; + lists.forEach(l => { + Lists.update(l._id, { + $set: { + sort: count, + }, + }); + count += 1; }); - count += 1; - }); + } + this._sortFieldsFixed.add(`lists-${boardId}`); } } }, From 940df024561114a31f4c23e811f29053a1e2ee2d Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Thu, 23 Oct 2025 04:08:49 +0300 Subject: [PATCH 02/17] Updated translations. --- imports/i18n/data/ms-MY.i18n.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/imports/i18n/data/ms-MY.i18n.json b/imports/i18n/data/ms-MY.i18n.json index ede03686cb..2112ab947d 100644 --- a/imports/i18n/data/ms-MY.i18n.json +++ b/imports/i18n/data/ms-MY.i18n.json @@ -89,9 +89,9 @@ "setListWidthPopup-title": "Tetapkan Lebar", "set-list-width": "Tetapkan Lebar", "set-list-width-value": "Tetapkan lebar minimum dan maksimum (piksel)", - "list-width-error-message": "List widths must be integers greater than 100", - "keyboard-shortcuts-enabled": "Keyboard shortcuts enabled. Click to disable.", - "keyboard-shortcuts-disabled": "Keyboard shortcuts disabled. Click to enable.", + "list-width-error-message": "Lebar senarai mestilah integer lebih besar dari 100", + "keyboard-shortcuts-enabled": "Pintasan papan kekunci didayakan. Klik untuk batal.", + "keyboard-shortcuts-disabled": "Pintasan papan kekunci didayakan. Klik untuk batal.", "setSwimlaneHeightPopup-title": "Set Swimlane Height", "set-swimlane-height": "Set Swimlane Height", "set-swimlane-height-value": "Swimlane Height (pixels)", From 0cbc9402f3648c9a33423febcae07b6dca94aab8 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Thu, 23 Oct 2025 04:09:14 +0300 Subject: [PATCH 03/17] v8.14 --- CHANGELOG.md | 9 +++++++++ Dockerfile | 6 +++--- Stackerfile.yml | 2 +- docs/Platforms/Propietary/Windows/Offline.md | 4 ++-- package-lock.json | 2 +- package.json | 2 +- sandstorm-pkgdef.capnp | 4 ++-- snapcraft.yaml | 8 ++++---- 8 files changed, 23 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30a838e0e9..cb1bae4711 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,15 @@ Fixing other platforms In Progress. WeKan 8.00-8.06 had wrong raw database directory setting /var/snap/wekan/common/wekan and some cards were not visible. Those are fixed at WeKan 8.07 where database directory is back to /var/snap/wekan/common and all cards are visible. +# v8.14 2025-10-23 WeKan ® release + +This release fixes the following bugs: + +- [Fix board reloading page every second](https://github.com/wekan/wekan/commit/b4b598f542d0cefc5f2d5d6c7286f0a312cf6a55). + Thanks to xet7. + +Thanks to above GitHub users for their contributions and translators for their translations. + # v8.12 2025-10-23 WeKan ® release This release fixes the following bugs: diff --git a/Dockerfile b/Dockerfile index b53398c748..83582c102e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -249,9 +249,9 @@ cd /home/wekan/app # Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc. #rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy #mv /home/wekan/app_build/bundle /build -wget "https://github.com/wekan/wekan/releases/download/v8.12/wekan-8.12-amd64.zip" -unzip wekan-8.12-amd64.zip -rm wekan-8.12-amd64.zip +wget "https://github.com/wekan/wekan/releases/download/v8.14/wekan-8.14-amd64.zip" +unzip wekan-8.14-amd64.zip +rm wekan-8.14-amd64.zip mv /home/wekan/app/bundle /build # Put back the original tar diff --git a/Stackerfile.yml b/Stackerfile.yml index e472459bcc..567bcc5215 100644 --- a/Stackerfile.yml +++ b/Stackerfile.yml @@ -1,5 +1,5 @@ appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928 -appVersion: "v8.12.0" +appVersion: "v8.14.0" files: userUploads: - README.md diff --git a/docs/Platforms/Propietary/Windows/Offline.md b/docs/Platforms/Propietary/Windows/Offline.md index e4a9865e32..44a35e12aa 100644 --- a/docs/Platforms/Propietary/Windows/Offline.md +++ b/docs/Platforms/Propietary/Windows/Offline.md @@ -10,7 +10,7 @@ This is without container (without Docker or Snap). Right click and download files 1-4: -1. [wekan-8.12-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.12/wekan-8.12-amd64-windows.zip) +1. [wekan-8.14-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.14/wekan-8.14-amd64-windows.zip) 2. [node.exe](https://nodejs.org/dist/latest-v14.x/win-x64/node.exe) @@ -22,7 +22,7 @@ Right click and download files 1-4: 6. Double click `mongodb-windows-x86_64-7.0.25-signed.msi` . In installer, uncheck downloading MongoDB compass. -7. Unzip `wekan-8.12-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files: +7. Unzip `wekan-8.14-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files: ``` bundle (directory) diff --git a/package-lock.json b/package-lock.json index 9d54787511..445be60692 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "wekan", - "version": "v8.12.0", + "version": "v8.14.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5d2d1a430e..f52f128152 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wekan", - "version": "v8.12.0", + "version": "v8.14.0", "description": "Open-Source kanban", "private": true, "repository": { diff --git a/sandstorm-pkgdef.capnp b/sandstorm-pkgdef.capnp index cf9bae85d0..9683133b91 100644 --- a/sandstorm-pkgdef.capnp +++ b/sandstorm-pkgdef.capnp @@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = ( appTitle = (defaultText = "Wekan"), # The name of the app as it is displayed to the user. - appVersion = 812, + appVersion = 814, # Increment this for every release. - appMarketingVersion = (defaultText = "8.12.0~2025-10-23"), + appMarketingVersion = (defaultText = "8.14.0~2025-10-23"), # Human-readable presentation of the app version. minUpgradableAppVersion = 0, diff --git a/snapcraft.yaml b/snapcraft.yaml index c91a57b7f9..a7e4f76f68 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: wekan -version: '8.12' +version: '8.14' base: core24 summary: Open Source kanban description: | @@ -166,9 +166,9 @@ parts: # Cleanup mkdir .build cd .build - wget https://github.com/wekan/wekan/releases/download/v8.12/wekan-8.12-amd64.zip - unzip wekan-8.12-amd64.zip - rm wekan-8.12-amd64.zip + wget https://github.com/wekan/wekan/releases/download/v8.14/wekan-8.14-amd64.zip + unzip wekan-8.14-amd64.zip + rm wekan-8.14-amd64.zip cd .. ##cd .build/bundle ##find . -type d -name '*-garbage*' | xargs rm -rf From 8662c96d1c8d4fa76ce7b31eb06678ad59c3ebe1 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Thu, 23 Oct 2025 04:33:34 +0300 Subject: [PATCH 04/17] Fix drag lists did not work. Thanks to xet7 ! --- client/components/swimlanes/swimlanes.js | 204 +++++++++++++++++++++++ 1 file changed, 204 insertions(+) diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js index cb0eb4c9d9..c1ddcd369b 100644 --- a/client/components/swimlanes/swimlanes.js +++ b/client/components/swimlanes/swimlanes.js @@ -722,6 +722,96 @@ setTimeout(() => { } }, stop(evt, ui) { + // To attribute the new index number, we need to get the DOM element + // of the previous and the following list -- if any. + const prevListDom = ui.item.prev('.js-list').get(0); + const nextListDom = ui.item.next('.js-list').get(0); + const sortIndex = calculateIndex(prevListDom, nextListDom, 1); + + const listDomElement = ui.item.get(0); + if (!listDomElement) { + console.error('List DOM element not found during drag stop'); + return; + } + + let list; + try { + list = Blaze.getData(listDomElement); + } catch (error) { + console.error('Error getting list data:', error); + return; + } + + if (!list) { + console.error('List data not found for element:', listDomElement); + return; + } + + // Detect if the list was dropped in a different swimlane + const targetSwimlaneDom = ui.item.closest('.js-swimlane'); + let targetSwimlaneId = null; + + if (targetSwimlaneDom.length > 0) { + // List was dropped in a swimlane + try { + targetSwimlaneId = targetSwimlaneDom.attr('id').replace('swimlane-', ''); + } catch (error) { + console.error('Error getting target swimlane ID:', error); + return; + } + } else { + // List was dropped in lists view (not swimlanes view) + // In this case, assign to the default swimlane + const currentBoard = ReactiveCache.getBoard(Session.get('currentBoard')); + if (currentBoard) { + const defaultSwimlane = currentBoard.getDefaultSwimline(); + if (defaultSwimlane) { + targetSwimlaneId = defaultSwimlane._id; + } + } + } + + // Get the original swimlane ID of the list (handle backward compatibility) + const originalSwimlaneId = list.getEffectiveSwimlaneId ? list.getEffectiveSwimlaneId() : (list.swimlaneId || null); + + // Prepare update object + const updateData = { + sort: sortIndex.base, + }; + + // Check if the list was dropped in a different swimlane + const isDifferentSwimlane = targetSwimlaneId && targetSwimlaneId !== originalSwimlaneId; + + // If the list was dropped in a different swimlane, update the swimlaneId + if (isDifferentSwimlane) { + updateData.swimlaneId = targetSwimlaneId; + + // Move all cards in the list to the new swimlane + const cardsInList = ReactiveCache.getCards({ + listId: list._id, + archived: false + }); + + cardsInList.forEach(card => { + card.move(list.boardId, targetSwimlaneId, list._id); + }); + + // Don't cancel the sortable when moving to a different swimlane + // The DOM move should be allowed to complete + } else { + // If staying in the same swimlane, cancel the sortable to prevent DOM manipulation issues + $swimlane.sortable('cancel'); + } + + try { + Lists.update(list._id, { + $set: updateData, + }); + } catch (error) { + console.error('Error updating list:', error); + return; + } + // Try to get board component try { const boardComponent = BlazeComponent.getComponentForElement(ui.item[0]); @@ -731,6 +821,18 @@ setTimeout(() => { } catch (e) { // Silent fail } + + // Re-enable dragscroll after list dragging is complete + try { + dragscroll.reset(); + } catch (e) { + // Silent fail + } + + // Re-enable dragscroll on all swimlanes + $('.js-swimlane').each(function() { + $(this).addClass('dragscroll'); + }); } }); } @@ -769,6 +871,96 @@ setTimeout(() => { } }, stop(evt, ui) { + // To attribute the new index number, we need to get the DOM element + // of the previous and the following list -- if any. + const prevListDom = ui.item.prev('.js-list').get(0); + const nextListDom = ui.item.next('.js-list').get(0); + const sortIndex = calculateIndex(prevListDom, nextListDom, 1); + + const listDomElement = ui.item.get(0); + if (!listDomElement) { + console.error('List DOM element not found during drag stop'); + return; + } + + let list; + try { + list = Blaze.getData(listDomElement); + } catch (error) { + console.error('Error getting list data:', error); + return; + } + + if (!list) { + console.error('List data not found for element:', listDomElement); + return; + } + + // Detect if the list was dropped in a different swimlane + const targetSwimlaneDom = ui.item.closest('.js-swimlane'); + let targetSwimlaneId = null; + + if (targetSwimlaneDom.length > 0) { + // List was dropped in a swimlane + try { + targetSwimlaneId = targetSwimlaneDom.attr('id').replace('swimlane-', ''); + } catch (error) { + console.error('Error getting target swimlane ID:', error); + return; + } + } else { + // List was dropped in lists view (not swimlanes view) + // In this case, assign to the default swimlane + const currentBoard = ReactiveCache.getBoard(Session.get('currentBoard')); + if (currentBoard) { + const defaultSwimlane = currentBoard.getDefaultSwimline(); + if (defaultSwimlane) { + targetSwimlaneId = defaultSwimlane._id; + } + } + } + + // Get the original swimlane ID of the list (handle backward compatibility) + const originalSwimlaneId = list.getEffectiveSwimlaneId ? list.getEffectiveSwimlaneId() : (list.swimlaneId || null); + + // Prepare update object + const updateData = { + sort: sortIndex.base, + }; + + // Check if the list was dropped in a different swimlane + const isDifferentSwimlane = targetSwimlaneId && targetSwimlaneId !== originalSwimlaneId; + + // If the list was dropped in a different swimlane, update the swimlaneId + if (isDifferentSwimlane) { + updateData.swimlaneId = targetSwimlaneId; + + // Move all cards in the list to the new swimlane + const cardsInList = ReactiveCache.getCards({ + listId: list._id, + archived: false + }); + + cardsInList.forEach(card => { + card.move(list.boardId, targetSwimlaneId, list._id); + }); + + // Don't cancel the sortable when moving to a different swimlane + // The DOM move should be allowed to complete + } else { + // If staying in the same swimlane, cancel the sortable to prevent DOM manipulation issues + $listsGroup.sortable('cancel'); + } + + try { + Lists.update(list._id, { + $set: updateData, + }); + } catch (error) { + console.error('Error updating list:', error); + return; + } + // Try to get board component try { const boardComponent = BlazeComponent.getComponentForElement(ui.item[0]); @@ -778,6 +970,18 @@ setTimeout(() => { } catch (e) { // Silent fail } + + // Re-enable dragscroll after list dragging is complete + try { + dragscroll.reset(); + } catch (e) { + // Silent fail + } + + // Re-enable dragscroll on all swimlanes + $('.js-swimlane').each(function() { + $(this).addClass('dragscroll'); + }); } }); } From 0cebd8aa4dbe0bf2418b814716744ab806b671c2 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Thu, 23 Oct 2025 04:35:33 +0300 Subject: [PATCH 05/17] Fix drag lists did not work. Part 2. Thanks to xet7 ! --- client/components/swimlanes/swimlanes.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js index c1ddcd369b..47b9c58434 100644 --- a/client/components/swimlanes/swimlanes.js +++ b/client/components/swimlanes/swimlanes.js @@ -730,7 +730,6 @@ setTimeout(() => { const listDomElement = ui.item.get(0); if (!listDomElement) { - console.error('List DOM element not found during drag stop'); return; } @@ -738,12 +737,10 @@ setTimeout(() => { try { list = Blaze.getData(listDomElement); } catch (error) { - console.error('Error getting list data:', error); return; } if (!list) { - console.error('List data not found for element:', listDomElement); return; } @@ -756,7 +753,6 @@ setTimeout(() => { try { targetSwimlaneId = targetSwimlaneDom.attr('id').replace('swimlane-', ''); } catch (error) { - console.error('Error getting target swimlane ID:', error); return; } } else { @@ -808,7 +804,6 @@ setTimeout(() => { $set: updateData, }); } catch (error) { - console.error('Error updating list:', error); return; } @@ -879,7 +874,6 @@ setTimeout(() => { const listDomElement = ui.item.get(0); if (!listDomElement) { - console.error('List DOM element not found during drag stop'); return; } @@ -887,12 +881,10 @@ setTimeout(() => { try { list = Blaze.getData(listDomElement); } catch (error) { - console.error('Error getting list data:', error); return; } if (!list) { - console.error('List data not found for element:', listDomElement); return; } @@ -905,7 +897,6 @@ setTimeout(() => { try { targetSwimlaneId = targetSwimlaneDom.attr('id').replace('swimlane-', ''); } catch (error) { - console.error('Error getting target swimlane ID:', error); return; } } else { @@ -957,7 +948,6 @@ setTimeout(() => { $set: updateData, }); } catch (error) { - console.error('Error updating list:', error); return; } From 7fe7fb4c1597585f01b75355b0e691ac09063018 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Thu, 23 Oct 2025 04:41:34 +0300 Subject: [PATCH 06/17] v8.15 --- CHANGELOG.md | 11 +++++++++++ Dockerfile | 6 +++--- Stackerfile.yml | 2 +- docs/Platforms/Propietary/Windows/Offline.md | 4 ++-- package-lock.json | 2 +- package.json | 2 +- sandstorm-pkgdef.capnp | 4 ++-- snapcraft.yaml | 8 ++++---- 8 files changed, 25 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb1bae4711..2ba71ed713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,17 @@ Fixing other platforms In Progress. WeKan 8.00-8.06 had wrong raw database directory setting /var/snap/wekan/common/wekan and some cards were not visible. Those are fixed at WeKan 8.07 where database directory is back to /var/snap/wekan/common and all cards are visible. +# v8.15 2025-10-23 WeKan ® release + +This release fixes the following bugs: + +- Fix drag lists did not work + [Part 1](https://github.com/wekan/wekan/commit/8662c96d1c8d4fa76ce7b31eb06678ad59c3ebe1), + [Part 2](https://github.com/wekan/wekan/commit/0cebd8aa4dbe0bf2418b814716744ab806b671c2). + Thanks to xet7. + +Thanks to above GitHub users for their contributions and translators for their translations. + # v8.14 2025-10-23 WeKan ® release This release fixes the following bugs: diff --git a/Dockerfile b/Dockerfile index 83582c102e..149a46db47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -249,9 +249,9 @@ cd /home/wekan/app # Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc. #rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy #mv /home/wekan/app_build/bundle /build -wget "https://github.com/wekan/wekan/releases/download/v8.14/wekan-8.14-amd64.zip" -unzip wekan-8.14-amd64.zip -rm wekan-8.14-amd64.zip +wget "https://github.com/wekan/wekan/releases/download/v8.15/wekan-8.15-amd64.zip" +unzip wekan-8.15-amd64.zip +rm wekan-8.15-amd64.zip mv /home/wekan/app/bundle /build # Put back the original tar diff --git a/Stackerfile.yml b/Stackerfile.yml index 567bcc5215..c17665e862 100644 --- a/Stackerfile.yml +++ b/Stackerfile.yml @@ -1,5 +1,5 @@ appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928 -appVersion: "v8.14.0" +appVersion: "v8.15.0" files: userUploads: - README.md diff --git a/docs/Platforms/Propietary/Windows/Offline.md b/docs/Platforms/Propietary/Windows/Offline.md index 44a35e12aa..8b94bb4643 100644 --- a/docs/Platforms/Propietary/Windows/Offline.md +++ b/docs/Platforms/Propietary/Windows/Offline.md @@ -10,7 +10,7 @@ This is without container (without Docker or Snap). Right click and download files 1-4: -1. [wekan-8.14-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.14/wekan-8.14-amd64-windows.zip) +1. [wekan-8.15-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.15/wekan-8.15-amd64-windows.zip) 2. [node.exe](https://nodejs.org/dist/latest-v14.x/win-x64/node.exe) @@ -22,7 +22,7 @@ Right click and download files 1-4: 6. Double click `mongodb-windows-x86_64-7.0.25-signed.msi` . In installer, uncheck downloading MongoDB compass. -7. Unzip `wekan-8.14-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files: +7. Unzip `wekan-8.15-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files: ``` bundle (directory) diff --git a/package-lock.json b/package-lock.json index 445be60692..3fb6043e97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "wekan", - "version": "v8.14.0", + "version": "v8.15.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f52f128152..a4dcfe9498 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wekan", - "version": "v8.14.0", + "version": "v8.15.0", "description": "Open-Source kanban", "private": true, "repository": { diff --git a/sandstorm-pkgdef.capnp b/sandstorm-pkgdef.capnp index 9683133b91..edc3008e12 100644 --- a/sandstorm-pkgdef.capnp +++ b/sandstorm-pkgdef.capnp @@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = ( appTitle = (defaultText = "Wekan"), # The name of the app as it is displayed to the user. - appVersion = 814, + appVersion = 815, # Increment this for every release. - appMarketingVersion = (defaultText = "8.14.0~2025-10-23"), + appMarketingVersion = (defaultText = "8.15.0~2025-10-23"), # Human-readable presentation of the app version. minUpgradableAppVersion = 0, diff --git a/snapcraft.yaml b/snapcraft.yaml index a7e4f76f68..dab9beb63e 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: wekan -version: '8.14' +version: '8.15' base: core24 summary: Open Source kanban description: | @@ -166,9 +166,9 @@ parts: # Cleanup mkdir .build cd .build - wget https://github.com/wekan/wekan/releases/download/v8.14/wekan-8.14-amd64.zip - unzip wekan-8.14-amd64.zip - rm wekan-8.14-amd64.zip + wget https://github.com/wekan/wekan/releases/download/v8.15/wekan-8.15-amd64.zip + unzip wekan-8.15-amd64.zip + rm wekan-8.15-amd64.zip cd .. ##cd .build/bundle ##find . -type d -name '*-garbage*' | xargs rm -rf From 91b846e2cdee9154b045d11b4b4c1a7ae1d79016 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Thu, 23 Oct 2025 05:50:43 +0300 Subject: [PATCH 07/17] List menu / More / Delete duplicate lists that do not have any cards. Thanks to xet7 ! --- client/components/sidebar/sidebar.jade | 10 +++++++ client/components/sidebar/sidebar.js | 39 ++++++++++++++++++++++++++ imports/i18n/data/en.i18n.json | 2 ++ 3 files changed, 51 insertions(+) diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index fdc4e6b07e..662c84ad8b 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -537,6 +537,12 @@ template(name="archiveBoardPopup") | 📦 | {{_ 'archive'}} +template(name="deleteDuplicateListsPopup") + p {{_ 'delete-duplicate-lists-confirm'}} + button.js-confirm.negate.full(type="submit") + | 🗑️ + | {{_ 'delete'}} + template(name="outgoingWebhooksPopup") each integrations form.integration-form @@ -621,6 +627,10 @@ template(name="boardMenuPopup") if currentUser.isBoardAdmin hr ul.pop-over-list + li + a.js-delete-duplicate-lists + | 🗑️ + | {{_ 'delete-duplicate-lists'}} li a.js-archive-board | ➡️📦 diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index 3d507ff8c6..18f2716912 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -275,6 +275,45 @@ Template.boardMenuPopup.events({ 'click .js-change-background-image': Popup.open('boardChangeBackgroundImage'), 'click .js-board-info-on-my-boards': Popup.open('boardInfoOnMyBoards'), 'click .js-change-language': Popup.open('changeLanguage'), + 'click .js-delete-duplicate-lists': Popup.afterConfirm('deleteDuplicateLists', function() { + const currentBoard = Utils.getCurrentBoard(); + if (!currentBoard) return; + + // Get all lists in the current board + const allLists = ReactiveCache.getLists({ boardId: currentBoard._id, archived: false }); + + // Group lists by title to find duplicates + const listsByTitle = {}; + allLists.forEach(list => { + if (!listsByTitle[list.title]) { + listsByTitle[list.title] = []; + } + listsByTitle[list.title].push(list); + }); + + // Find and delete duplicate lists that have no cards + let deletedCount = 0; + Object.keys(listsByTitle).forEach(title => { + const listsWithSameTitle = listsByTitle[title]; + if (listsWithSameTitle.length > 1) { + // Keep the first list, delete the rest if they have no cards + for (let i = 1; i < listsWithSameTitle.length; i++) { + const list = listsWithSameTitle[i]; + const cardsInList = ReactiveCache.getCards({ listId: list._id, archived: false }); + + if (cardsInList.length === 0) { + Lists.remove(list._id); + deletedCount++; + } + } + } + }); + + // Show notification + if (deletedCount > 0) { + // You could add a toast notification here if available + } + }), 'click .js-archive-board ': Popup.afterConfirm('archiveBoard', function() { const currentBoard = Utils.getCurrentBoard(); currentBoard.archive(); diff --git a/imports/i18n/data/en.i18n.json b/imports/i18n/data/en.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/en.i18n.json +++ b/imports/i18n/data/en.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", From 92bfbb2d0ce808620be436d8ada2bb7d81487074 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Thu, 23 Oct 2025 05:54:25 +0300 Subject: [PATCH 08/17] Updated ChangeLog. --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ba71ed713..ffef70a57f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,15 @@ Fixing other platforms In Progress. WeKan 8.00-8.06 had wrong raw database directory setting /var/snap/wekan/common/wekan and some cards were not visible. Those are fixed at WeKan 8.07 where database directory is back to /var/snap/wekan/common and all cards are visible. +# Upcoming WeKan ® release + +This release adds the following new features: + +- [List menu / More / Delete duplicate lists that do not have any cards](https://github.com/wekan/wekan/commit/91b846e2cdee9154b045d11b4b4c1a7ae1d79016). + Thanks to xet7. + +Thanks to above GitHub users for their contributions and translators for their translations. + # v8.15 2025-10-23 WeKan ® release This release fixes the following bugs: From d1a51b42f6910375ac0e08e92e29dc1e26c689eb Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Fri, 24 Oct 2025 18:43:21 +0300 Subject: [PATCH 09/17] Updated translations. --- imports/i18n/data/af.i18n.json | 2 + imports/i18n/data/af_ZA.i18n.json | 2 + imports/i18n/data/ar-DZ.i18n.json | 2 + imports/i18n/data/ar-EG.i18n.json | 2 + imports/i18n/data/ar.i18n.json | 2 + imports/i18n/data/ary.i18n.json | 2 + imports/i18n/data/ast-ES.i18n.json | 2 + imports/i18n/data/az-AZ.i18n.json | 2 + imports/i18n/data/az-LA.i18n.json | 2 + imports/i18n/data/az.i18n.json | 2 + imports/i18n/data/bg.i18n.json | 2 + imports/i18n/data/br.i18n.json | 2 + imports/i18n/data/ca.i18n.json | 2 + imports/i18n/data/ca@valencia.i18n.json | 2 + imports/i18n/data/ca_ES.i18n.json | 2 + imports/i18n/data/cmn.i18n.json | 2 + imports/i18n/data/cs-CZ.i18n.json | 2 + imports/i18n/data/cs.i18n.json | 2 + imports/i18n/data/cy-GB.i18n.json | 2 + imports/i18n/data/cy.i18n.json | 2 + imports/i18n/data/da.i18n.json | 2 + imports/i18n/data/de-AT.i18n.json | 2 + imports/i18n/data/de-CH.i18n.json | 2 + imports/i18n/data/de.i18n.json | 2 + imports/i18n/data/de_DE.i18n.json | 2 + imports/i18n/data/el-GR.i18n.json | 2 + imports/i18n/data/el.i18n.json | 2 + imports/i18n/data/en-BR.i18n.json | 2 + imports/i18n/data/en-DE.i18n.json | 2 + imports/i18n/data/en-GB.i18n.json | 2 + imports/i18n/data/en-IT.i18n.json | 2 + imports/i18n/data/en-MY.i18n.json | 2 + imports/i18n/data/en-YS.i18n.json | 2 + imports/i18n/data/en_AU.i18n.json | 2 + imports/i18n/data/en_ID.i18n.json | 2 + imports/i18n/data/en_SG.i18n.json | 2 + imports/i18n/data/en_TR.i18n.json | 2 + imports/i18n/data/en_ZA.i18n.json | 2 + imports/i18n/data/eo.i18n.json | 2 + imports/i18n/data/es-AR.i18n.json | 2 + imports/i18n/data/es-CL.i18n.json | 2 + imports/i18n/data/es-LA.i18n.json | 2 + imports/i18n/data/es-MX.i18n.json | 2 + imports/i18n/data/es-PE.i18n.json | 2 + imports/i18n/data/es-PY.i18n.json | 2 + imports/i18n/data/es.i18n.json | 2 + imports/i18n/data/es_CO.i18n.json | 2 + imports/i18n/data/et-EE.i18n.json | 2 + imports/i18n/data/eu.i18n.json | 2 + imports/i18n/data/fa-IR.i18n.json | 2 + imports/i18n/data/fa.i18n.json | 2 + imports/i18n/data/fi.i18n.json | 2 + imports/i18n/data/fr-CH.i18n.json | 2 + imports/i18n/data/fr-FR.i18n.json | 2 + imports/i18n/data/fr.i18n.json | 124 ++++++++++++------------ imports/i18n/data/fy-NL.i18n.json | 2 + imports/i18n/data/fy.i18n.json | 2 + imports/i18n/data/gl-ES.i18n.json | 2 + imports/i18n/data/gl.i18n.json | 2 + imports/i18n/data/gu-IN.i18n.json | 2 + imports/i18n/data/he-IL.i18n.json | 2 + imports/i18n/data/he.i18n.json | 2 + imports/i18n/data/hi-IN.i18n.json | 2 + imports/i18n/data/hi.i18n.json | 2 + imports/i18n/data/hr.i18n.json | 2 + imports/i18n/data/hu.i18n.json | 2 + imports/i18n/data/hy.i18n.json | 2 + imports/i18n/data/id.i18n.json | 2 + imports/i18n/data/ig.i18n.json | 2 + imports/i18n/data/it.i18n.json | 2 + imports/i18n/data/ja-HI.i18n.json | 2 + imports/i18n/data/ja.i18n.json | 2 + imports/i18n/data/ka.i18n.json | 2 + imports/i18n/data/km.i18n.json | 2 + imports/i18n/data/ko-KR.i18n.json | 2 + imports/i18n/data/ko.i18n.json | 2 + imports/i18n/data/lt.i18n.json | 2 + imports/i18n/data/lv.i18n.json | 2 + imports/i18n/data/mk.i18n.json | 2 + imports/i18n/data/mn.i18n.json | 2 + imports/i18n/data/ms-MY.i18n.json | 110 ++++++++++----------- imports/i18n/data/ms.i18n.json | 2 + imports/i18n/data/nb.i18n.json | 2 + imports/i18n/data/nl-NL.i18n.json | 2 + imports/i18n/data/nl.i18n.json | 14 +-- imports/i18n/data/oc.i18n.json | 2 + imports/i18n/data/or_IN.i18n.json | 2 + imports/i18n/data/pa.i18n.json | 2 + imports/i18n/data/pl-PL.i18n.json | 2 + imports/i18n/data/pl.i18n.json | 2 + imports/i18n/data/pt-BR.i18n.json | 14 +-- imports/i18n/data/pt.i18n.json | 2 + imports/i18n/data/pt_PT.i18n.json | 2 + imports/i18n/data/ro-RO.i18n.json | 2 + imports/i18n/data/ro.i18n.json | 2 + imports/i18n/data/ru-UA.i18n.json | 2 + imports/i18n/data/ru.i18n.json | 2 + imports/i18n/data/sk.i18n.json | 2 + imports/i18n/data/sl.i18n.json | 2 + imports/i18n/data/sr.i18n.json | 2 + imports/i18n/data/sv.i18n.json | 22 +++-- imports/i18n/data/sw.i18n.json | 2 + imports/i18n/data/ta.i18n.json | 2 + imports/i18n/data/te-IN.i18n.json | 2 + imports/i18n/data/th.i18n.json | 2 + imports/i18n/data/tk_TM.i18n.json | 2 + imports/i18n/data/tlh.i18n.json | 2 + imports/i18n/data/tr.i18n.json | 2 + imports/i18n/data/ug.i18n.json | 2 + imports/i18n/data/uk-UA.i18n.json | 2 + imports/i18n/data/uk.i18n.json | 2 + imports/i18n/data/uz-AR.i18n.json | 2 + imports/i18n/data/uz-LA.i18n.json | 2 + imports/i18n/data/uz-UZ.i18n.json | 2 + imports/i18n/data/uz.i18n.json | 2 + imports/i18n/data/ve-CC.i18n.json | 2 + imports/i18n/data/ve-PP.i18n.json | 2 + imports/i18n/data/ve.i18n.json | 2 + imports/i18n/data/vi-VN.i18n.json | 2 + imports/i18n/data/vi.i18n.json | 2 + imports/i18n/data/vl-SS.i18n.json | 2 + imports/i18n/data/vo.i18n.json | 2 + imports/i18n/data/wa-RR.i18n.json | 2 + imports/i18n/data/wa.i18n.json | 2 + imports/i18n/data/wo.i18n.json | 2 + imports/i18n/data/wuu-Hans.i18n.json | 2 + imports/i18n/data/xh.i18n.json | 2 + imports/i18n/data/yi.i18n.json | 2 + imports/i18n/data/yo.i18n.json | 2 + imports/i18n/data/yue_CN.i18n.json | 2 + imports/i18n/data/zgh.i18n.json | 2 + imports/i18n/data/zh-CN.i18n.json | 2 + imports/i18n/data/zh-GB.i18n.json | 2 + imports/i18n/data/zh-HK.i18n.json | 2 + imports/i18n/data/zh-Hans.i18n.json | 2 + imports/i18n/data/zh-Hant.i18n.json | 8 +- imports/i18n/data/zh-TW.i18n.json | 8 +- imports/i18n/data/zh.i18n.json | 2 + imports/i18n/data/zu-ZA.i18n.json | 2 + imports/i18n/data/zu.i18n.json | 2 + 140 files changed, 423 insertions(+), 143 deletions(-) diff --git a/imports/i18n/data/af.i18n.json b/imports/i18n/data/af.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/af.i18n.json +++ b/imports/i18n/data/af.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/af_ZA.i18n.json b/imports/i18n/data/af_ZA.i18n.json index 471a35a378..ecc4080e20 100644 --- a/imports/i18n/data/af_ZA.i18n.json +++ b/imports/i18n/data/af_ZA.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ar-DZ.i18n.json b/imports/i18n/data/ar-DZ.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/ar-DZ.i18n.json +++ b/imports/i18n/data/ar-DZ.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ar-EG.i18n.json b/imports/i18n/data/ar-EG.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/ar-EG.i18n.json +++ b/imports/i18n/data/ar-EG.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ar.i18n.json b/imports/i18n/data/ar.i18n.json index 5c478b0b10..b4628a45d4 100644 --- a/imports/i18n/data/ar.i18n.json +++ b/imports/i18n/data/ar.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ary.i18n.json b/imports/i18n/data/ary.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/ary.i18n.json +++ b/imports/i18n/data/ary.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ast-ES.i18n.json b/imports/i18n/data/ast-ES.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/ast-ES.i18n.json +++ b/imports/i18n/data/ast-ES.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/az-AZ.i18n.json b/imports/i18n/data/az-AZ.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/az-AZ.i18n.json +++ b/imports/i18n/data/az-AZ.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/az-LA.i18n.json b/imports/i18n/data/az-LA.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/az-LA.i18n.json +++ b/imports/i18n/data/az-LA.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/az.i18n.json b/imports/i18n/data/az.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/az.i18n.json +++ b/imports/i18n/data/az.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/bg.i18n.json b/imports/i18n/data/bg.i18n.json index 4297d1cd70..043015731a 100644 --- a/imports/i18n/data/bg.i18n.json +++ b/imports/i18n/data/bg.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Всички списъци, карти, имена и действия ще бъдат изтрити и няма да можете да възстановите съдържанието на дъската. Няма връщане назад.", "boardDeletePopup-title": "Изтриване на Таблото?", "delete-board": "Изтрий таблото", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Подзадачи за табло __board__", "default": "по подразбиране", "defaultdefault": "по подразбиране", diff --git a/imports/i18n/data/br.i18n.json b/imports/i18n/data/br.i18n.json index b9e0166686..899f9dbb2a 100644 --- a/imports/i18n/data/br.i18n.json +++ b/imports/i18n/data/br.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ca.i18n.json b/imports/i18n/data/ca.i18n.json index 28660dddfd..019a5eacd6 100644 --- a/imports/i18n/data/ca.i18n.json +++ b/imports/i18n/data/ca.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Se suprimiran totes les llistes, fitxes, etiquetes i activitats i no podreu recuperar el contingut del tauler. No hi ha cap desfer.", "boardDeletePopup-title": "Vols suprimir el tauler?", "delete-board": "Suprimeix el tauler", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasques per al tauler __board__", "default": "Per defecte", "defaultdefault": "Per defecte", diff --git a/imports/i18n/data/ca@valencia.i18n.json b/imports/i18n/data/ca@valencia.i18n.json index 6455ca3ed9..ab9a378b1a 100644 --- a/imports/i18n/data/ca@valencia.i18n.json +++ b/imports/i18n/data/ca@valencia.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ca_ES.i18n.json b/imports/i18n/data/ca_ES.i18n.json index 06d043b5fd..7e2ba5ba7a 100644 --- a/imports/i18n/data/ca_ES.i18n.json +++ b/imports/i18n/data/ca_ES.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/cmn.i18n.json b/imports/i18n/data/cmn.i18n.json index e2718dafa9..f99bd548e8 100644 --- a/imports/i18n/data/cmn.i18n.json +++ b/imports/i18n/data/cmn.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/cs-CZ.i18n.json b/imports/i18n/data/cs-CZ.i18n.json index f7b2fe0b32..0e915cedf0 100644 --- a/imports/i18n/data/cs-CZ.i18n.json +++ b/imports/i18n/data/cs-CZ.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Všechny sloupce, štítky a aktivity budou smazány a obsah tabla nebude možné obnovit. Toto nelze vrátit zpět.", "boardDeletePopup-title": "Smazat tablo?", "delete-board": "Smazat tablo", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Podúkoly pro tablo __board__", "default": "Výchozí", "defaultdefault": "Výchozí", diff --git a/imports/i18n/data/cs.i18n.json b/imports/i18n/data/cs.i18n.json index a41e047737..58abbc60d1 100644 --- a/imports/i18n/data/cs.i18n.json +++ b/imports/i18n/data/cs.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Všechny sloupce, štítky a aktivity budou smazány a obsah tabla nebude možné obnovit. Toto nelze vrátit zpět.", "boardDeletePopup-title": "Smazat tablo?", "delete-board": "Smazat tablo", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Podúkoly pro tablo __board__", "default": "Výchozí", "defaultdefault": "Výchozí", diff --git a/imports/i18n/data/cy-GB.i18n.json b/imports/i18n/data/cy-GB.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/cy-GB.i18n.json +++ b/imports/i18n/data/cy-GB.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/cy.i18n.json b/imports/i18n/data/cy.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/cy.i18n.json +++ b/imports/i18n/data/cy.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/da.i18n.json b/imports/i18n/data/da.i18n.json index 79bdfb7aae..bfe07883d8 100644 --- a/imports/i18n/data/da.i18n.json +++ b/imports/i18n/data/da.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Alle lister, kort, etiketter og aktiviteter vil blive slettet og du får ikke mulighed for at genskabe tavlens indhold. Dette kan ikke fortrydes.", "boardDeletePopup-title": "Slet tavle?", "delete-board": "Slet tavle", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Delopgaver for tavlen __board__", "default": "Standard", "defaultdefault": "Standard", diff --git a/imports/i18n/data/de-AT.i18n.json b/imports/i18n/data/de-AT.i18n.json index 4f5633d2e1..625ad56557 100644 --- a/imports/i18n/data/de-AT.i18n.json +++ b/imports/i18n/data/de-AT.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Alle Listen, Karten, Labels und Akivitäten werden gelöscht und Sie können die Inhalte des Boards nicht wiederherstellen! Die Aktion kann nicht rückgängig gemacht werden.", "boardDeletePopup-title": "Board löschen?", "delete-board": "Board löschen", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Teilaufgabe für __board__ Board", "default": "Standard", "defaultdefault": "Standard", diff --git a/imports/i18n/data/de-CH.i18n.json b/imports/i18n/data/de-CH.i18n.json index 77c5ba55e3..4c96823822 100644 --- a/imports/i18n/data/de-CH.i18n.json +++ b/imports/i18n/data/de-CH.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Alle Listen, Karten, Labels und Akivitäten werden gelöscht und Sie können die Inhalte des Boards nicht wiederherstellen! Die Aktion kann nicht rückgängig gemacht werden.", "boardDeletePopup-title": "Board löschen?", "delete-board": "Board löschen", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Teilaufgabe für __board__ Board", "default": "Standard", "defaultdefault": "Standard", diff --git a/imports/i18n/data/de.i18n.json b/imports/i18n/data/de.i18n.json index 586c2ab972..f138b8d6c2 100644 --- a/imports/i18n/data/de.i18n.json +++ b/imports/i18n/data/de.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Alle Listen, Karten, Labels und Akivitäten werden gelöscht und Sie können die Inhalte des Boards nicht wiederherstellen! Die Aktion kann nicht rückgängig gemacht werden.", "boardDeletePopup-title": "Board löschen?", "delete-board": "Board löschen", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Teilaufgabe für __board__ Board", "default": "Standard", "defaultdefault": "Standard", diff --git a/imports/i18n/data/de_DE.i18n.json b/imports/i18n/data/de_DE.i18n.json index 44f91e057e..c995377fba 100644 --- a/imports/i18n/data/de_DE.i18n.json +++ b/imports/i18n/data/de_DE.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Alle Listen, Karten, Labels und Akivitäten werden gelöscht und Sie können die Inhalte des Boards nicht wiederherstellen! Die Aktion kann nicht rückgängig gemacht werden.", "boardDeletePopup-title": "Board löschen?", "delete-board": "Board löschen", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Teilaufgabe für __board__ Board", "default": "Standard", "defaultdefault": "Standard", diff --git a/imports/i18n/data/el-GR.i18n.json b/imports/i18n/data/el-GR.i18n.json index d1030cd9a9..48fcea53ad 100644 --- a/imports/i18n/data/el-GR.i18n.json +++ b/imports/i18n/data/el-GR.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Διαγραφή Πίνακα;", "delete-board": "Διαγραφή Πίνακα", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Προεπιλογή", "defaultdefault": "Προεπιλογή", diff --git a/imports/i18n/data/el.i18n.json b/imports/i18n/data/el.i18n.json index 096701fd03..c4da6359dd 100644 --- a/imports/i18n/data/el.i18n.json +++ b/imports/i18n/data/el.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Διαγραφή Πίνακα;", "delete-board": "Διαγραφή Πίνακα", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Προεπιλογή", "defaultdefault": "Προεπιλογή", diff --git a/imports/i18n/data/en-BR.i18n.json b/imports/i18n/data/en-BR.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/en-BR.i18n.json +++ b/imports/i18n/data/en-BR.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/en-DE.i18n.json b/imports/i18n/data/en-DE.i18n.json index 6895e31db4..50ddae1d2e 100644 --- a/imports/i18n/data/en-DE.i18n.json +++ b/imports/i18n/data/en-DE.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/en-GB.i18n.json b/imports/i18n/data/en-GB.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/en-GB.i18n.json +++ b/imports/i18n/data/en-GB.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/en-IT.i18n.json b/imports/i18n/data/en-IT.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/en-IT.i18n.json +++ b/imports/i18n/data/en-IT.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/en-MY.i18n.json b/imports/i18n/data/en-MY.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/en-MY.i18n.json +++ b/imports/i18n/data/en-MY.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/en-YS.i18n.json b/imports/i18n/data/en-YS.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/en-YS.i18n.json +++ b/imports/i18n/data/en-YS.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/en_AU.i18n.json b/imports/i18n/data/en_AU.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/en_AU.i18n.json +++ b/imports/i18n/data/en_AU.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/en_ID.i18n.json b/imports/i18n/data/en_ID.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/en_ID.i18n.json +++ b/imports/i18n/data/en_ID.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/en_SG.i18n.json b/imports/i18n/data/en_SG.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/en_SG.i18n.json +++ b/imports/i18n/data/en_SG.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/en_TR.i18n.json b/imports/i18n/data/en_TR.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/en_TR.i18n.json +++ b/imports/i18n/data/en_TR.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/en_ZA.i18n.json b/imports/i18n/data/en_ZA.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/en_ZA.i18n.json +++ b/imports/i18n/data/en_ZA.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/eo.i18n.json b/imports/i18n/data/eo.i18n.json index 66aaa66fa7..20a85f9f64 100644 --- a/imports/i18n/data/eo.i18n.json +++ b/imports/i18n/data/eo.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Defaŭlto", "defaultdefault": "Defaŭlto", diff --git a/imports/i18n/data/es-AR.i18n.json b/imports/i18n/data/es-AR.i18n.json index 6be715c615..874d60c157 100644 --- a/imports/i18n/data/es-AR.i18n.json +++ b/imports/i18n/data/es-AR.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/es-CL.i18n.json b/imports/i18n/data/es-CL.i18n.json index d3c77ecd46..6b167dc522 100644 --- a/imports/i18n/data/es-CL.i18n.json +++ b/imports/i18n/data/es-CL.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Se eliminarán todas las listas, tarjetas, etiquetas y actividades, y no podrás recuperar los contenidos del tablero. Esta acción no puede deshacerse.", "boardDeletePopup-title": "¿Eliminar el tablero?", "delete-board": "Eliminar el tablero", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtareas para el tablero __board__", "default": "Por defecto", "defaultdefault": "Por defecto", diff --git a/imports/i18n/data/es-LA.i18n.json b/imports/i18n/data/es-LA.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/es-LA.i18n.json +++ b/imports/i18n/data/es-LA.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/es-MX.i18n.json b/imports/i18n/data/es-MX.i18n.json index 29cf721b73..858a623c91 100644 --- a/imports/i18n/data/es-MX.i18n.json +++ b/imports/i18n/data/es-MX.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/es-PE.i18n.json b/imports/i18n/data/es-PE.i18n.json index 6a26d0f5ee..276b1d3686 100644 --- a/imports/i18n/data/es-PE.i18n.json +++ b/imports/i18n/data/es-PE.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Se eliminarán todas las listas, tarjetas, etiquetas y actividades, y no podrás recuperar los contenidos del tablero. Esta acción no puede deshacerse.", "boardDeletePopup-title": "¿Eliminar el tablero?", "delete-board": "Eliminar el tablero", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtareas para el tablero __board__", "default": "Por defecto", "defaultdefault": "Por defecto", diff --git a/imports/i18n/data/es-PY.i18n.json b/imports/i18n/data/es-PY.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/es-PY.i18n.json +++ b/imports/i18n/data/es-PY.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/es.i18n.json b/imports/i18n/data/es.i18n.json index 31713206d0..c0ea65cd5d 100644 --- a/imports/i18n/data/es.i18n.json +++ b/imports/i18n/data/es.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Se eliminarán todas las listas, tarjetas, etiquetas y actividades, y no podrás recuperar los contenidos del tablero. Esta acción no puede deshacerse.", "boardDeletePopup-title": "¿Eliminar el tablero?", "delete-board": "Eliminar el tablero", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtareas para el tablero __board__", "default": "Por defecto", "defaultdefault": "Por defecto", diff --git a/imports/i18n/data/es_CO.i18n.json b/imports/i18n/data/es_CO.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/es_CO.i18n.json +++ b/imports/i18n/data/es_CO.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/et-EE.i18n.json b/imports/i18n/data/et-EE.i18n.json index e53458d178..40bf924c44 100644 --- a/imports/i18n/data/et-EE.i18n.json +++ b/imports/i18n/data/et-EE.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Kõik nimekirjad, kaardid, sildid ja tegevused kustutatakse ja te ei saa tahvli sisu taastada. Tühistamist ei ole võimalik teha.", "boardDeletePopup-title": "Kustuta juhatus?", "delete-board": "Kustuta juhatus", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Juhatuse __board__ alamülesanded", "default": "Vaikimisi", "defaultdefault": "Vaikimisi", diff --git a/imports/i18n/data/eu.i18n.json b/imports/i18n/data/eu.i18n.json index 4c84d0baf0..84dd9440d9 100644 --- a/imports/i18n/data/eu.i18n.json +++ b/imports/i18n/data/eu.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Zerrenda, txartel eta aktibitate guztiak ezabatuko dira eta ezingo dituzu berreskuratu arbelaren edukiak. Atzera bueltarik ez du.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Lehenetsia", "defaultdefault": "Lehenetsia", diff --git a/imports/i18n/data/fa-IR.i18n.json b/imports/i18n/data/fa-IR.i18n.json index 82f4b17c3f..ddfb6a36db 100644 --- a/imports/i18n/data/fa-IR.i18n.json +++ b/imports/i18n/data/fa-IR.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "تمام لیست ها، کارت ها، لیبل ها و فعالیت ها حذف خواهند شد و شما نمی توانید محتوای برد را بازیابی کنید. هیچ واکنشی وجود ندارد", "boardDeletePopup-title": "حذف برد؟", "delete-board": "حذف برد", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "زیروظایفِ برد __board__", "default": "پیش‌فرض", "defaultdefault": "پیش‌فرض", diff --git a/imports/i18n/data/fa.i18n.json b/imports/i18n/data/fa.i18n.json index 7b18e0445c..38d280360e 100644 --- a/imports/i18n/data/fa.i18n.json +++ b/imports/i18n/data/fa.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "تمام لیست ها، کارت ها، لیبل ها و فعالیت ها حذف خواهند شد و شما نمی توانید محتوای برد را بازیابی کنید. هیچ واکنشی وجود ندارد", "boardDeletePopup-title": "حذف برد؟", "delete-board": "حذف برد", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "زیروظایفِ برد __board__", "default": "پیش‌فرض", "defaultdefault": "پیش‌فرض", diff --git a/imports/i18n/data/fi.i18n.json b/imports/i18n/data/fi.i18n.json index b9c5fc9db8..cdec49369b 100644 --- a/imports/i18n/data/fi.i18n.json +++ b/imports/i18n/data/fi.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Kaikki listat, kortit, nimilaput ja toimet poistetaan ja et pysty palauttamaan taulun sisältöä. Tätä ei voi peruuttaa.", "boardDeletePopup-title": "Poista taulu?", "delete-board": "Poista taulu", + "delete-duplicate-lists": "Poista ylimääräiset lista kopiot", + "delete-duplicate-lists-confirm": "Oletko varma? Tämä poistaa kaikki ylimääräiset lista kopiot, joilla on sama nimi ja jotka eivät sisällä kortteja.", "default-subtasks-board": "Alitehtävät taululle __board__", "default": "Oletus", "defaultdefault": "Oletus", diff --git a/imports/i18n/data/fr-CH.i18n.json b/imports/i18n/data/fr-CH.i18n.json index ffc4282798..ab5df8545c 100644 --- a/imports/i18n/data/fr-CH.i18n.json +++ b/imports/i18n/data/fr-CH.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/fr-FR.i18n.json b/imports/i18n/data/fr-FR.i18n.json index feb923960a..643f299b74 100644 --- a/imports/i18n/data/fr-FR.i18n.json +++ b/imports/i18n/data/fr-FR.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Toutes les listes, cartes, étiquettes et activités seront supprimées et vous ne pourrez pas retrouver le contenu du tableau. Cela est irréversible.", "boardDeletePopup-title": "Supprimer le tableau ?", "delete-board": "Supprimer le tableau", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Sous-tâches du tableau __board__", "default": "Défaut", "defaultdefault": "Défaut", diff --git a/imports/i18n/data/fr.i18n.json b/imports/i18n/data/fr.i18n.json index a12c9a2e99..042abb941e 100644 --- a/imports/i18n/data/fr.i18n.json +++ b/imports/i18n/data/fr.i18n.json @@ -357,9 +357,9 @@ "custom-fields": "Champs personnalisés", "date": "Date", "date-format": "Format de la date", - "date-format-yyyy-mm-dd": "YYYY-MM-DD", - "date-format-dd-mm-yyyy": "DD-MM-YYYY", - "date-format-mm-dd-yyyy": "MM-DD-YYYY", + "date-format-yyyy-mm-dd": "AAAA-MM-JJ", + "date-format-dd-mm-yyyy": "JJ-MM-AAAA", + "date-format-mm-dd-yyyy": "MM-JJ-AAAA", "decline": "Refuser", "default-avatar": "Avatar par défaut", "delete": "Supprimer", @@ -384,21 +384,21 @@ "editLabelPopup-title": "Modifier l'étiquette", "editNotificationPopup-title": "Modifier la notification", "editProfilePopup-title": "Modifier le profil", - "email": "E-mail", - "email-address": "Email Address", + "email": "Courriel", + "email-address": "Adresse de courriel", "email-enrollAccount-subject": "Un compte a été créé pour vous sur __siteName__", "email-enrollAccount-text": "Bonjour __user__,\n\nPour commencer à utiliser ce service, il suffit de cliquer sur le lien ci-dessous.\n\n__url__\n\nMerci.", "email-fail": "Échec de l'envoi du courriel.", - "email-fail-text": "Une erreur est survenue en tentant d'envoyer l'email", - "email-invalid": "Adresse e-mail incorrecte.", - "email-invite": "Inviter par e-mail", + "email-fail-text": "Une erreur est survenue en tentant d'envoyer le courriel", + "email-invalid": "Adresse de courriel incorrecte.", + "email-invite": "Inviter par courriel", "email-invite-subject": "__inviter__ vous a envoyé une invitation", "email-invite-text": "Cher/Chère __user__,\n\n__inviter__ vous invite à rejoindre le tableau \"__board__\" pour collaborer.\n\nVeuillez suivre le lien ci-dessous :\n\n__url__\n\nMerci.", "email-resetPassword-subject": "Réinitialiser votre mot de passe sur __siteName__", "email-resetPassword-text": "Bonjour __user__,\n\nPour réinitialiser votre mot de passe, cliquez sur le lien ci-dessous.\n\n__url__\n\nMerci.", "email-sent": "Courriel envoyé", "email-verifyEmail-subject": "Vérifier votre adresse de courriel sur __siteName__", - "email-verifyEmail-text": "Bonjour __user__,\n\nPour vérifier votre compte courriel, il suffit de cliquer sur le lien ci-dessous.\n\n__url__\n\nMerci.", + "email-verifyEmail-text": "Bonjour __user__,\n\nPour vérifier le courriel de votre compte, il suffit de cliquer sur le lien ci-dessous.\n\n__url__\n\nMerci.", "enable-vertical-scrollbars": "Activer les barres de défilement verticales", "enable-wip-limit": "Activer la limite WIP", "error-board-doesNotExist": "Ce tableau n'existe pas", @@ -414,7 +414,7 @@ "error-username-taken": "Ce nom d'utilisateur est déjà utilisé", "error-orgname-taken": "Ce nom d'organisation est déjà utilisé", "error-teamname-taken": "Ce nom d'équipe est déjà utilisé", - "error-email-taken": "Cette adresse mail est déjà utilisée", + "error-email-taken": "Cette adresse de courriel est déjà utilisée", "export-board": "Exporter le tableau", "export-board-json": "Exporter le tableau en JSON", "export-board-csv": "Exporter le tableau en CSV", @@ -638,9 +638,9 @@ "upload": "Télécharger", "upload-avatar": "Télécharger un avatar", "uploaded-avatar": "Avatar téléchargé", - "uploading-files": "Uploading files", - "upload-failed": "Upload failed", - "upload-completed": "Upload completed", + "uploading-files": "Chargement des fichiers", + "upload-failed": "Le chargement a échoué", + "upload-completed": "Le chargement est terminé", "custom-top-left-corner-logo-image-url": "URL de l'Image du logo personnalisé dans le coin supérieur gauche", "custom-top-left-corner-logo-link-url": "Lien URL du logo personnalisé dans le coin supérieur gauche", "custom-top-left-corner-logo-height": "Hauteur du logo personnalisé dans le coin supérieur gauche. Défaut : 27", @@ -676,9 +676,9 @@ "invite": "Inviter", "invite-people": "Inviter une personne", "to-boards": "Au(x) tableau(x)", - "email-addresses": "Adresses mail", - "smtp-host-description": "L'adresse du serveur SMTP qui gère vos mails.", - "smtp-port-description": "Le port des mails sortants du serveur SMTP.", + "email-addresses": "Adresses de courriel", + "smtp-host-description": "L'adresse du serveur SMTP qui gère vos courriels.", + "smtp-port-description": "Le port du serveur SMTP utilisé pour les courriels sortants.", "smtp-tls-description": "Activer la gestion de TLS sur le serveur SMTP", "smtp-host": "Hôte SMTP", "smtp-port": "Port SMTP", @@ -686,12 +686,12 @@ "smtp-password": "Mot de passe", "smtp-tls": "Prise en charge de TLS", "send-from": "De", - "send-smtp-test": "Envoyer un mail de test à vous-même", + "send-smtp-test": "Envoyer un courriel de test à vous-même", "invitation-code": "Code d'invitation", "email-invite-register-subject": "__inviter__ vous a envoyé une invitation", "email-invite-register-text": "Cher/Chère __user__,\n\n__inviter__ vous invite à le rejoindre sur le tableau kanban pour collaborer.\n\nVeuillez suivre le lien ci-dessous :\n__url__\n\nVotre code d'invitation est : __icode__\n\nMerci.", - "email-smtp-test-subject": "E-mail de test SMTP", - "email-smtp-test-text": "Vous avez envoyé un mail avec succès", + "email-smtp-test-subject": "Courriel de test SMTP", + "email-smtp-test-text": "Vous avez envoyé un courriel avec succès", "error-invitation-code-not-exist": "Ce code d'invitation n'existe pas.", "error-notAuthorized": "Vous n'êtes pas autorisé à accéder à cette page.", "webhook-title": "Nom du webhook", @@ -730,7 +730,7 @@ "yes": "Oui", "no": "Non", "accounts": "Comptes", - "accounts-allowEmailChange": "Autoriser le changement d'adresse mail", + "accounts-allowEmailChange": "Autoriser le changement d'adresse de courriel", "accounts-allowUserNameChange": "Autoriser le changement d'identifiant", "tableVisibilityMode-allowPrivateOnly": "Visibilité des tableaux: N'autoriser que des tableaux privés", "tableVisibilityMode" : "Visibilité des tableaux", @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Toutes les listes, cartes, étiquettes et activités seront supprimées et vous ne pourrez pas retrouver le contenu du tableau. Cela est irréversible.", "boardDeletePopup-title": "Supprimer le tableau ?", "delete-board": "Supprimer le tableau", + "delete-duplicate-lists": "Supprimer les listes en doublon ? ", + "delete-duplicate-lists-confirm": "Êtes-vous sûr ? Cela supprimera toutes les listes en doublon qui ont le même nom et qui ne contiennent aucune carte.", "default-subtasks-board": "Sous-tâches du tableau __board__", "default": "Défaut", "defaultdefault": "Défaut", @@ -848,7 +850,7 @@ "r-uncheck": "Décocher", "r-item": "élément", "r-of-checklist": "de la check-list", - "r-send-email": "Envoyer un email", + "r-send-email": "Envoyer un courriel", "r-to": "à", "r-of": "sur", "r-subject": "sujet", @@ -857,7 +859,7 @@ "r-d-move-to-top-spec": "Déplacer la carte en haut de la liste", "r-d-move-to-bottom-gen": "Déplacer la carte en bas de sa liste", "r-d-move-to-bottom-spec": "Déplacer la carte en bas de la liste", - "r-d-send-email": "Envoyer un email", + "r-d-send-email": "Envoyer le courriel", "r-d-send-email-to": "à", "r-d-send-email-subject": "sujet", "r-d-send-email-message": "message", @@ -1016,8 +1018,8 @@ "dueCardsViewChange-choice-me": "Moi", "dueCardsViewChange-choice-all": "Tous les utilisateurs", "dueCardsViewChange-choice-all-description": "Visualise toutes les cartes incomplètes avec une date *échue* pour lesquelles l'utilisateur possède les droits", - "dueCards-noResults-title": "No Due Cards Found", - "dueCards-noResults-description": "You don't have any cards with due dates at the moment.", + "dueCards-noResults-title": "Aucune carte avec échéance trouvée", + "dueCards-noResults-description": "Vous n'avez aucune carte avec échéance en ce moment.", "broken-cards": "Cartes en erreur", "board-title-not-found": "Tableau '%s' non trouvé.", "swimlane-title-not-found": "Couloir '%s' non trouvé.", @@ -1323,8 +1325,8 @@ "admin-people-user-inactive": "L'utilisateur est désactivé - Cliquer pour l'activer", "accounts-lockout-all-users-unlocked": "Tous les utilisateurs bloqués ont été déverrouillés", "accounts-lockout-unlock-all": "Tout déverrouiller", - "active-cron-jobs": "Active Scheduled Jobs", - "add-cron-job": "Add Scheduled Job", + "active-cron-jobs": "Travaux actifs planifiés", + "add-cron-job": "Ajouter un travail planifié", "add-cron-job-placeholder": "Add Scheduled Job functionality coming soon", "attachment-storage-configuration": "Attachment Storage Configuration", "attachments-path": "Attachments Path", @@ -1338,8 +1340,8 @@ "board-cleanup-failed": "Failed to schedule board cleanup", "board-cleanup-scheduled": "Board cleanup scheduled successfully", "board-operations": "Board Operations", - "cron-jobs": "Scheduled Jobs", - "cron-migrations": "Scheduled Migrations", + "cron-jobs": "Travaux planifiés", + "cron-migrations": "Migrations planifiées", "cron-job-delete-confirm": "Are you sure you want to delete this scheduled job?", "cron-job-delete-failed": "Failed to delete scheduled job", "cron-job-deleted": "Scheduled job deleted successfully", @@ -1398,34 +1400,34 @@ "attachment-monitoring": "Attachment Monitoring", "attachment-settings": "Attachment Settings", "attachment-storage-settings": "Storage Settings", - "automatic-migration": "Automatic Migration", - "back-to-settings": "Back to Settings", - "board-id": "Board ID", + "automatic-migration": "Migration automatique", + "back-to-settings": "Retour aux paramètres", + "board-id": "ID du tableau", "board-migration": "Board Migration", - "card-show-lists-on-minicard": "Show Lists on Minicard", + "card-show-lists-on-minicard": "Afficher les listes sur la mini-carte", "cleanup": "Cleanup", "cleanup-old-jobs": "Cleanup Old Jobs", "completed": "Terminé", "conversion-info-text": "This conversion is performed once per board and improves performance. You can continue using the board normally.", "converting-board": "Converting Board", "converting-board-description": "Converting board structure for improved functionality. This may take a few moments.", - "cpu-cores": "CPU Cores", - "cpu-usage": "CPU Usage", + "cpu-cores": "Cœurs du CPU ", + "cpu-usage": "Utilisation du CPU", "current-action": "Current Action", - "database-migration": "Database Migration", + "database-migration": "Migration base de données", "database-migration-description": "Updating database structure for improved functionality and performance. This process may take several minutes.", - "database-migrations": "Database Migrations", - "days-old": "Days Old", - "duration": "Duration", - "errors": "Errors", - "estimated-time-remaining": "Estimated time remaining", - "every-1-day": "Every 1 day", - "every-1-hour": "Every 1 hour", - "every-1-minute": "Every 1 minute", - "every-10-minutes": "Every 10 minutes", - "every-30-minutes": "Every 30 minutes", - "every-5-minutes": "Every 5 minutes", - "every-6-hours": "Every 6 hours", + "database-migrations": "Migrations base de données", + "days-old": "Jours d'ancienneté", + "duration": "Durée", + "errors": "Erreurs", + "estimated-time-remaining": "Temps restant estimé", + "every-1-day": "Tous les jours", + "every-1-hour": "Toutes les heures", + "every-1-minute": "Toutes les minutes", + "every-10-minutes": "Toutes les 10 minutes", + "every-30-minutes": "Toutes les 30 minutes", + "every-5-minutes": "Toutes les 5 minutes", + "every-6-hours": "Toutes les 6 heures", "export-monitoring": "Export Monitoring", "filesystem-attachments": "Filesystem Attachments", "filesystem-size": "Filesystem Size", @@ -1434,22 +1436,22 @@ "gridfs-attachments": "GridFS Attachments", "gridfs-size": "GridFS Size", "gridfs-storage": "GridFS", - "hide-list-on-minicard": "Hide List on Minicard", + "hide-list-on-minicard": "Masquer la liste sur la mini-carte", "idle-migration": "Idle Migration", "job-description": "Job Description", "job-details": "Job Details", "job-name": "Job Name", "job-queue": "Job Queue", - "last-run": "Last Run", + "last-run": "Dernière exécution", "max-concurrent": "Max Concurrent", - "memory-usage": "Memory Usage", + "memory-usage": "Utilisation de la mémoire", "migrate-all-to-filesystem": "Migrate All to Filesystem", "migrate-all-to-gridfs": "Migrate All to GridFS", "migrate-all-to-s3": "Migrate All to S3", "migrated-attachments": "Migrated Attachments", "migration-batch-size": "Batch Size", "migration-batch-size-description": "Number of attachments to process in each batch (1-100)", - "migration-cpu-threshold": "CPU Threshold (%)", + "migration-cpu-threshold": "Seuil CPU (%)", "migration-cpu-threshold-description": "Pause migration when CPU usage exceeds this percentage (10-90)", "migration-delay-ms": "Delay (ms)", "migration-delay-ms-description": "Delay between batches in milliseconds (100-10000)", @@ -1463,26 +1465,26 @@ "migration-warning-text": "Please do not close your browser during migration. The process will continue in the background but may take longer to complete.", "monitoring-export-failed": "Failed to export monitoring data", "monitoring-refresh-failed": "Failed to refresh monitoring data", - "next": "Next", - "next-run": "Next Run", + "next": "Suivant", + "next-run": "Prochaine exécution", "of": "sur", - "operation-type": "Operation Type", + "operation-type": "Type d'opération", "overall-progress": "Overall Progress", "page": "Page", "pause-migration": "Pause Migration", - "previous": "Previous", + "previous": "Précédent", "refresh": "Refresh", "refresh-monitoring": "Refresh Monitoring", "remaining-attachments": "Remaining Attachments", "resume-migration": "Resume Migration", - "run-once": "Run once", - "s3-attachments": "S3 Attachments", - "s3-size": "S3 Size", + "run-once": "Exécuter une fois", + "s3-attachments": "Pièces jointes S3", + "s3-size": "Taille S3", "s3-storage": "S3", "scanning-status": "Scanning Status", "schedule": "Schedule", "search-boards-or-operations": "Search boards or operations...", - "show-list-on-minicard": "Show List on Minicard", + "show-list-on-minicard": "Afficher la liste sur la mini-carte", "showing": "Showing", "start-test-operation": "Start Test Operation", "start-time": "Start Time", @@ -1492,8 +1494,8 @@ "system-resources": "System Resources", "total-attachments": "Total Attachments", "total-operations": "Total Operations", - "total-size": "Total Size", - "unmigrated-boards": "Unmigrated Boards", + "total-size": "Taille totale", + "unmigrated-boards": "Tableaux non migrés", "weight": "Poids", "idle": "Inactif", "complete": "Terminé", diff --git a/imports/i18n/data/fy-NL.i18n.json b/imports/i18n/data/fy-NL.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/fy-NL.i18n.json +++ b/imports/i18n/data/fy-NL.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/fy.i18n.json b/imports/i18n/data/fy.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/fy.i18n.json +++ b/imports/i18n/data/fy.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/gl-ES.i18n.json b/imports/i18n/data/gl-ES.i18n.json index 6e381a12a8..8c523be4e8 100644 --- a/imports/i18n/data/gl-ES.i18n.json +++ b/imports/i18n/data/gl-ES.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/gl.i18n.json b/imports/i18n/data/gl.i18n.json index 9c24ed0236..9b9438f288 100644 --- a/imports/i18n/data/gl.i18n.json +++ b/imports/i18n/data/gl.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/gu-IN.i18n.json b/imports/i18n/data/gu-IN.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/gu-IN.i18n.json +++ b/imports/i18n/data/gu-IN.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/he-IL.i18n.json b/imports/i18n/data/he-IL.i18n.json index d8cd290c46..b34277bac8 100644 --- a/imports/i18n/data/he-IL.i18n.json +++ b/imports/i18n/data/he-IL.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/he.i18n.json b/imports/i18n/data/he.i18n.json index ad49be1228..2b6d8f5ef1 100644 --- a/imports/i18n/data/he.i18n.json +++ b/imports/i18n/data/he.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "כל הרשימות, הכרטיסים, התווית והפעולות יימחקו ולא תהיה לך דרך לשחזר את תכני הלוח. אין אפשרות לבטל.", "boardDeletePopup-title": "למחוק את הלוח?", "delete-board": "מחיקת לוח", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "תת־משימות עבור הלוח __board__", "default": "בררת מחדל", "defaultdefault": "בררת מחדל", diff --git a/imports/i18n/data/hi-IN.i18n.json b/imports/i18n/data/hi-IN.i18n.json index fc15c79d75..e65f94bf7c 100644 --- a/imports/i18n/data/hi-IN.i18n.json +++ b/imports/i18n/data/hi-IN.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, कार्ड,नामपत्र , और activities हो जाएगा deleted और you won't be able तक recover the बोर्ड contents. There is no undo.", "boardDeletePopup-title": "मिटाएँ बोर्ड?", "delete-board": "मिटाएँ बोर्ड", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ बोर्ड", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/hi.i18n.json b/imports/i18n/data/hi.i18n.json index 34d873c703..8febb7ce75 100644 --- a/imports/i18n/data/hi.i18n.json +++ b/imports/i18n/data/hi.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, कार्ड,नामपत्र , और activities हो जाएगा deleted और you won't be able तक recover the बोर्ड contents. There is no undo.", "boardDeletePopup-title": "मिटाएँ बोर्ड?", "delete-board": "मिटाएँ बोर्ड", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ बोर्ड", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/hr.i18n.json b/imports/i18n/data/hr.i18n.json index e50f9b494a..5b66d06dd0 100644 --- a/imports/i18n/data/hr.i18n.json +++ b/imports/i18n/data/hr.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Obrisati ploču?", "delete-board": "Obriši ploču", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Zadano", "defaultdefault": "Zadano", diff --git a/imports/i18n/data/hu.i18n.json b/imports/i18n/data/hu.i18n.json index e0dc138899..82d25500ae 100644 --- a/imports/i18n/data/hu.i18n.json +++ b/imports/i18n/data/hu.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Minden Lista, Kártya, Címke és Esemény véglegesen törlésre kerül és nincs rá mód, hogy visszanyerd a Tábla tartalmát. Nincs visszavonási lehetőség sem.", "boardDeletePopup-title": "TÖRLÖD a Táblát?", "delete-board": "Tábla törlése", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Rész-feladatok ehhez a Táblához: __board__", "default": "Alapértelmezett", "defaultdefault": "Alapértelmezett", diff --git a/imports/i18n/data/hy.i18n.json b/imports/i18n/data/hy.i18n.json index 5f4d671f27..78f216ac6c 100644 --- a/imports/i18n/data/hy.i18n.json +++ b/imports/i18n/data/hy.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/id.i18n.json b/imports/i18n/data/id.i18n.json index 7ee0e51d8d..e5646003c0 100644 --- a/imports/i18n/data/id.i18n.json +++ b/imports/i18n/data/id.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Hapus Papan?", "delete-board": "Hapus Papan", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Standar", "defaultdefault": "Standar", diff --git a/imports/i18n/data/ig.i18n.json b/imports/i18n/data/ig.i18n.json index 09e6ad1433..38e4d0620e 100644 --- a/imports/i18n/data/ig.i18n.json +++ b/imports/i18n/data/ig.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/it.i18n.json b/imports/i18n/data/it.i18n.json index 00abbe4c54..a6046dd35e 100644 --- a/imports/i18n/data/it.i18n.json +++ b/imports/i18n/data/it.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Tutte le liste, schede, etichette e azioni saranno rimosse e non sarai più in grado di recuperare il contenuto della bacheca. L'azione non è annullabile.", "boardDeletePopup-title": "Eliminare la bacheca?", "delete-board": "Elimina bacheca", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Sottocompiti per la bacheca __board__", "default": "Predefinito", "defaultdefault": "Predefinito", diff --git a/imports/i18n/data/ja-HI.i18n.json b/imports/i18n/data/ja-HI.i18n.json index 1f3b6d7b19..db428e93a2 100644 --- a/imports/i18n/data/ja-HI.i18n.json +++ b/imports/i18n/data/ja-HI.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ja.i18n.json b/imports/i18n/data/ja.i18n.json index d69fc4d274..3f6a29f132 100644 --- a/imports/i18n/data/ja.i18n.json +++ b/imports/i18n/data/ja.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "すべてのリスト、カード、ラベル、アクティビティは削除され、ボードの内容を元に戻すことができません。", "boardDeletePopup-title": "ボードを削除しますか?", "delete-board": "ボードを削除", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "__board__ ボードのサブタスク", "default": "デフォルト", "defaultdefault": "デフォルト", diff --git a/imports/i18n/data/ka.i18n.json b/imports/i18n/data/ka.i18n.json index 001b2ff8c8..0e41f26714 100644 --- a/imports/i18n/data/ka.i18n.json +++ b/imports/i18n/data/ka.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "ყველა ჩამონათვალი, ბარათი, ნიშანი და აქტივობა წაიშლება და თქვენ ვეღარ შეძლებთ მის აღდგენას.", "boardDeletePopup-title": "წავშალოთ დაფა?", "delete-board": "დაფის წაშლა", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "ქვესაქმიანობა __board__ დაფისთვის", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/km.i18n.json b/imports/i18n/data/km.i18n.json index 04a544ca73..99d757f8b1 100644 --- a/imports/i18n/data/km.i18n.json +++ b/imports/i18n/data/km.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ko-KR.i18n.json b/imports/i18n/data/ko-KR.i18n.json index c447ad6815..284d8e20b0 100644 --- a/imports/i18n/data/ko-KR.i18n.json +++ b/imports/i18n/data/ko-KR.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ko.i18n.json b/imports/i18n/data/ko.i18n.json index c423e1b7a6..62f508b82a 100644 --- a/imports/i18n/data/ko.i18n.json +++ b/imports/i18n/data/ko.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "모든 목록, 카드, 레이블 및 활동이 삭제되고 보드 내용을 복구할 수 없습니다. 실행 취소는 불가능합니다.", "boardDeletePopup-title": "보드 삭제?", "delete-board": "보드 삭제", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "기본", "defaultdefault": "기본", diff --git a/imports/i18n/data/lt.i18n.json b/imports/i18n/data/lt.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/lt.i18n.json +++ b/imports/i18n/data/lt.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/lv.i18n.json b/imports/i18n/data/lv.i18n.json index deaddc65b3..d051fb8a50 100644 --- a/imports/i18n/data/lv.i18n.json +++ b/imports/i18n/data/lv.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Visi saraksti, kartiņas, birkas un darbības tiks dzēstas un dēli nevarēs atgūt. Darbība nav atsaucama.", "boardDeletePopup-title": "Dzēst dēli?", "delete-board": "Dzēst dēli", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Apakšuzdevumi priekš __board__ dēļa", "default": "Noklusēts", "defaultdefault": "Noklusēts", diff --git a/imports/i18n/data/mk.i18n.json b/imports/i18n/data/mk.i18n.json index 14eb56f73a..491feeb6e6 100644 --- a/imports/i18n/data/mk.i18n.json +++ b/imports/i18n/data/mk.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Изтриване на Таблото?", "delete-board": "Изтрий таблото", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Подзадачи за табло __board__", "default": "по подразбиране", "defaultdefault": "по подразбиране", diff --git a/imports/i18n/data/mn.i18n.json b/imports/i18n/data/mn.i18n.json index 7ef8ae586c..de46279643 100644 --- a/imports/i18n/data/mn.i18n.json +++ b/imports/i18n/data/mn.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ms-MY.i18n.json b/imports/i18n/data/ms-MY.i18n.json index 2112ab947d..e1ea29b8a5 100644 --- a/imports/i18n/data/ms-MY.i18n.json +++ b/imports/i18n/data/ms-MY.i18n.json @@ -92,10 +92,10 @@ "list-width-error-message": "Lebar senarai mestilah integer lebih besar dari 100", "keyboard-shortcuts-enabled": "Pintasan papan kekunci didayakan. Klik untuk batal.", "keyboard-shortcuts-disabled": "Pintasan papan kekunci didayakan. Klik untuk batal.", - "setSwimlaneHeightPopup-title": "Set Swimlane Height", - "set-swimlane-height": "Set Swimlane Height", - "set-swimlane-height-value": "Swimlane Height (pixels)", - "swimlane-height-error-message": "Swimlane height must be a positive integer", + "setSwimlaneHeightPopup-title": "Tetapkan ketinggian Swimlane", + "set-swimlane-height": "Tetapkan ketinggian Swimlane", + "set-swimlane-height-value": "Ketinggian Swimlane (piksel)", + "swimlane-height-error-message": "Ketinggian Swimlane mestilah integer positif", "add-swimlane": "Add Swimlane", "add-subtask": "Add Subtask", "add-checklist": "Add Checklist", @@ -103,10 +103,10 @@ "close-add-checklist-item": "Close add an item to checklist form", "close-edit-checklist-item": "Close edit an item to checklist form", "convertChecklistItemToCardPopup-title": "Convert to Card", - "add-cover": "Add cover image to minicard", + "add-cover": "Tambah imej depan pada kad mini", "add-label": "Add Label", "add-list": "Tambah Senarai", - "add-after-list": "Add After List", + "add-after-list": "Tambah selepas senarai", "add-members": "Tambah Ahli", "added": "Ditambah", "addMemberPopup-title": "Ahli-ahli", @@ -125,7 +125,7 @@ "archive": "Move to Archive", "archive-all": "Move All to Archive", "archive-board": "Move Board to Archive", - "archive-board-confirm": "Are you sure you want to archive this board?", + "archive-board-confirm": "Anda pasti untuk mengarkibkan papan ini?", "archive-card": "Move Card to Archive", "archive-list": "Move List to Archive", "archive-swimlane": "Move Swimlane to Archive", @@ -147,13 +147,13 @@ "attachmentDeletePopup-title": "Delete Attachment?", "attachments": "Attachments", "auto-watch": "Automatically watch boards when they are created", - "avatar-too-big": "The avatar is too large (__size__ max)", + "avatar-too-big": "Saiz Avatar terlalu besar (__size__max)", "back": "Back", "board-change-color": "Change color", - "board-change-background-image": "Change Background Image", - "board-background-image-url": "Background Image URL", - "add-background-image": "Add Background Image", - "remove-background-image": "Remove Background Image", + "board-change-background-image": "Ubah imej latar", + "board-background-image-url": "URL imej latar", + "add-background-image": "Tambah imej latar", + "remove-background-image": "Hapus imej latar", "show-at-all-boards-page" : "Show at All Boards page", "board-info-on-my-boards" : "All Boards Settings", "boardInfoOnMyBoardsPopup-title" : "All Boards Settings", @@ -166,9 +166,9 @@ "board-public-info": "This board will be public.", "board-drag-drop-reorder-or-click-open": "Drag and drop to reorder board icons. Click board icon to open board.", "boardChangeColorPopup-title": "Change Board Background", - "boardChangeBackgroundImagePopup-title": "Change Background Image", + "boardChangeBackgroundImagePopup-title": "Ubah imej latar", "allBoardsChangeColorPopup-title": "Change color", - "allBoardsChangeBackgroundImagePopup-title": "Change Background Image", + "allBoardsChangeBackgroundImagePopup-title": "Ubah imej latar", "boardChangeTitlePopup-title": "Rename Board", "boardChangeVisibilityPopup-title": "Change Visibility", "boardChangeWatchPopup-title": "Change Watch", @@ -177,22 +177,22 @@ "boardChangeViewPopup-title": "Board View", "boards": "Boards", "board-view": "Board View", - "desktop-mode": "Desktop Mode", - "mobile-mode": "Mobile Mode", - "mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode", - "zoom-in": "Zoom In", - "zoom-out": "Zoom Out", - "click-to-change-zoom": "Click to change zoom level", - "zoom-level": "Zoom Level", - "enter-zoom-level": "Enter zoom level (50-300%):", + "desktop-mode": "Mod Desktop", + "mobile-mode": "Mod Mudah Alih", + "mobile-desktop-toggle": "Tukar antara mod mudah alih dan desktop", + "zoom-in": "Zum masuk", + "zoom-out": "Zum keluar", + "click-to-change-zoom": "Klik untuk ubah aras zum", + "zoom-level": "Aras zum", + "enter-zoom-level": "Masukkan aras zum (50-300%)", "board-view-cal": "Calendar", "board-view-swimlanes": "Swimlanes", "board-view-collapse": "Collapse", "board-view-gantt": "Gantt", "board-view-lists": "Lists", - "bucket-example": "Like \"Bucket List\" for example", - "calendar-previous-month-label": "Previous Month", - "calendar-next-month-label": "Next Month", + "bucket-example": "Contohnya \"Bucket List\"", + "calendar-previous-month-label": "Bulan Sebelum", + "calendar-next-month-label": "Bulan Depan", "cancel": "Cancel", "card-archived": "This card is moved to Archive.", "board-archived": "This board is moved to Archive.", @@ -277,9 +277,9 @@ "checklists": "Checklists", "click-to-star": "Click to star this board.", "click-to-unstar": "Click to unstar this board.", - "click-to-enable-auto-width": "Auto list width disabled. Click to enable.", - "click-to-disable-auto-width": "Auto list width enabled. Click to disable.", - "auto-list-width": "Auto list width", + "click-to-enable-auto-width": "Lebar senarai lalai tidak didayakan. Klik untuk dayakan.", + "click-to-disable-auto-width": "Lebar senarai lalai didayakan. Klik untuk tidak didayakan.", + "auto-list-width": "Lebar senarai lalai.", "clipboard": "Clipboard or drag & drop", "close": "Close", "close-board": "Close Board", @@ -311,7 +311,7 @@ "color-white": "white", "color-yellow": "yellow", "unset-color": "Unset", - "comments": "Comments", + "comments": "Komen", "comment": "Comment", "comment-placeholder": "Write Comment", "comment-only": "Comment only", @@ -356,10 +356,10 @@ "custom-field-text": "Text", "custom-fields": "Custom Fields", "date": "Date", - "date-format": "Date Format", - "date-format-yyyy-mm-dd": "YYYY-MM-DD", - "date-format-dd-mm-yyyy": "DD-MM-YYYY", - "date-format-mm-dd-yyyy": "MM-DD-YYYY", + "date-format": "Format Tarikh", + "date-format-yyyy-mm-dd": "TTTT-BB-HH", + "date-format-dd-mm-yyyy": "HH-BB-TTTT", + "date-format-mm-dd-yyyy": "BB-HH-TTTT", "decline": "Decline", "default-avatar": "Default avatar", "delete": "Delete", @@ -385,7 +385,7 @@ "editNotificationPopup-title": "Edit Notification", "editProfilePopup-title": "Edit Profile", "email": "Email", - "email-address": "Email Address", + "email-address": "Alamat Email", "email-enrollAccount-subject": "An account created for you on __siteName__", "email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.", "email-fail": "Sending email failed", @@ -399,14 +399,14 @@ "email-sent": "Email sent", "email-verifyEmail-subject": "Verify your email address on __siteName__", "email-verifyEmail-text": "Hello __user__,\n\nTo verify your account email, simply click the link below.\n\n__url__\n\nThanks.", - "enable-vertical-scrollbars": "Enable vertical scrollbars", + "enable-vertical-scrollbars": "Dayakan bar skrol melintang", "enable-wip-limit": "Enable WIP Limit", "error-board-doesNotExist": "This board does not exist", "error-board-notAdmin": "You need to be admin of this board to do that", "error-board-notAMember": "You need to be a member of this board to do that", "error-json-malformed": "Your text is not valid JSON", "error-json-schema": "Your JSON data does not include the proper information in the correct format", - "error-csv-schema": "Your CSV(Comma Separated Values)/TSV (Tab Separated Values) does not include the proper information in the correct format ", + "error-csv-schema": "CSV(Comma Separated Values)/TSV (Tab Separated Values) anda tidak mengikut format yang betul dengan maklumat yang bersesuaian.", "error-list-doesNotExist": "This list does not exist", "error-user-doesNotExist": "This user does not exist", "error-user-notAllowSelf": "You can not invite yourself", @@ -445,7 +445,7 @@ "filter-overdue": "Overdue", "filter-due-today": "Due today", "filter-due-this-week": "Due this week", - "filter-due-next-week": "Due next week", + "filter-due-next-week": "Tamat minggu depan", "filter-due-tomorrow": "Due tomorrow", "list-filter-label": "Filter List by Title", "filter-clear": "Clear filter", @@ -574,7 +574,7 @@ "public": "Public", "public-desc": "This board is public. It's visible to anyone with the link and will show up in search engines like Google. Only people added to the board can edit.", "quick-access-description": "Star a board to add a shortcut in this bar.", - "remove-cover": "Remove cover image from minicard", + "remove-cover": "Hapus imej depan daripada kad mini", "remove-from-board": "Remove from Board", "remove-label": "Remove Label", "listDeletePopup-title": "Delete List ?", @@ -596,14 +596,14 @@ "select-board": "Select Board", "set-wip-limit-value": "Set a limit for the maximum number of tasks in this list", "setWipLimitPopup-title": "Set WIP Limit", - "shortcut-add-self": "Add yourself to current card", + "shortcut-add-self": "Tambah diri sendiri kepada kad ini", "shortcut-assign-self": "Assign yourself to current card", "shortcut-autocomplete-emoji": "Autocomplete emoji", "shortcut-autocomplete-members": "Autocomplete members", "shortcut-clear-filters": "Clear all filters", "shortcut-close-dialog": "Close Dialog", "shortcut-filter-my-cards": "Filter my cards", - "shortcut-filter-my-assigned-cards": "Filter my assigned cards", + "shortcut-filter-my-assigned-cards": "Tapis kad sendiri", "shortcut-show-shortcuts": "Bring up this shortcuts list", "shortcut-toggle-filterbar": "Toggle Filter Sidebar", "shortcut-toggle-searchbar": "Toggle Search Sidebar", @@ -638,9 +638,9 @@ "upload": "Upload", "upload-avatar": "Upload an avatar", "uploaded-avatar": "Uploaded an avatar", - "uploading-files": "Uploading files", - "upload-failed": "Upload failed", - "upload-completed": "Upload completed", + "uploading-files": "Muat naik fail", + "upload-failed": "Muat naik gagal", + "upload-completed": "Muat naik selesai", "custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL", "custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL", "custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27", @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", @@ -769,9 +771,9 @@ "deposit-subtasks-list": "Landing list for subtasks deposited here:", "show-parent-in-minicard": "Show parent in minicard:", "description-on-minicard": "Description on minicard", - "cover-attachment-on-minicard": "Cover image on minicard", - "badge-attachment-on-minicard": "Count of attachments on minicard", - "card-sorting-by-number-on-minicard": "Card sorting by number on minicard", + "cover-attachment-on-minicard": "Imej depan kad mini", + "badge-attachment-on-minicard": "Bilangan lampiran pada kad mini", + "card-sorting-by-number-on-minicard": "Kad disusun mengikut nombor pada kad mini", "prefix-with-full-path": "Prefix with full path", "prefix-with-parent": "Prefix with parent", "subtext-with-full-path": "Subtext with full path", @@ -884,7 +886,7 @@ "r-items-list": "item1,item2,item3", "r-add-swimlane": "Add swimlane", "r-swimlane-name": "swimlane name", - "r-board-note": "Note: leave a field empty to match every possible value. ", + "r-board-note": "Nota: Tinggalkan ruang kosong untuk padankan setiap nilai yang mungkin.", "r-checklist-note": "Note: checklist's items have to be written as comma separated values.", "r-when-a-card-is-moved": "When a card is moved to another list", "r-set": "Set", @@ -915,10 +917,10 @@ "oidc-button-text": "Customize the OIDC button text", "default-authentication-method": "Default Authentication Method", "duplicate-board": "Duplicate Board", - "duplicate-board-confirm": "Are you sure you want to duplicate this board?", - "org-number": "The number of organizations is: ", - "team-number": "The number of teams is: ", - "people-number": "The number of people is: ", + "duplicate-board-confirm": "Anda pasti untuk menduplikasi papan ini?", + "org-number": "Jumlah Organisasi ialah:", + "team-number": "Jumlah kumpulan ialah:", + "people-number": "Jumlah individu ialah:", "swimlaneDeletePopup-title": "Delete Swimlane ?", "swimlane-delete-pop": "All actions will be removed from the activity feed and you won't be able to recover the swimlane. There is no undo.", "restore-all": "Restore all", @@ -986,7 +988,7 @@ "delete-linked-card-before-this-card": "You can not delete this card before first deleting linked card that has", "delete-linked-cards-before-this-list": "You can not delete this list before first deleting linked cards that are pointing to cards in this list", "hide-checked-items": "Hide checked items", - "hide-finished-checklist": "Hide finished checklist", + "hide-finished-checklist": "Sembunyikan senarai semak yang telah selesai", "task": "Task", "create-task": "Create Task", "ok": "OK", @@ -994,7 +996,7 @@ "teams": "Teams", "displayName": "Nama Paparan", "shortName": "Nama Ringkas", - "autoAddUsersWithDomainName": "Automatically add users with the domain name", + "autoAddUsersWithDomainName": "Tambah pengguna dengan nama domain ini secara lalai", "website": "Laman Sesawang", "person": "Person", "my-cards": "Kad Saya", @@ -1016,7 +1018,7 @@ "dueCardsViewChange-choice-me": "Saya", "dueCardsViewChange-choice-all": "Semua Pengguna", "dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.", - "dueCards-noResults-title": "No Due Cards Found", + "dueCards-noResults-title": "Tiada Kad Tamat Ditemui", "dueCards-noResults-description": "You don't have any cards with due dates at the moment.", "broken-cards": "Broken Cards", "board-title-not-found": "Board '%s' not found.", diff --git a/imports/i18n/data/ms.i18n.json b/imports/i18n/data/ms.i18n.json index 4f030d480e..ad26e0f97a 100644 --- a/imports/i18n/data/ms.i18n.json +++ b/imports/i18n/data/ms.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Semua senarai, kad, label dan aktiviti akan dihapus dan anda tidak akan dapat pulihkan semula kandungan papan. Tiada undur semula", "boardDeletePopup-title": "Hapus Papan?", "delete-board": "Hapus papan", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "subtugas untuk __board__ papan", "default": "Lalai", "defaultdefault": "Lalai", diff --git a/imports/i18n/data/nb.i18n.json b/imports/i18n/data/nb.i18n.json index 58958c9f4c..f3fb0f0ed0 100644 --- a/imports/i18n/data/nb.i18n.json +++ b/imports/i18n/data/nb.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Alle lister, kort, etiketter og aktiviteter vil bli slettet og du vil ikke kunne gjenopprette innholdet på tavlen. Det er ikke mulig å angre.", "boardDeletePopup-title": "Slett Tavle?", "delete-board": "Slett Tavle", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Underoppgave for __board__ tavle", "default": "Standard", "defaultdefault": "Standard", diff --git a/imports/i18n/data/nl-NL.i18n.json b/imports/i18n/data/nl-NL.i18n.json index a5d645f170..e1e932403a 100644 --- a/imports/i18n/data/nl-NL.i18n.json +++ b/imports/i18n/data/nl-NL.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Alle lijsten, kaarten, labels en activiteiten zullen worden verwijderd en je kunt de bordinhoud niet terughalen. Er is geen herstelmogelijkheid.", "boardDeletePopup-title": "Bord verwijderen?", "delete-board": "Verwijder bord", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtaken voor __board__ bord", "default": "Standaard", "defaultdefault": "Standaard", diff --git a/imports/i18n/data/nl.i18n.json b/imports/i18n/data/nl.i18n.json index 4dd27955da..e854597fd8 100644 --- a/imports/i18n/data/nl.i18n.json +++ b/imports/i18n/data/nl.i18n.json @@ -357,9 +357,9 @@ "custom-fields": "Maatwerkvelden", "date": "Datum", "date-format": "Datumformaat", - "date-format-yyyy-mm-dd": "YYYY-MM-DD", - "date-format-dd-mm-yyyy": "DD-MM-YYYY", - "date-format-mm-dd-yyyy": "MM-DD-YYYY", + "date-format-yyyy-mm-dd": "JJJJ-MM-DD", + "date-format-dd-mm-yyyy": "DD-MM-JJJJ", + "date-format-mm-dd-yyyy": "MM-DD-JJJJ", "decline": "Weigeren", "default-avatar": "Standaard avatar", "delete": "Verwijderen", @@ -385,7 +385,7 @@ "editNotificationPopup-title": "Wijzig notificatie", "editProfilePopup-title": "Wijzig profiel", "email": "E-mail", - "email-address": "Email Address", + "email-address": "Emailadres", "email-enrollAccount-subject": "Er is een account voor je aangemaakt op __siteName__", "email-enrollAccount-text": "Hallo __user__,\n\nOm gebruik te maken van de online dienst, kan je op de volgende link klikken.\n\n__url__\n\nBedankt.", "email-fail": "E-mail verzenden is mislukt", @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Alle lijsten, kaarten, labels en activiteiten zullen worden verwijderd en je kunt de bordinhoud niet terughalen. Er is geen herstelmogelijkheid.", "boardDeletePopup-title": "Bord verwijderen?", "delete-board": "Verwijder bord", + "delete-duplicate-lists": "Verwijder Dubbele Lijsten", + "delete-duplicate-lists-confirm": "Weet je het zeker? Alle dubbele lijsten die dezelfde naam hebben en geen kaarten bevatten worden verwijderd.", "default-subtasks-board": "Subtaken voor __board__ bord", "default": "Standaard", "defaultdefault": "Standaard", @@ -1016,8 +1018,8 @@ "dueCardsViewChange-choice-me": "Mij", "dueCardsViewChange-choice-all": "Alle gebruikers", "dueCardsViewChange-choice-all-description": "Toon incomplete kaarten met een *achterstallige* datum van borden waarvoor de gebruiker toegang heeft.", - "dueCards-noResults-title": "No Due Cards Found", - "dueCards-noResults-description": "You don't have any cards with due dates at the moment.", + "dueCards-noResults-title": "Geen Achterstallige Kaarten Gevonden", + "dueCards-noResults-description": "Je hebt nu geen kaarten met achterstallige datums.", "broken-cards": "Defecte kaarten", "board-title-not-found": "Bord '%s' niet gevonden.", "swimlane-title-not-found": "Swimlane '%s' niet gevonden.", diff --git a/imports/i18n/data/oc.i18n.json b/imports/i18n/data/oc.i18n.json index 2b85e2c673..5da5ae2bc6 100644 --- a/imports/i18n/data/oc.i18n.json +++ b/imports/i18n/data/oc.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Suprimir lo tablèu ?", "delete-board": "Tablèu suprimit", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/or_IN.i18n.json b/imports/i18n/data/or_IN.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/or_IN.i18n.json +++ b/imports/i18n/data/or_IN.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/pa.i18n.json b/imports/i18n/data/pa.i18n.json index 510b05b6b5..1ab8eeaa84 100644 --- a/imports/i18n/data/pa.i18n.json +++ b/imports/i18n/data/pa.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/pl-PL.i18n.json b/imports/i18n/data/pl-PL.i18n.json index 555e519a07..641328e6d5 100644 --- a/imports/i18n/data/pl-PL.i18n.json +++ b/imports/i18n/data/pl-PL.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Wszystkie listy, etykiety oraz aktywności zostaną usunięte i nie będziesz w stanie przywrócić zawartości tablicy. Tego nie da się cofnąć.", "boardDeletePopup-title": "Usunąć tablicę?", "delete-board": "Usuń tablicę", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Podzadania dla tablicy __board__", "default": "Domyślny", "defaultdefault": "Domyślny", diff --git a/imports/i18n/data/pl.i18n.json b/imports/i18n/data/pl.i18n.json index 79250ae108..7ae12a506d 100644 --- a/imports/i18n/data/pl.i18n.json +++ b/imports/i18n/data/pl.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Wszystkie listy, etykiety oraz aktywności zostaną usunięte i nie będziesz w stanie przywrócić zawartości tablicy. Tego nie da się cofnąć.", "boardDeletePopup-title": "Usunąć tablicę?", "delete-board": "Usuń tablicę", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Podzadania dla tablicy __board__", "default": "Domyślny", "defaultdefault": "Domyślny", diff --git a/imports/i18n/data/pt-BR.i18n.json b/imports/i18n/data/pt-BR.i18n.json index 6b8cbd923e..5a135f543d 100644 --- a/imports/i18n/data/pt-BR.i18n.json +++ b/imports/i18n/data/pt-BR.i18n.json @@ -357,9 +357,9 @@ "custom-fields": "Campos customizados", "date": "Data", "date-format": "Formato da Data", - "date-format-yyyy-mm-dd": "YYYY-MM-DD", - "date-format-dd-mm-yyyy": "DD-MM-YYYY", - "date-format-mm-dd-yyyy": "MM-DD-YYYY", + "date-format-yyyy-mm-dd": "AAAA-MM-DD", + "date-format-dd-mm-yyyy": "DD-MM-AAAA", + "date-format-mm-dd-yyyy": "MM-DD-AAAA", "decline": "Rejeitar", "default-avatar": "Avatar padrão", "delete": "Excluir", @@ -385,7 +385,7 @@ "editNotificationPopup-title": "Editar Notificações", "editProfilePopup-title": "Editar Perfil", "email": "E-mail", - "email-address": "Email Address", + "email-address": "Endereço de e-mail", "email-enrollAccount-subject": "Uma conta foi criada para você em __siteName__", "email-enrollAccount-text": "Olá __user__\npara iniciar utilizando o serviço basta clicar no link abaixo.\n__url__\nMuito Obrigado.", "email-fail": "Falhou ao enviar e-mail", @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Todas as listas, cartões, etiquetas e atividades serão excluídas e você não poderá recuperar o conteúdo do quadro. Não há como desfazer.", "boardDeletePopup-title": "Excluir quadro?", "delete-board": "Excluir quadro", + "delete-duplicate-lists": "Excluir Listas Duplicadas", + "delete-duplicate-lists-confirm": "Você tem certeza? Isso vai apagar todas as litas duplicadas que possuem o mesmo nome e que não possuem cartões", "default-subtasks-board": "Subtarefas para quadro __board__", "default": "Padrão", "defaultdefault": "Padrão", @@ -1016,8 +1018,8 @@ "dueCardsViewChange-choice-me": "Eu", "dueCardsViewChange-choice-all": "Todos os usuários", "dueCardsViewChange-choice-all-description": "Mostrar todos os cartões incompletos com *Prazo Final* nos quadros em que o usuário tem permissão", - "dueCards-noResults-title": "No Due Cards Found", - "dueCards-noResults-description": "You don't have any cards with due dates at the moment.", + "dueCards-noResults-title": "Sem Cartões com prazo final", + "dueCards-noResults-description": "Você não possui cartões com prazo final neste momento", "broken-cards": "Cartões quebrados", "board-title-not-found": "Quadro '%s' não encontrado.", "swimlane-title-not-found": "Raia '%s' não encontrada.", diff --git a/imports/i18n/data/pt.i18n.json b/imports/i18n/data/pt.i18n.json index 9cd9017c48..1f228c838c 100644 --- a/imports/i18n/data/pt.i18n.json +++ b/imports/i18n/data/pt.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Todas as listas, cartões, etiquetas e atividades serão apagadas e não poderá recuperar o conteúdo do quadro. Não é reversível.", "boardDeletePopup-title": "Apagar Quadro?", "delete-board": "Apagar Quadro", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Sub-tarefas para o quadro __board__", "default": "Omissão", "defaultdefault": "Omissão", diff --git a/imports/i18n/data/pt_PT.i18n.json b/imports/i18n/data/pt_PT.i18n.json index 1ce34dfd85..fa36c3204a 100644 --- a/imports/i18n/data/pt_PT.i18n.json +++ b/imports/i18n/data/pt_PT.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Todas as listas, cartões, etiquetas e atividades serão apagadas e não poderá recuperar o conteúdo do quadro. Não é reversível.", "boardDeletePopup-title": "Apagar Quadro?", "delete-board": "Apagar Quadro", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Sub-tarefas para o quadro __board__", "default": "Omissão", "defaultdefault": "Omissão", diff --git a/imports/i18n/data/ro-RO.i18n.json b/imports/i18n/data/ro-RO.i18n.json index 6d125e0e03..0adfeaf0e4 100644 --- a/imports/i18n/data/ro-RO.i18n.json +++ b/imports/i18n/data/ro-RO.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ro.i18n.json b/imports/i18n/data/ro.i18n.json index d2952f1063..5fbd58448b 100644 --- a/imports/i18n/data/ro.i18n.json +++ b/imports/i18n/data/ro.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ru-UA.i18n.json b/imports/i18n/data/ru-UA.i18n.json index b1ca45606b..055c85cdc3 100644 --- a/imports/i18n/data/ru-UA.i18n.json +++ b/imports/i18n/data/ru-UA.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ru.i18n.json b/imports/i18n/data/ru.i18n.json index a80e4f894e..354f21a238 100644 --- a/imports/i18n/data/ru.i18n.json +++ b/imports/i18n/data/ru.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Все списки, карточки, метки и действия будут удалены, и вы не сможете восстановить содержимое доски. Отменить нельзя.", "boardDeletePopup-title": "Удалить доску?", "delete-board": "Удалить доску", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Подзадача для доски __board__", "default": "По умолчанию", "defaultdefault": "По умолчанию", diff --git a/imports/i18n/data/sk.i18n.json b/imports/i18n/data/sk.i18n.json index bd84a2e77f..4eff2776c6 100644 --- a/imports/i18n/data/sk.i18n.json +++ b/imports/i18n/data/sk.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Zmazať nástenku?", "delete-board": "Zmazať nástenku", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/sl.i18n.json b/imports/i18n/data/sl.i18n.json index 84c16407f1..7c7f0d6847 100644 --- a/imports/i18n/data/sl.i18n.json +++ b/imports/i18n/data/sl.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Vsi seznami, kartice, oznake in dejavnosti bodo izbrisani in vsebine table ne boste mogli obnoviti. Razveljavitve ni.", "boardDeletePopup-title": "Izbriši tablo?", "delete-board": "Izbriši tablo", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Podopravila za tablo", "default": "Privzeto", "defaultdefault": "Privzeto", diff --git a/imports/i18n/data/sr.i18n.json b/imports/i18n/data/sr.i18n.json index 1bf947d881..2022488c12 100644 --- a/imports/i18n/data/sr.i18n.json +++ b/imports/i18n/data/sr.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Све деонице, картице са задацима, налепнице и радње биће избачене и нећете моћи да повратите садржај књиге пословања. Опозив ове радње неће бити могућ.", "boardDeletePopup-title": "Избацићете пословну књигу?", "delete-board": "Избаци пословну књигу", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Под задаци за __board__ књигу пословања", "default": "Подразумевано", "defaultdefault": "Подразумевано", diff --git a/imports/i18n/data/sv.i18n.json b/imports/i18n/data/sv.i18n.json index 12289c8794..6a4e1616dc 100644 --- a/imports/i18n/data/sv.i18n.json +++ b/imports/i18n/data/sv.i18n.json @@ -357,9 +357,9 @@ "custom-fields": "Anpassade fält", "date": "Datum", "date-format": "Datumformat", - "date-format-yyyy-mm-dd": "YYYY-MM-DD", - "date-format-dd-mm-yyyy": "DD-MM-YYYY", - "date-format-mm-dd-yyyy": "MM-DD-YYYY", + "date-format-yyyy-mm-dd": "ÅÅÅÅ-MM-DD", + "date-format-dd-mm-yyyy": "DD-MM-ÅÅÅÅ", + "date-format-mm-dd-yyyy": "MM-DD-ÅÅÅÅ", "decline": "Neka", "default-avatar": "Standard avatar", "delete": "Ta bort", @@ -385,7 +385,7 @@ "editNotificationPopup-title": "Redigera notis", "editProfilePopup-title": "Redigera profil", "email": "E-post", - "email-address": "Email Address", + "email-address": "E-postadress", "email-enrollAccount-subject": "Ett konto skapat för dig på __siteName__", "email-enrollAccount-text": "Hej __user__,\n\nFör att börja använda tjänsten, klicka på länken nedan.\n\n__url__\n\nTack!", "email-fail": "Sändning av e-post misslyckades", @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Alla listor, kort, etiketter och aktiviteter kommer tas bort och du kommer inte kunna återställa tavlans innehåll. Det går inte att ångra.", "boardDeletePopup-title": "Ta bort tavla?", "delete-board": "Ta bort tavla", + "delete-duplicate-lists": "Ta bort dubblettlistor", + "delete-duplicate-lists-confirm": "Är du säker? Detta kommer att ta bort alla dubblettlistor som har samma namn och inte innehåller några kort.", "default-subtasks-board": "Deluppgifter för __board__ board", "default": "Standard", "defaultdefault": "Standard", @@ -1016,8 +1018,8 @@ "dueCardsViewChange-choice-me": "Jag", "dueCardsViewChange-choice-all": "Alla användare", "dueCardsViewChange-choice-all-description": "Visar alla oklara kort med *förfallo* datum från tavlor som användaren har tillgång till.", - "dueCards-noResults-title": "No Due Cards Found", - "dueCards-noResults-description": "You don't have any cards with due dates at the moment.", + "dueCards-noResults-title": " Inga kort med förfallodatum hittades", + "dueCards-noResults-description": "Du har inga kort med förfallodatum just nu.", "broken-cards": "Trasiga kort", "board-title-not-found": "Tavla '%s' hittades inte.", "swimlane-title-not-found": "Simbana '%s' hittades inte.", @@ -1328,10 +1330,10 @@ "add-cron-job-placeholder": "Funktionen för att lägga till schemalagda jobb kommer snart", "attachment-storage-configuration": "Konfiguration av fillagringsplats", "attachments-path": " Sökväg för bilagor", - "attachments-path-description": "Path where attachment files are stored", - "avatars-path": "Avatars Path", - "avatars-path-description": "Path where avatar files are stored", - "board-archive-failed": "Failed to schedule board archive", + "attachments-path-description": "Sökväg där bifogade filer lagras", + "avatars-path": "Sökväg för avatarer", + "avatars-path-description": "Sökväg där avatarfiler lagras", + "board-archive-failed": "Misslyckades att schemalägga arkivering av tavla", "board-archive-scheduled": "Board archive scheduled successfully", "board-backup-failed": "Failed to schedule board backup", "board-backup-scheduled": "Board backup scheduled successfully", diff --git a/imports/i18n/data/sw.i18n.json b/imports/i18n/data/sw.i18n.json index 565cf03f90..ae5c41a0d4 100644 --- a/imports/i18n/data/sw.i18n.json +++ b/imports/i18n/data/sw.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ta.i18n.json b/imports/i18n/data/ta.i18n.json index 285d23de6c..c2e38c9eb2 100644 --- a/imports/i18n/data/ta.i18n.json +++ b/imports/i18n/data/ta.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/te-IN.i18n.json b/imports/i18n/data/te-IN.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/te-IN.i18n.json +++ b/imports/i18n/data/te-IN.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/th.i18n.json b/imports/i18n/data/th.i18n.json index 963f73cca5..fc992cb5d6 100644 --- a/imports/i18n/data/th.i18n.json +++ b/imports/i18n/data/th.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/tk_TM.i18n.json b/imports/i18n/data/tk_TM.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/tk_TM.i18n.json +++ b/imports/i18n/data/tk_TM.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/tlh.i18n.json b/imports/i18n/data/tlh.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/tlh.i18n.json +++ b/imports/i18n/data/tlh.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/tr.i18n.json b/imports/i18n/data/tr.i18n.json index 418fc19179..a4d61f39c5 100644 --- a/imports/i18n/data/tr.i18n.json +++ b/imports/i18n/data/tr.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Tüm listeler, kartlar, etiketler ve etkinlikler silinecek ve pano içeriğini kurtaramayacaksınız. Geri dönüş yok.", "boardDeletePopup-title": "Panoyu Sil?", "delete-board": "Panoyu Sil", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "__board__ panosu için alt görevler", "default": "Varsayılan", "defaultdefault": "Varsayılan", diff --git a/imports/i18n/data/ug.i18n.json b/imports/i18n/data/ug.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/ug.i18n.json +++ b/imports/i18n/data/ug.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/uk-UA.i18n.json b/imports/i18n/data/uk-UA.i18n.json index 4e7e492d8b..037a7a3e55 100644 --- a/imports/i18n/data/uk-UA.i18n.json +++ b/imports/i18n/data/uk-UA.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Усі списки, картки, мітки та діяльність будуть видалені, і ви не зможете відновити вміст дошки. Немає відкату.", "boardDeletePopup-title": "Видалити дошку?", "delete-board": "Видалити дошку", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Підзадачі для дошки __board__", "default": "За замовчуванням", "defaultdefault": "За замовчуванням", diff --git a/imports/i18n/data/uk.i18n.json b/imports/i18n/data/uk.i18n.json index 222a2e60fe..e1d01b105b 100644 --- a/imports/i18n/data/uk.i18n.json +++ b/imports/i18n/data/uk.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Усі списки, картки, мітки та діяльність будуть видалені, і ви не зможете відновити вміст дошки. Немає відкату.", "boardDeletePopup-title": "Видалити дошку?", "delete-board": "Видалити дошку", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Підзадачі для дошки __board__", "default": "За замовчуванням", "defaultdefault": "За замовчуванням", diff --git a/imports/i18n/data/uz-AR.i18n.json b/imports/i18n/data/uz-AR.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/uz-AR.i18n.json +++ b/imports/i18n/data/uz-AR.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/uz-LA.i18n.json b/imports/i18n/data/uz-LA.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/uz-LA.i18n.json +++ b/imports/i18n/data/uz-LA.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/uz-UZ.i18n.json b/imports/i18n/data/uz-UZ.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/uz-UZ.i18n.json +++ b/imports/i18n/data/uz-UZ.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/uz.i18n.json b/imports/i18n/data/uz.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/uz.i18n.json +++ b/imports/i18n/data/uz.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ve-CC.i18n.json b/imports/i18n/data/ve-CC.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/ve-CC.i18n.json +++ b/imports/i18n/data/ve-CC.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ve-PP.i18n.json b/imports/i18n/data/ve-PP.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/ve-PP.i18n.json +++ b/imports/i18n/data/ve-PP.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/ve.i18n.json b/imports/i18n/data/ve.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/ve.i18n.json +++ b/imports/i18n/data/ve.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/vi-VN.i18n.json b/imports/i18n/data/vi-VN.i18n.json index bef9464f7f..9dbb74fa43 100644 --- a/imports/i18n/data/vi-VN.i18n.json +++ b/imports/i18n/data/vi-VN.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/vi.i18n.json b/imports/i18n/data/vi.i18n.json index 702b0e2c53..8843c5833a 100644 --- a/imports/i18n/data/vi.i18n.json +++ b/imports/i18n/data/vi.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "Tất cả danh sách, thẻ, nhãn và hoạt động sẽ bị xóa và bạn sẽ không thể khôi phục nội dung bảng. Không thể hoàn tác.", "boardDeletePopup-title": "Xoá Bảng?", "delete-board": "Xoá Bảng", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Nhiệm vụ phụ cho __board__ bảng", "default": "Mặc định", "defaultdefault": "Mặc định", diff --git a/imports/i18n/data/vl-SS.i18n.json b/imports/i18n/data/vl-SS.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/vl-SS.i18n.json +++ b/imports/i18n/data/vl-SS.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/vo.i18n.json b/imports/i18n/data/vo.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/vo.i18n.json +++ b/imports/i18n/data/vo.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/wa-RR.i18n.json b/imports/i18n/data/wa-RR.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/wa-RR.i18n.json +++ b/imports/i18n/data/wa-RR.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/wa.i18n.json b/imports/i18n/data/wa.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/wa.i18n.json +++ b/imports/i18n/data/wa.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/wo.i18n.json b/imports/i18n/data/wo.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/wo.i18n.json +++ b/imports/i18n/data/wo.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/wuu-Hans.i18n.json b/imports/i18n/data/wuu-Hans.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/wuu-Hans.i18n.json +++ b/imports/i18n/data/wuu-Hans.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/xh.i18n.json b/imports/i18n/data/xh.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/xh.i18n.json +++ b/imports/i18n/data/xh.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/yi.i18n.json b/imports/i18n/data/yi.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/yi.i18n.json +++ b/imports/i18n/data/yi.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/yo.i18n.json b/imports/i18n/data/yo.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/yo.i18n.json +++ b/imports/i18n/data/yo.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/yue_CN.i18n.json b/imports/i18n/data/yue_CN.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/yue_CN.i18n.json +++ b/imports/i18n/data/yue_CN.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/zgh.i18n.json b/imports/i18n/data/zgh.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/zgh.i18n.json +++ b/imports/i18n/data/zgh.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/zh-CN.i18n.json b/imports/i18n/data/zh-CN.i18n.json index a52be28036..2f3dba6334 100644 --- a/imports/i18n/data/zh-CN.i18n.json +++ b/imports/i18n/data/zh-CN.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "所有列表、卡片、标签和活动都回被删除,将无法恢复看板内容。不支持撤销。", "boardDeletePopup-title": "删除看板?", "delete-board": "删除看板", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "__board__ 看板的子任务", "default": "默认", "defaultdefault": "默认", diff --git a/imports/i18n/data/zh-GB.i18n.json b/imports/i18n/data/zh-GB.i18n.json index 0c4f978f70..151bcf1fd6 100644 --- a/imports/i18n/data/zh-GB.i18n.json +++ b/imports/i18n/data/zh-GB.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/zh-HK.i18n.json b/imports/i18n/data/zh-HK.i18n.json index 1e13b42dae..5c71d764d2 100644 --- a/imports/i18n/data/zh-HK.i18n.json +++ b/imports/i18n/data/zh-HK.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/zh-Hans.i18n.json b/imports/i18n/data/zh-Hans.i18n.json index 3cb94e011c..1938e05903 100644 --- a/imports/i18n/data/zh-Hans.i18n.json +++ b/imports/i18n/data/zh-Hans.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/zh-Hant.i18n.json b/imports/i18n/data/zh-Hant.i18n.json index bcdd000614..c1d5ed8b7c 100644 --- a/imports/i18n/data/zh-Hant.i18n.json +++ b/imports/i18n/data/zh-Hant.i18n.json @@ -1,6 +1,6 @@ { - "accept": "Accept", - "act-activity-notify": "Activity Notification", + "accept": "接受", + "act-activity-notify": "動態通知", "act-addAttachment": "added attachment __attachment__ to card __card__ at list __list__ at swimlane __swimlane__ at board __board__", "act-deleteAttachment": "deleted attachment __attachment__ at card __card__ at list __list__ at swimlane __swimlane__ at board __board__", "act-addSubtask": "added subtask __subtask__ to card __card__ at list __list__ at swimlane __swimlane__ at board __board__", @@ -48,7 +48,7 @@ "activity-added": "added %s to %s", "activity-archived": "%s moved to Archive", "activity-attached": "attached %s to %s", - "activity-created": "created %s", + "activity-created": "已建立", "activity-changedListTitle": "renamed list to %s", "activity-customfield-created": "created custom field %s", "activity-excluded": "excluded %s from %s", @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/zh-TW.i18n.json b/imports/i18n/data/zh-TW.i18n.json index 383163ae03..754785cd26 100644 --- a/imports/i18n/data/zh-TW.i18n.json +++ b/imports/i18n/data/zh-TW.i18n.json @@ -385,7 +385,7 @@ "editNotificationPopup-title": "更改通知", "editProfilePopup-title": "編輯個人資料", "email": "電子郵件", - "email-address": "Email Address", + "email-address": "電子郵件地址", "email-enrollAccount-subject": "您在 __siteName__ 的帳號已經建立", "email-enrollAccount-text": "親愛的 __user__,\n\n點選下面的連結,即刻開始使用這項服務。\n\n__url__\n\n謝謝。", "email-fail": "郵件寄送失敗", @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "所有清單、卡片、標籤和活動都會被刪除,將無法恢覆看板內容。不支援撤銷。", "boardDeletePopup-title": "刪除看板?", "delete-board": "刪除看板", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "__board__ 看板的子任務", "default": "預設值", "defaultdefault": "預設值", @@ -1016,8 +1018,8 @@ "dueCardsViewChange-choice-me": "我", "dueCardsViewChange-choice-all": "全部使用者", "dueCardsViewChange-choice-all-description": "顯示看板內所有已設定到期日,且使用者有權限的未完成卡片", - "dueCards-noResults-title": "No Due Cards Found", - "dueCards-noResults-description": "You don't have any cards with due dates at the moment.", + "dueCards-noResults-title": "找不到到期卡片", + "dueCards-noResults-description": "您目前沒有任何有到期日的卡片。", "broken-cards": "損毀卡片", "board-title-not-found": "看板%s不存在", "swimlane-title-not-found": "泳道流程圖%s不存在", diff --git a/imports/i18n/data/zh.i18n.json b/imports/i18n/data/zh.i18n.json index 39eb3f485c..78fd22be49 100644 --- a/imports/i18n/data/zh.i18n.json +++ b/imports/i18n/data/zh.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/zu-ZA.i18n.json b/imports/i18n/data/zu-ZA.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/zu-ZA.i18n.json +++ b/imports/i18n/data/zu-ZA.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", diff --git a/imports/i18n/data/zu.i18n.json b/imports/i18n/data/zu.i18n.json index bcdd000614..eef27e1fe8 100644 --- a/imports/i18n/data/zu.i18n.json +++ b/imports/i18n/data/zu.i18n.json @@ -755,6 +755,8 @@ "delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.", "boardDeletePopup-title": "Delete Board?", "delete-board": "Delete Board", + "delete-duplicate-lists": "Delete Duplicate Lists", + "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", "default-subtasks-board": "Subtasks for __board__ board", "default": "Default", "defaultdefault": "Default", From 034dc08269520ca31c780cce64e0150969e9228e Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Sat, 25 Oct 2025 19:17:09 +0300 Subject: [PATCH 10/17] Disabled migrations that happen when opening board. Defaulting to per-swimlane lists and drag drop list to same or different swimlane. Thanks to xet7 ! --- client/components/boards/boardBody.js | 77 +++++++++++++----------- client/components/lists/list.css | 3 - client/components/swimlanes/swimlanes.js | 17 +++--- models/swimlanes.js | 8 ++- 4 files changed, 57 insertions(+), 48 deletions(-) diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index 5eb317b9bf..a9b04cddbd 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -99,51 +99,60 @@ BlazeComponent.extendComponent({ } // Check if board needs migration based on migration version - const needsMigration = !board.migrationVersion || board.migrationVersion < 1; + // DISABLED: Migration check and execution + // const needsMigration = !board.migrationVersion || board.migrationVersion < 1; - if (needsMigration) { - // Start background migration for old boards - this.isMigrating.set(true); - await this.startBackgroundMigration(boardId); - this.isMigrating.set(false); - } + // if (needsMigration) { + // // Start background migration for old boards + // this.isMigrating.set(true); + // await this.startBackgroundMigration(boardId); + // this.isMigrating.set(false); + // } // Check if board needs conversion (for old structure) - if (boardConverter.isBoardConverted(boardId)) { - if (process.env.DEBUG === 'true') { - console.log(`Board ${boardId} has already been converted, skipping conversion`); - } - this.isBoardReady.set(true); - } else { - const needsConversion = boardConverter.needsConversion(boardId); - - if (needsConversion) { - this.isConverting.set(true); - const success = await boardConverter.convertBoard(boardId); - this.isConverting.set(false); - - if (success) { - this.isBoardReady.set(true); - } else { - console.error('Board conversion failed, setting ready to true anyway'); - this.isBoardReady.set(true); // Still show board even if conversion failed - } - } else { - this.isBoardReady.set(true); - } - } + // DISABLED: Board conversion logic + // if (boardConverter.isBoardConverted(boardId)) { + // if (process.env.DEBUG === 'true') { + // console.log(`Board ${boardId} has already been converted, skipping conversion`); + // } + // this.isBoardReady.set(true); + // } else { + // const needsConversion = boardConverter.needsConversion(boardId); + // + // if (needsConversion) { + // this.isConverting.set(true); + // const success = await boardConverter.convertBoard(boardId); + // this.isConverting.set(false); + // + // if (success) { + // this.isBoardReady.set(true); + // } else { + // console.error('Board conversion failed, setting ready to true anyway'); + // this.isBoardReady.set(true); // Still show board even if conversion failed + // } + // } else { + // this.isBoardReady.set(true); + // } + // } + + // Set board ready immediately since conversions are disabled + this.isBoardReady.set(true); // Convert shared lists to per-swimlane lists if needed - await this.convertSharedListsToPerSwimlane(boardId); + // DISABLED: Shared lists conversion + // await this.convertSharedListsToPerSwimlane(boardId); // Fix missing lists migration (for cards with wrong listId references) - await this.fixMissingLists(boardId); + // DISABLED: Missing lists fix + // await this.fixMissingLists(boardId); // Fix duplicate lists created by WeKan 8.10 - await this.fixDuplicateLists(boardId); + // DISABLED: Duplicate lists fix + // await this.fixDuplicateLists(boardId); // Start attachment migration in background if needed - this.startAttachmentMigrationIfNeeded(boardId); + // DISABLED: Attachment migration + // this.startAttachmentMigrationIfNeeded(boardId); } catch (error) { console.error('Error during board conversion check:', error); this.isConverting.set(false); diff --git a/client/components/lists/list.css b/client/components/lists/list.css index 7c238efcd2..53426199b4 100644 --- a/client/components/lists/list.css +++ b/client/components/lists/list.css @@ -378,9 +378,6 @@ body.list-resizing-active * { position: relative; text-overflow: ellipsis; white-space: nowrap; -} -.list-header .list-rotated { - } .list-header .list-header-watch-icon { padding-left: 10px; diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js index 47b9c58434..e0dd896d53 100644 --- a/client/components/swimlanes/swimlanes.js +++ b/client/components/swimlanes/swimlanes.js @@ -228,10 +228,8 @@ function initSortable(boardComponent, $listsDom) { // Don't cancel the sortable when moving to a different swimlane // The DOM move should be allowed to complete - } else { - // If staying in the same swimlane, cancel the sortable to prevent DOM manipulation issues - $listsDom.sortable('cancel'); } + // Allow reordering within the same swimlane by not canceling the sortable try { Lists.update(list._id, { @@ -682,6 +680,11 @@ Template.swimlane.helpers({ canSeeAddList() { return ReactiveCache.getCurrentUser().isBoardAdmin(); }, + + lists() { + // Return per-swimlane lists for this swimlane + return this.myLists(); + } }); // Initialize sortable on DOM elements @@ -794,10 +797,8 @@ setTimeout(() => { // Don't cancel the sortable when moving to a different swimlane // The DOM move should be allowed to complete - } else { - // If staying in the same swimlane, cancel the sortable to prevent DOM manipulation issues - $swimlane.sortable('cancel'); } + // Allow reordering within the same swimlane by not canceling the sortable try { Lists.update(list._id, { @@ -938,10 +939,8 @@ setTimeout(() => { // Don't cancel the sortable when moving to a different swimlane // The DOM move should be allowed to complete - } else { - // If staying in the same swimlane, cancel the sortable to prevent DOM manipulation issues - $listsGroup.sortable('cancel'); } + // Allow reordering within the same swimlane by not canceling the sortable try { Lists.update(list._id, { diff --git a/models/swimlanes.js b/models/swimlanes.js index 012e56f21b..659111d04b 100644 --- a/models/swimlanes.js +++ b/models/swimlanes.js @@ -232,8 +232,12 @@ Swimlanes.helpers({ }, myLists() { - // Revert to shared lists: provide lists by board for this swimlane's board - return ReactiveCache.getLists({ boardId: this.boardId }); + // Return per-swimlane lists: provide lists specific to this swimlane + return ReactiveCache.getLists({ + boardId: this.boardId, + swimlaneId: this._id, + archived: false + }); }, allCards() { From 0c99cb3103fec61cedde25b41a4a5ce30777f9dc Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Sat, 25 Oct 2025 19:19:35 +0300 Subject: [PATCH 11/17] Updated ChangeLog. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffef70a57f..8c590aacca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ This release adds the following new features: - [List menu / More / Delete duplicate lists that do not have any cards](https://github.com/wekan/wekan/commit/91b846e2cdee9154b045d11b4b4c1a7ae1d79016). Thanks to xet7. +- [Disabled migrations that happen when opening board. Defaulting to per-swimlane lists and drag drop list to same or different swimlane](https://github.com/wekan/wekan/commit/034dc08269520ca31c780cce64e0150969e9228e). + Thanks to xet7. Thanks to above GitHub users for their contributions and translators for their translations. From ecf2418347cae4329deb292b534f68eb099d3f90 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Sat, 25 Oct 2025 19:23:35 +0300 Subject: [PATCH 12/17] Fix changing swimlane color to not reload webpage. Thanks to xet7 ! --- client/components/swimlanes/swimlaneHeader.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/components/swimlanes/swimlaneHeader.js b/client/components/swimlanes/swimlaneHeader.js index 17988f4543..c0ef354532 100644 --- a/client/components/swimlanes/swimlaneHeader.js +++ b/client/components/swimlanes/swimlaneHeader.js @@ -178,6 +178,11 @@ BlazeComponent.extendComponent({ events() { return [ { + 'submit form'(event) { + event.preventDefault(); + this.currentSwimlane.setColor(this.currentColor.get()); + Popup.back(); + }, 'click .js-palette-color'() { this.currentColor.set(this.currentData().color); }, From bccc22c5fedd5374f8fe78195c289c958e4a6171 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Sat, 25 Oct 2025 19:25:04 +0300 Subject: [PATCH 13/17] Updated ChangeLog. --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c590aacca..a8fe3555fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,11 @@ This release adds the following new features: - [Disabled migrations that happen when opening board. Defaulting to per-swimlane lists and drag drop list to same or different swimlane](https://github.com/wekan/wekan/commit/034dc08269520ca31c780cce64e0150969e9228e). Thanks to xet7. +and fixes the following bugs: + +- [Fix changing swimlane color to not reload webpage](https://github.com/wekan/wekan/commit/ecf2418347cae4329deb292b534f68eb099d3f90). + Thanks to xet7. + Thanks to above GitHub users for their contributions and translators for their translations. # v8.15 2025-10-23 WeKan ® release From 30620d0ca4b750582429cba18d8c676b2191e57a Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Sat, 25 Oct 2025 21:09:07 +0300 Subject: [PATCH 14/17] Some migrations and mobile fixes. Thanks to xet7 ! --- client/components/boards/boardBody.css | 46 +- client/components/boards/boardBody.js | 202 +++-- client/components/boards/boardHeader.css | 126 +-- client/components/lists/list.css | 66 +- client/components/migrationProgress.css | 425 ++++------ client/components/migrationProgress.jade | 98 +-- client/components/migrationProgress.js | 234 +++++- client/components/swimlanes/swimlanes.css | 2 +- client/components/users/userAvatar.jade | 2 +- client/components/users/userAvatar.js | 4 +- models/attachments.js | 12 + models/avatars.js | 16 +- models/lib/universalUrlGenerator.js | 194 +++++ server/00checkStartup.js | 6 + server/cors.js | 15 + .../migrations/comprehensiveBoardMigration.js | 767 ++++++++++++++++++ server/migrations/fixAllFileUrls.js | 277 +++++++ server/migrations/fixAvatarUrls.js | 128 +++ server/routes/avatarServer.js | 123 +++ server/routes/universalFileServer.js | 393 +++++++++ 20 files changed, 2616 insertions(+), 520 deletions(-) create mode 100644 models/lib/universalUrlGenerator.js create mode 100644 server/migrations/comprehensiveBoardMigration.js create mode 100644 server/migrations/fixAllFileUrls.js create mode 100644 server/migrations/fixAvatarUrls.js create mode 100644 server/routes/avatarServer.js create mode 100644 server/routes/universalFileServer.js diff --git a/client/components/boards/boardBody.css b/client/components/boards/boardBody.css index 05fa8fc58a..f65cbaffc9 100644 --- a/client/components/boards/boardBody.css +++ b/client/components/boards/boardBody.css @@ -269,57 +269,71 @@ } /* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */ .board-wrapper.mobile-view { - width: 100% !important; - min-width: 100% !important; + width: 100vw !important; + max-width: 100vw !important; + min-width: 100vw !important; left: 0 !important; right: 0 !important; + overflow-x: hidden !important; + overflow-y: auto !important; } .board-wrapper.mobile-view .board-canvas { - width: 100% !important; - min-width: 100% !important; + width: 100vw !important; + max-width: 100vw !important; + min-width: 100vw !important; left: 0 !important; right: 0 !important; + overflow-x: hidden !important; + overflow-y: auto !important; } .board-wrapper.mobile-view .board-canvas.mobile-view .swimlane { border-bottom: 1px solid #ccc; - display: flex; + display: block !important; flex-direction: column; margin: 0; padding: 0; - overflow-x: hidden; + overflow-x: hidden !important; overflow-y: auto; - width: 100%; - min-width: 100%; + width: 100vw !important; + max-width: 100vw !important; + min-width: 100vw !important; } @media screen and (max-width: 800px), screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) { .board-wrapper { - width: 100% !important; - min-width: 100% !important; + width: 100vw !important; + max-width: 100vw !important; + min-width: 100vw !important; left: 0 !important; right: 0 !important; + overflow-x: hidden !important; + overflow-y: auto !important; } .board-wrapper .board-canvas { - width: 100% !important; - min-width: 100% !important; + width: 100vw !important; + max-width: 100vw !important; + min-width: 100vw !important; left: 0 !important; right: 0 !important; + overflow-x: hidden !important; + overflow-y: auto !important; } .board-wrapper .board-canvas .swimlane { border-bottom: 1px solid #ccc; - display: flex; + display: block !important; flex-direction: column; margin: 0; padding: 0; - overflow-x: hidden; + overflow-x: hidden !important; overflow-y: auto; - width: 100%; - min-width: 100%; + width: 100vw !important; + max-width: 100vw !important; + min-width: 100vw !important; } } .calendar-event-green { diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index a9b04cddbd..e8e83a134b 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -4,6 +4,7 @@ import dragscroll from '@wekanteam/dragscroll'; import { boardConverter } from '/client/lib/boardConverter'; import { migrationManager } from '/client/lib/migrationManager'; import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager'; +import { migrationProgressManager } from '/client/components/migrationProgress'; import Swimlanes from '/models/swimlanes'; import Lists from '/models/lists'; @@ -98,61 +99,25 @@ BlazeComponent.extendComponent({ return; } - // Check if board needs migration based on migration version - // DISABLED: Migration check and execution - // const needsMigration = !board.migrationVersion || board.migrationVersion < 1; + // Check if board needs comprehensive migration + const needsMigration = await this.checkComprehensiveMigration(boardId); - // if (needsMigration) { - // // Start background migration for old boards - // this.isMigrating.set(true); - // await this.startBackgroundMigration(boardId); - // this.isMigrating.set(false); - // } - - // Check if board needs conversion (for old structure) - // DISABLED: Board conversion logic - // if (boardConverter.isBoardConverted(boardId)) { - // if (process.env.DEBUG === 'true') { - // console.log(`Board ${boardId} has already been converted, skipping conversion`); - // } - // this.isBoardReady.set(true); - // } else { - // const needsConversion = boardConverter.needsConversion(boardId); - // - // if (needsConversion) { - // this.isConverting.set(true); - // const success = await boardConverter.convertBoard(boardId); - // this.isConverting.set(false); - // - // if (success) { - // this.isBoardReady.set(true); - // } else { - // console.error('Board conversion failed, setting ready to true anyway'); - // this.isBoardReady.set(true); // Still show board even if conversion failed - // } - // } else { - // this.isBoardReady.set(true); - // } - // } - - // Set board ready immediately since conversions are disabled - this.isBoardReady.set(true); - - // Convert shared lists to per-swimlane lists if needed - // DISABLED: Shared lists conversion - // await this.convertSharedListsToPerSwimlane(boardId); - - // Fix missing lists migration (for cards with wrong listId references) - // DISABLED: Missing lists fix - // await this.fixMissingLists(boardId); - - // Fix duplicate lists created by WeKan 8.10 - // DISABLED: Duplicate lists fix - // await this.fixDuplicateLists(boardId); + if (needsMigration) { + // Start comprehensive migration + this.isMigrating.set(true); + const success = await this.executeComprehensiveMigration(boardId); + this.isMigrating.set(false); + + if (success) { + this.isBoardReady.set(true); + } else { + console.error('Comprehensive migration failed, setting ready to true anyway'); + this.isBoardReady.set(true); // Still show board even if migration failed + } + } else { + this.isBoardReady.set(true); + } - // Start attachment migration in background if needed - // DISABLED: Attachment migration - // this.startAttachmentMigrationIfNeeded(boardId); } catch (error) { console.error('Error during board conversion check:', error); this.isConverting.set(false); @@ -161,6 +126,137 @@ BlazeComponent.extendComponent({ } }, + /** + * Check if board needs comprehensive migration + */ + async checkComprehensiveMigration(boardId) { + try { + return new Promise((resolve, reject) => { + Meteor.call('comprehensiveBoardMigration.needsMigration', boardId, (error, result) => { + if (error) { + console.error('Error checking comprehensive migration:', error); + reject(error); + } else { + resolve(result); + } + }); + }); + } catch (error) { + console.error('Error checking comprehensive migration:', error); + return false; + } + }, + + /** + * Execute comprehensive migration for a board + */ + async executeComprehensiveMigration(boardId) { + try { + // Start progress tracking + migrationProgressManager.startMigration(); + + // Simulate progress updates since we can't easily pass callbacks through Meteor methods + const progressSteps = [ + { step: 'analyze_board_structure', name: 'Analyze Board Structure', duration: 1000 }, + { step: 'fix_orphaned_cards', name: 'Fix Orphaned Cards', duration: 2000 }, + { step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 3000 }, + { step: 'ensure_per_swimlane_lists', name: 'Ensure Per-Swimlane Lists', duration: 1500 }, + { step: 'cleanup_empty_lists', name: 'Cleanup Empty Lists', duration: 1000 }, + { step: 'validate_migration', name: 'Validate Migration', duration: 1000 }, + { step: 'fix_avatar_urls', name: 'Fix Avatar URLs', duration: 1000 }, + { step: 'fix_attachment_urls', name: 'Fix Attachment URLs', duration: 1000 } + ]; + + // Start the actual migration + const migrationPromise = new Promise((resolve, reject) => { + Meteor.call('comprehensiveBoardMigration.execute', boardId, (error, result) => { + if (error) { + console.error('Error executing comprehensive migration:', error); + migrationProgressManager.failMigration(error); + reject(error); + } else { + if (process.env.DEBUG === 'true') { + console.log('Comprehensive migration completed for board:', boardId, result); + } + resolve(result.success); + } + }); + }); + + // Simulate progress updates + const progressPromise = this.simulateMigrationProgress(progressSteps); + + // Wait for both to complete + const [migrationResult] = await Promise.all([migrationPromise, progressPromise]); + + migrationProgressManager.completeMigration(); + return migrationResult; + + } catch (error) { + console.error('Error executing comprehensive migration:', error); + migrationProgressManager.failMigration(error); + return false; + } + }, + + /** + * Simulate migration progress updates + */ + async simulateMigrationProgress(progressSteps) { + const totalSteps = progressSteps.length; + + for (let i = 0; i < progressSteps.length; i++) { + const step = progressSteps[i]; + const stepProgress = Math.round(((i + 1) / totalSteps) * 100); + + // Update progress for this step + migrationProgressManager.updateProgress({ + overallProgress: stepProgress, + currentStep: i + 1, + totalSteps, + stepName: step.step, + stepProgress: 0, + stepStatus: `Starting ${step.name}...`, + stepDetails: null, + boardId: Session.get('currentBoard') + }); + + // Simulate step progress + const stepDuration = step.duration; + const updateInterval = 100; // Update every 100ms + const totalUpdates = stepDuration / updateInterval; + + for (let j = 0; j < totalUpdates; j++) { + const stepStepProgress = Math.round(((j + 1) / totalUpdates) * 100); + + migrationProgressManager.updateProgress({ + overallProgress: stepProgress, + currentStep: i + 1, + totalSteps, + stepName: step.step, + stepProgress: stepStepProgress, + stepStatus: `Processing ${step.name}...`, + stepDetails: { progress: `${stepStepProgress}%` }, + boardId: Session.get('currentBoard') + }); + + await new Promise(resolve => setTimeout(resolve, updateInterval)); + } + + // Complete the step + migrationProgressManager.updateProgress({ + overallProgress: stepProgress, + currentStep: i + 1, + totalSteps, + stepName: step.step, + stepProgress: 100, + stepStatus: `${step.name} completed`, + stepDetails: { status: 'completed' }, + boardId: Session.get('currentBoard') + }); + } + }, + async startBackgroundMigration(boardId) { try { // Start background migration using the cron system diff --git a/client/components/boards/boardHeader.css b/client/components/boards/boardHeader.css index f3cb652e76..faf20e2f51 100644 --- a/client/components/boards/boardHeader.css +++ b/client/components/boards/boardHeader.css @@ -505,73 +505,73 @@ flex-wrap: nowrap !important; align-items: stretch !important; justify-content: flex-start !important; - width: 100% !important; - max-width: 100% !important; - min-width: 100% !important; + width: 100vw !important; + max-width: 100vw !important; + min-width: 100vw !important; overflow-x: hidden !important; overflow-y: auto !important; } -.mobile-mode .swimlane { - display: block !important; - width: 100% !important; - max-width: 100% !important; - min-width: 100% !important; - margin: 0 0 2rem 0 !important; - padding: 0 !important; - float: none !important; - clear: both !important; -} + .mobile-mode .swimlane { + display: block !important; + width: 100vw !important; + max-width: 100vw !important; + min-width: 100vw !important; + margin: 0 0 2rem 0 !important; + padding: 0 !important; + float: none !important; + clear: both !important; + } -.mobile-mode .swimlane .swimlane-header { - display: block !important; - width: 100% !important; - max-width: 100% !important; - min-width: 100% !important; - margin: 0 0 1rem 0 !important; - padding: 1rem !important; - font-size: clamp(18px, 2.5vw, 32px) !important; - font-weight: bold !important; - border-bottom: 2px solid #ccc !important; -} + .mobile-mode .swimlane .swimlane-header { + display: block !important; + width: 100vw !important; + max-width: 100vw !important; + min-width: 100vw !important; + margin: 0 0 1rem 0 !important; + padding: 1rem !important; + font-size: clamp(18px, 2.5vw, 32px) !important; + font-weight: bold !important; + border-bottom: 2px solid #ccc !important; + } -.mobile-mode .swimlane .lists { - display: block !important; - width: 100% !important; - max-width: 100% !important; - min-width: 100% !important; - margin: 0 !important; - padding: 0 !important; - flex-direction: column !important; - flex-wrap: nowrap !important; - align-items: stretch !important; - justify-content: flex-start !important; -} + .mobile-mode .swimlane .lists { + display: block !important; + width: 100vw !important; + max-width: 100vw !important; + min-width: 100vw !important; + margin: 0 !important; + padding: 0 !important; + flex-direction: column !important; + flex-wrap: nowrap !important; + align-items: stretch !important; + justify-content: flex-start !important; + } -.mobile-mode .list { - display: block !important; - width: 100% !important; - max-width: 100% !important; - min-width: 100% !important; - margin: 0 0 2rem 0 !important; - padding: 0 !important; - float: none !important; - clear: both !important; - border-left: none !important; - border-right: none !important; - border-top: none !important; - border-bottom: 2px solid #ccc !important; - flex: none !important; - flex-basis: auto !important; - flex-grow: 0 !important; - flex-shrink: 0 !important; - position: static !important; - left: auto !important; - right: auto !important; - top: auto !important; - bottom: auto !important; - transform: none !important; -} + .mobile-mode .list { + display: block !important; + width: 100vw !important; + max-width: 100vw !important; + min-width: 100vw !important; + margin: 0 0 2rem 0 !important; + padding: 0 !important; + float: none !important; + clear: both !important; + border-left: none !important; + border-right: none !important; + border-top: none !important; + border-bottom: 2px solid #ccc !important; + flex: none !important; + flex-basis: auto !important; + flex-grow: 0 !important; + flex-shrink: 0 !important; + position: static !important; + left: auto !important; + right: auto !important; + top: auto !important; + bottom: auto !important; + transform: none !important; + } .mobile-mode .list:first-child { margin-left: 0 !important; @@ -667,9 +667,9 @@ flex-wrap: nowrap !important; align-items: stretch !important; justify-content: flex-start !important; - width: 100% !important; - max-width: 100% !important; - min-width: 100% !important; + width: 100vw !important; + max-width: 100vw !important; + min-width: 100vw !important; overflow-x: hidden !important; overflow-y: auto !important; } diff --git a/client/components/lists/list.css b/client/components/lists/list.css index 53426199b4..77e78de299 100644 --- a/client/components/lists/list.css +++ b/client/components/lists/list.css @@ -641,17 +641,22 @@ body.list-resizing-active * { .mini-list.mobile-view { flex: 0 0 60px; height: auto; - width: 100%; - min-width: 100%; + width: 100vw; + max-width: 100vw; + min-width: 100vw; border-left: 0px !important; border-bottom: 1px solid #ccc; + display: block !important; } .list.mobile-view { - display: contents; + display: block !important; flex-basis: auto; - width: 100%; - min-width: 100%; + width: 100vw; + max-width: 100vw; + min-width: 100vw; border-left: 0px !important; + margin: 0 !important; + padding: 0 !important; } .list.mobile-view:first-child { margin-left: 0px; @@ -659,9 +664,11 @@ body.list-resizing-active * { .list.mobile-view.ui-sortable-helper { flex: 0 0 60px; height: 60px; - width: 100%; + width: 100vw; + max-width: 100vw; border-left: 0px !important; border-bottom: 1px solid #ccc; + display: block !important; } .list.mobile-view.ui-sortable-helper .list-header.ui-sortable-handle { cursor: grabbing; @@ -669,14 +676,17 @@ body.list-resizing-active * { .list.mobile-view.placeholder { flex: 0 0 60px; height: 60px; - width: 100%; + width: 100vw; + max-width: 100vw; border-left: 0px !important; border-bottom: 1px solid #ccc; + display: block !important; } .list.mobile-view .list-body { padding: 15px 19px; - width: 100%; - min-width: 100%; + width: 100vw; + max-width: 100vw; + min-width: 100vw; } .list.mobile-view .list-header { /*Updated padding values for mobile devices, this should fix text grouping issue*/ @@ -685,8 +695,9 @@ body.list-resizing-active * { min-height: 30px; margin-top: 10px; align-items: center; - width: 100%; - min-width: 100%; + width: 100vw; + max-width: 100vw; + min-width: 100vw; /* Force grid layout for iPhone */ display: grid !important; grid-template-columns: 30px 1fr auto auto !important; @@ -767,17 +778,22 @@ body.list-resizing-active * { .mini-list { flex: 0 0 60px; height: auto; - width: 100%; - min-width: 100%; + width: 100vw; + max-width: 100vw; + min-width: 100vw; border-left: 0px !important; border-bottom: 1px solid #ccc; + display: block !important; } .list { - display: contents; + display: block !important; flex-basis: auto; - width: 100%; - min-width: 100%; + width: 100vw; + max-width: 100vw; + min-width: 100vw; border-left: 0px !important; + margin: 0 !important; + padding: 0 !important; } .list:first-child { margin-left: 0px; @@ -785,9 +801,11 @@ body.list-resizing-active * { .list.ui-sortable-helper { flex: 0 0 60px; height: 60px; - width: 100%; + width: 100vw; + max-width: 100vw; border-left: 0px !important; border-bottom: 1px solid #ccc; + display: block !important; } .list.ui-sortable-helper .list-header.ui-sortable-handle { cursor: grabbing; @@ -795,14 +813,17 @@ body.list-resizing-active * { .list.placeholder { flex: 0 0 60px; height: 60px; - width: 100%; + width: 100vw; + max-width: 100vw; border-left: 0px !important; border-bottom: 1px solid #ccc; + display: block !important; } .list-body { padding: 15px 19px; - width: 100%; - min-width: 100%; + width: 100vw; + max-width: 100vw; + min-width: 100vw; } .list-header { /*Updated padding values for mobile devices, this should fix text grouping issue*/ @@ -811,8 +832,9 @@ body.list-resizing-active * { min-height: 30px; margin-top: 10px; align-items: center; - width: 100%; - min-width: 100%; + width: 100vw; + max-width: 100vw; + min-width: 100vw; } .list-header .list-header-left-icon { padding: 7px; diff --git a/client/components/migrationProgress.css b/client/components/migrationProgress.css index d44f4eda89..f3b9a45d44 100644 --- a/client/components/migrationProgress.css +++ b/client/components/migrationProgress.css @@ -1,38 +1,33 @@ /* Migration Progress Styles */ -.migration-overlay { +.migration-progress-overlay { position: fixed; top: 0; left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.8); - z-index: 10000; - display: none; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + z-index: 9999; + display: flex; align-items: center; justify-content: center; - overflow-y: auto; + backdrop-filter: blur(2px); } -.migration-overlay.active { - display: flex; -} - -.migration-modal { +.migration-progress-modal { background: white; - border-radius: 12px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); - max-width: 800px; - width: 95%; - max-height: 90vh; + border-radius: 8px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + max-width: 500px; + width: 90%; + max-height: 80vh; overflow: hidden; - animation: slideInScale 0.4s ease-out; - margin: 20px; + animation: migrationModalSlideIn 0.3s ease-out; } -@keyframes slideInScale { +@keyframes migrationModalSlideIn { from { opacity: 0; - transform: translateY(-30px) scale(0.95); + transform: translateY(-20px) scale(0.95); } to { opacity: 1; @@ -40,333 +35,235 @@ } } -.migration-header { - padding: 24px 32px 20px; - border-bottom: 2px solid #e0e0e0; - text-align: center; +.migration-progress-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; + padding: 20px; + display: flex; + justify-content: space-between; + align-items: center; } -.migration-header h3 { - margin: 0 0 8px 0; - font-size: 24px; +.migration-progress-title { + margin: 0; + font-size: 18px; font-weight: 600; } -.migration-header h3 i { - margin-right: 12px; - color: #FFD700; +.migration-progress-close { + cursor: pointer; + font-size: 16px; + opacity: 0.8; + transition: opacity 0.2s ease; } -.migration-header p { - margin: 0; - font-size: 16px; - opacity: 0.9; +.migration-progress-close:hover { + opacity: 1; } -.migration-content { - padding: 24px 32px; - max-height: 60vh; - overflow-y: auto; +.migration-progress-content { + padding: 30px; } -.migration-overview { - margin-bottom: 32px; - padding: 20px; - background: #f8f9fa; - border-radius: 8px; - border-left: 4px solid #667eea; +.migration-progress-overall { + margin-bottom: 25px; } -.overall-progress { - margin-bottom: 20px; +.migration-progress-overall-label { + font-weight: 600; + color: #333; + margin-bottom: 8px; + font-size: 14px; } -.progress-bar { - width: 100%; +.migration-progress-overall-bar { + background: #e9ecef; + border-radius: 10px; height: 12px; - background-color: #e0e0e0; - border-radius: 6px; overflow: hidden; - margin-bottom: 8px; - position: relative; + margin-bottom: 5px; } -.progress-fill { +.migration-progress-overall-fill { + background: linear-gradient(90deg, #28a745, #20c997); height: 100%; - background: linear-gradient(90deg, #667eea, #764ba2); - border-radius: 6px; + border-radius: 10px; transition: width 0.3s ease; position: relative; } -.progress-fill::after { +.migration-progress-overall-fill::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; - background: linear-gradient( - 90deg, - transparent, - rgba(255, 255, 255, 0.4), - transparent - ); - animation: shimmer 2s infinite; -} - -@keyframes shimmer { - 0% { - transform: translateX(-100%); - } - 100% { - transform: translateX(100%); - } -} - -.progress-text { - text-align: center; - font-weight: 700; - color: #667eea; - font-size: 18px; -} - -.progress-label { - text-align: center; - color: #666; - font-size: 14px; - margin-top: 4px; -} - -.current-step { - text-align: center; - color: #333; - font-size: 16px; - font-weight: 500; - margin-bottom: 16px; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + animation: migrationProgressShimmer 2s infinite; } -.current-step i { - margin-right: 8px; - color: #667eea; +@keyframes migrationProgressShimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } } -.estimated-time { - text-align: center; +.migration-progress-overall-percentage { + text-align: right; + font-size: 12px; color: #666; - font-size: 14px; - background-color: #fff3cd; - padding: 8px 12px; - border-radius: 4px; - border: 1px solid #ffeaa7; -} - -.estimated-time i { - margin-right: 6px; - color: #f39c12; -} - -.migration-steps { - margin-bottom: 24px; -} - -.migration-steps h4 { - margin: 0 0 16px 0; - color: #333; - font-size: 18px; font-weight: 600; } -.steps-list { - max-height: 300px; - overflow-y: auto; - border: 1px solid #e0e0e0; - border-radius: 8px; -} - -.migration-step { - padding: 16px 20px; - border-bottom: 1px solid #f0f0f0; - transition: all 0.3s ease; -} - -.migration-step:last-child { - border-bottom: none; -} - -.migration-step.completed { - background-color: #d4edda; - border-left: 4px solid #28a745; -} - -.migration-step.current { - background-color: #cce7ff; - border-left: 4px solid #667eea; - animation: pulse 2s infinite; +.migration-progress-current-step { + margin-bottom: 25px; } -@keyframes pulse { - 0% { - box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4); - } - 70% { - box-shadow: 0 0 0 10px rgba(102, 126, 234, 0); - } - 100% { - box-shadow: 0 0 0 0 rgba(102, 126, 234, 0); - } -} - -.step-header { - display: flex; - align-items: center; - margin-bottom: 8px; -} - -.step-icon { - margin-right: 12px; - font-size: 18px; - width: 24px; - text-align: center; -} - -.step-icon i.fa-check-circle { - color: #28a745; -} - -.step-icon i.fa-cog.fa-spin { - color: #667eea; -} - -.step-icon i.fa-circle-o { - color: #ccc; -} - -.step-info { - flex: 1; -} - -.step-name { +.migration-progress-step-label { font-weight: 600; color: #333; + margin-bottom: 8px; font-size: 14px; - margin-bottom: 2px; } -.step-description { - color: #666; - font-size: 12px; - line-height: 1.3; +.migration-progress-step-bar { + background: #e9ecef; + border-radius: 8px; + height: 8px; + overflow: hidden; + margin-bottom: 5px; } -.step-progress { - text-align: right; - min-width: 40px; +.migration-progress-step-fill { + background: linear-gradient(90deg, #007bff, #0056b3); + height: 100%; + border-radius: 8px; + transition: width 0.3s ease; } -.step-progress .progress-text { +.migration-progress-step-percentage { + text-align: right; font-size: 12px; + color: #666; font-weight: 600; } -.step-progress-bar { - width: 100%; - height: 4px; - background-color: #e0e0e0; - border-radius: 2px; - overflow: hidden; - margin-top: 8px; +.migration-progress-status { + margin-bottom: 20px; + padding: 15px; + background: #f8f9fa; + border-radius: 6px; + border-left: 4px solid #007bff; } -.step-progress-bar .progress-fill { - height: 100%; - background: linear-gradient(90deg, #667eea, #764ba2); - border-radius: 2px; - transition: width 0.3s ease; +.migration-progress-status-label { + font-weight: 600; + color: #333; + margin-bottom: 5px; + font-size: 13px; } -.migration-status { - text-align: center; - color: #333; - font-size: 16px; - background-color: #e3f2fd; - padding: 12px 16px; - border-radius: 6px; - border: 1px solid #bbdefb; - margin-bottom: 16px; +.migration-progress-status-text { + color: #555; + font-size: 14px; + line-height: 1.4; } -.migration-status i { - margin-right: 8px; - color: #2196f3; +.migration-progress-details { + margin-bottom: 20px; + padding: 12px; + background: #e3f2fd; + border-radius: 6px; + border-left: 4px solid #2196f3; } -.migration-footer { - padding: 16px 32px 24px; - border-top: 1px solid #e0e0e0; - background-color: #f8f9fa; +.migration-progress-details-label { + font-weight: 600; + color: #1976d2; + margin-bottom: 5px; + font-size: 13px; } -.migration-info { - text-align: center; - color: #666; +.migration-progress-details-text { + color: #1565c0; font-size: 13px; line-height: 1.4; - margin-bottom: 8px; } -.migration-info i { - margin-right: 6px; - color: #667eea; +.migration-progress-footer { + padding: 20px 30px; + background: #f8f9fa; + border-top: 1px solid #e9ecef; } -.migration-warning { +.migration-progress-note { text-align: center; - color: #856404; - font-size: 12px; - line-height: 1.3; - background-color: #fff3cd; - padding: 8px 12px; - border-radius: 4px; - border: 1px solid #ffeaa7; + color: #666; + font-size: 13px; + font-style: italic; } -.migration-warning i { - margin-right: 6px; - color: #f39c12; +/* Responsive design */ +@media (max-width: 600px) { + .migration-progress-modal { + width: 95%; + margin: 20px; + } + + .migration-progress-content { + padding: 20px; + } + + .migration-progress-header { + padding: 15px; + } + + .migration-progress-title { + font-size: 16px; + } } -/* Responsive design */ -@media (max-width: 768px) { - .migration-modal { - width: 98%; - margin: 10px; +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .migration-progress-modal { + background: #2d3748; + color: #e2e8f0; } - .migration-header, - .migration-content, - .migration-footer { - padding-left: 16px; - padding-right: 16px; + .migration-progress-overall-label, + .migration-progress-step-label, + .migration-progress-status-label { + color: #e2e8f0; } - .migration-header h3 { - font-size: 20px; + .migration-progress-status { + background: #4a5568; + border-left-color: #63b3ed; } - .step-header { - flex-direction: column; - align-items: flex-start; + .migration-progress-status-text { + color: #cbd5e0; } - .step-progress { - text-align: left; - margin-top: 8px; + .migration-progress-details { + background: #2b6cb0; + border-left-color: #4299e1; } - .steps-list { - max-height: 200px; + .migration-progress-details-label { + color: #bee3f8; } -} + + .migration-progress-details-text { + color: #90cdf4; + } + + .migration-progress-footer { + background: #4a5568; + border-top-color: #718096; + } + + .migration-progress-note { + color: #a0aec0; + } +} \ No newline at end of file diff --git a/client/components/migrationProgress.jade b/client/components/migrationProgress.jade index 274ea46214..250e20920b 100644 --- a/client/components/migrationProgress.jade +++ b/client/components/migrationProgress.jade @@ -1,63 +1,43 @@ template(name="migrationProgress") - .migration-overlay(class="{{#if isMigrating}}active{{/if}}") - .migration-modal - .migration-header - h3 - | 🗄️ - | {{_ 'database-migration'}} - p {{_ 'database-migration-description'}} - - .migration-content - .migration-overview - .overall-progress - .progress-bar - .progress-fill(style="width: {{migrationProgress}}%") - .progress-text {{migrationProgress}}% - .progress-label {{_ 'overall-progress'}} + if isMigrating + .migration-progress-overlay + .migration-progress-modal + .migration-progress-header + h3.migration-progress-title + | 🔄 Board Migration in Progress + .migration-progress-close.js-close-migration-progress + | ❌ + + .migration-progress-content + .migration-progress-overall + .migration-progress-overall-label + | Overall Progress: {{currentStep}} of {{totalSteps}} steps + .migration-progress-overall-bar + .migration-progress-overall-fill(style="{{progressBarStyle}}") + .migration-progress-overall-percentage + | {{overallProgress}}% - .current-step - | ⚙️ - | {{migrationCurrentStep}} + .migration-progress-current-step + .migration-progress-step-label + | Current Step: {{stepNameFormatted}} + .migration-progress-step-bar + .migration-progress-step-fill(style="{{stepProgressBarStyle}}") + .migration-progress-step-percentage + | {{stepProgress}}% - .estimated-time(style="{{#unless migrationEstimatedTime}}display: none;{{/unless}}") - | ⏰ - | {{_ 'estimated-time-remaining'}}: {{migrationEstimatedTime}} - - .migration-steps - h4 {{_ 'migration-steps'}} - .steps-list - each migrationSteps - .migration-step(class="{{#if completed}}completed{{/if}}" class="{{#if isCurrentStep}}current{{/if}}") - .step-header - .step-icon - if completed - | ✅ - else if isCurrentStep - | ⚙️ - else - | ⭕ - .step-info - .step-name {{name}} - .step-description {{description}} - .step-progress - if completed - .progress-text 100% - else if isCurrentStep - .progress-text {{progress}}% - else - .progress-text 0% - if isCurrentStep - .step-progress-bar - .progress-fill(style="width: {{progress}}%") + .migration-progress-status + .migration-progress-status-label + | Status: + .migration-progress-status-text + | {{stepStatus}} + + if stepDetailsFormatted + .migration-progress-details + .migration-progress-details-label + | Details: + .migration-progress-details-text + | {{stepDetailsFormatted}} - .migration-status - | ℹ️ - | {{migrationStatus}} - - .migration-footer - .migration-info - | 💡 - | {{_ 'migration-info-text'}} - .migration-warning - | ⚠️ - | {{_ 'migration-warning-text'}} + .migration-progress-footer + .migration-progress-note + | Please wait while we migrate your board to the latest structure... \ No newline at end of file diff --git a/client/components/migrationProgress.js b/client/components/migrationProgress.js index 83a05ea36e..7c4064d39f 100644 --- a/client/components/migrationProgress.js +++ b/client/components/migrationProgress.js @@ -1,54 +1,212 @@ -import { Template } from 'meteor/templating'; -import { - migrationManager, - isMigrating, - migrationProgress, - migrationStatus, - migrationCurrentStep, - migrationEstimatedTime, - migrationSteps -} from '/client/lib/migrationManager'; +/** + * Migration Progress Component + * Displays detailed progress for comprehensive board migration + */ +import { ReactiveVar } from 'meteor/reactive-var'; +import { ReactiveCache } from '/imports/reactiveCache'; + +// Reactive variables for migration progress +export const migrationProgress = new ReactiveVar(0); +export const migrationStatus = new ReactiveVar(''); +export const migrationStepName = new ReactiveVar(''); +export const migrationStepProgress = new ReactiveVar(0); +export const migrationStepStatus = new ReactiveVar(''); +export const migrationStepDetails = new ReactiveVar(null); +export const migrationCurrentStep = new ReactiveVar(0); +export const migrationTotalSteps = new ReactiveVar(0); +export const isMigrating = new ReactiveVar(false); + +class MigrationProgressManager { + constructor() { + this.progressHistory = []; + } + + /** + * Update migration progress + */ + updateProgress(progressData) { + const { + overallProgress, + currentStep, + totalSteps, + stepName, + stepProgress, + stepStatus, + stepDetails, + boardId + } = progressData; + + // Update reactive variables + migrationProgress.set(overallProgress); + migrationCurrentStep.set(currentStep); + migrationTotalSteps.set(totalSteps); + migrationStepName.set(stepName); + migrationStepProgress.set(stepProgress); + migrationStepStatus.set(stepStatus); + migrationStepDetails.set(stepDetails); + + // Store in history + this.progressHistory.push({ + timestamp: new Date(), + ...progressData + }); + + // Update overall status + migrationStatus.set(`${stepName}: ${stepStatus}`); + } + + /** + * Start migration + */ + startMigration() { + isMigrating.set(true); + migrationProgress.set(0); + migrationStatus.set('Starting migration...'); + migrationStepName.set(''); + migrationStepProgress.set(0); + migrationStepStatus.set(''); + migrationStepDetails.set(null); + migrationCurrentStep.set(0); + migrationTotalSteps.set(0); + this.progressHistory = []; + } + + /** + * Complete migration + */ + completeMigration() { + isMigrating.set(false); + migrationProgress.set(100); + migrationStatus.set('Migration completed successfully!'); + + // Clear step details after a delay + setTimeout(() => { + migrationStepName.set(''); + migrationStepProgress.set(0); + migrationStepStatus.set(''); + migrationStepDetails.set(null); + migrationCurrentStep.set(0); + migrationTotalSteps.set(0); + }, 3000); + } + + /** + * Fail migration + */ + failMigration(error) { + isMigrating.set(false); + migrationStatus.set(`Migration failed: ${error.message || error}`); + migrationStepStatus.set('Error occurred'); + } + + /** + * Get progress history + */ + getProgressHistory() { + return this.progressHistory; + } + + /** + * Clear progress + */ + clearProgress() { + isMigrating.set(false); + migrationProgress.set(0); + migrationStatus.set(''); + migrationStepName.set(''); + migrationStepProgress.set(0); + migrationStepStatus.set(''); + migrationStepDetails.set(null); + migrationCurrentStep.set(0); + migrationTotalSteps.set(0); + this.progressHistory = []; + } +} + +// Export singleton instance +export const migrationProgressManager = new MigrationProgressManager(); + +// Template helpers Template.migrationProgress.helpers({ isMigrating() { return isMigrating.get(); }, - - migrationProgress() { + + overallProgress() { return migrationProgress.get(); }, - - migrationStatus() { + + overallStatus() { return migrationStatus.get(); }, - - migrationCurrentStep() { + + currentStep() { return migrationCurrentStep.get(); }, - - migrationEstimatedTime() { - return migrationEstimatedTime.get(); + + totalSteps() { + return migrationTotalSteps.get(); + }, + + stepName() { + return migrationStepName.get(); + }, + + stepProgress() { + return migrationStepProgress.get(); + }, + + stepStatus() { + return migrationStepStatus.get(); + }, + + stepDetails() { + return migrationStepDetails.get(); + }, + + progressBarStyle() { + const progress = migrationProgress.get(); + return `width: ${progress}%`; + }, + + stepProgressBarStyle() { + const progress = migrationStepProgress.get(); + return `width: ${progress}%`; }, - - migrationSteps() { - const steps = migrationSteps.get(); - const currentStep = migrationCurrentStep.get(); + + stepNameFormatted() { + const stepName = migrationStepName.get(); + if (!stepName) return ''; - return steps.map(step => ({ - ...step, - isCurrentStep: step.name === currentStep - })); + // Convert snake_case to Title Case + return stepName + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + }, + + stepDetailsFormatted() { + const details = migrationStepDetails.get(); + if (!details) return ''; + + const formatted = []; + for (const [key, value] of Object.entries(details)) { + const formattedKey = key + .split(/(?=[A-Z])/) + .join(' ') + .toLowerCase() + .replace(/^\w/, c => c.toUpperCase()); + formatted.push(`${formattedKey}: ${value}`); + } + + return formatted.join(', '); } }); -Template.migrationProgress.onCreated(function() { - // Subscribe to migration state changes - this.autorun(() => { - isMigrating.get(); - migrationProgress.get(); - migrationStatus.get(); - migrationCurrentStep.get(); - migrationEstimatedTime.get(); - migrationSteps.get(); - }); -}); +// Template events +Template.migrationProgress.events({ + 'click .js-close-migration-progress'() { + migrationProgressManager.clearProgress(); + } +}); \ No newline at end of file diff --git a/client/components/swimlanes/swimlanes.css b/client/components/swimlanes/swimlanes.css index 4c20cb0f40..83540549f0 100644 --- a/client/components/swimlanes/swimlanes.css +++ b/client/components/swimlanes/swimlanes.css @@ -112,7 +112,7 @@ padding: 7px; top: 50%; transform: translateY(-50%); - left: 87vw; + right: 10px; font-size: 24px; cursor: move; z-index: 15; diff --git a/client/components/users/userAvatar.jade b/client/components/users/userAvatar.jade index b1bc7e2d48..b61eb5033f 100644 --- a/client/components/users/userAvatar.jade +++ b/client/components/users/userAvatar.jade @@ -87,7 +87,7 @@ template(name="changeAvatarPopup") each uploadedAvatars li: a.js-select-avatar .member - img.avatar.avatar-image(src="{{link}}?auth=false&brokenIsFine=true") + img.avatar.avatar-image(src="{{link}}") | {{_ 'uploaded-avatar'}} if isSelected | ✅ diff --git a/client/components/users/userAvatar.js b/client/components/users/userAvatar.js index 98ebc901e4..2869a97500 100644 --- a/client/components/users/userAvatar.js +++ b/client/components/users/userAvatar.js @@ -179,7 +179,7 @@ BlazeComponent.extendComponent({ isSelected() { const userProfile = ReactiveCache.getCurrentUser().profile; const avatarUrl = userProfile && userProfile.avatarUrl; - const currentAvatarUrl = `${this.currentData().link()}?auth=false&brokenIsFine=true`; + const currentAvatarUrl = this.currentData().link(); return avatarUrl === currentAvatarUrl; }, @@ -220,7 +220,7 @@ BlazeComponent.extendComponent({ } }, 'click .js-select-avatar'() { - const avatarUrl = `${this.currentData().link()}?auth=false&brokenIsFine=true`; + const avatarUrl = this.currentData().link(); this.setAvatar(avatarUrl); }, 'click .js-select-initials'() { diff --git a/models/attachments.js b/models/attachments.js index ac66c15c49..27d533e253 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -13,6 +13,7 @@ import FileStoreStrategyFactory, {moveToStorage, rename, STORAGE_NAME_FILESYSTEM // import { STORAGE_NAME_S3 } from '/models/lib/fileStoreStrategy'; import { getAttachmentWithBackwardCompatibility, getAttachmentsWithBackwardCompatibility } from './lib/attachmentBackwardCompatibility'; import AttachmentStorageSettings from './attachmentStorageSettings'; +import { generateUniversalAttachmentUrl, cleanFileUrl } from '/models/lib/universalUrlGenerator'; let attachmentUploadExternalProgram; let attachmentUploadMimeTypes = []; @@ -325,4 +326,15 @@ if (Meteor.isServer) { Attachments.getAttachmentWithBackwardCompatibility = getAttachmentWithBackwardCompatibility; Attachments.getAttachmentsWithBackwardCompatibility = getAttachmentsWithBackwardCompatibility; +// Override the link method to use universal URLs +if (Meteor.isClient) { + // Add custom link method to attachment documents + Attachments.collection.helpers({ + link(version = 'original') { + // Use universal URL generator for consistent, URL-agnostic URLs + return generateUniversalAttachmentUrl(this._id, version); + } + }); +} + export default Attachments; diff --git a/models/avatars.js b/models/avatars.js index 065728322e..6ce904bcbe 100644 --- a/models/avatars.js +++ b/models/avatars.js @@ -8,6 +8,7 @@ import { TAPi18n } from '/imports/i18n'; import fs from 'fs'; import path from 'path'; import FileStoreStrategyFactory, { FileStoreStrategyFilesystem, FileStoreStrategyGridFs, STORAGE_NAME_FILESYSTEM } from '/models/lib/fileStoreStrategy'; +import { generateUniversalAvatarUrl, cleanFileUrl } from '/models/lib/universalUrlGenerator'; const filesize = require('filesize'); @@ -116,7 +117,9 @@ Avatars = new FilesCollection({ const isValid = Promise.await(isFileValid(fileObj, avatarsUploadMimeTypes, avatarsUploadSize, avatarsUploadExternalProgram)); if (isValid) { - ReactiveCache.getUser(fileObj.userId).setAvatarUrl(`${formatFleURL(fileObj)}?auth=false&brokenIsFine=true`); + // Set avatar URL using universal URL generator (URL-agnostic) + const universalUrl = generateUniversalAvatarUrl(fileObj._id); + ReactiveCache.getUser(fileObj.userId).setAvatarUrl(universalUrl); } else { Avatars.remove(fileObj._id); } @@ -164,4 +167,15 @@ if (Meteor.isServer) { }); } +// Override the link method to use universal URLs +if (Meteor.isClient) { + // Add custom link method to avatar documents + Avatars.collection.helpers({ + link(version = 'original') { + // Use universal URL generator for consistent, URL-agnostic URLs + return generateUniversalAvatarUrl(this._id, version); + } + }); +} + export default Avatars; diff --git a/models/lib/universalUrlGenerator.js b/models/lib/universalUrlGenerator.js new file mode 100644 index 0000000000..16a8d00303 --- /dev/null +++ b/models/lib/universalUrlGenerator.js @@ -0,0 +1,194 @@ +/** + * Universal URL Generator + * Generates file URLs that work regardless of ROOT_URL and PORT settings + * Ensures all attachments and avatars are always visible + */ + +import { Meteor } from 'meteor/meteor'; + +/** + * Generate a universal file URL that works regardless of ROOT_URL and PORT + * @param {string} fileId - The file ID + * @param {string} type - The file type ('attachment' or 'avatar') + * @param {string} version - The file version (default: 'original') + * @returns {string} - Universal file URL + */ +export function generateUniversalFileUrl(fileId, type, version = 'original') { + if (!fileId) { + return ''; + } + + // Always use relative URLs to avoid ROOT_URL and PORT dependencies + if (type === 'attachment') { + return `/cdn/storage/attachments/${fileId}`; + } else if (type === 'avatar') { + return `/cdn/storage/avatars/${fileId}`; + } + + return ''; +} + +/** + * Generate a universal attachment URL + * @param {string} attachmentId - The attachment ID + * @param {string} version - The file version (default: 'original') + * @returns {string} - Universal attachment URL + */ +export function generateUniversalAttachmentUrl(attachmentId, version = 'original') { + return generateUniversalFileUrl(attachmentId, 'attachment', version); +} + +/** + * Generate a universal avatar URL + * @param {string} avatarId - The avatar ID + * @param {string} version - The file version (default: 'original') + * @returns {string} - Universal avatar URL + */ +export function generateUniversalAvatarUrl(avatarId, version = 'original') { + return generateUniversalFileUrl(avatarId, 'avatar', version); +} + +/** + * Clean and normalize a file URL to ensure it's universal + * @param {string} url - The URL to clean + * @param {string} type - The file type ('attachment' or 'avatar') + * @returns {string} - Cleaned universal URL + */ +export function cleanFileUrl(url, type) { + if (!url) { + return ''; + } + + // Remove any domain, port, or protocol from the URL + let cleanUrl = url; + + // Remove protocol and domain + cleanUrl = cleanUrl.replace(/^https?:\/\/[^\/]+/, ''); + + // Remove ROOT_URL pathname if present + if (Meteor.isServer && process.env.ROOT_URL) { + try { + const rootUrl = new URL(process.env.ROOT_URL); + if (rootUrl.pathname && rootUrl.pathname !== '/') { + cleanUrl = cleanUrl.replace(rootUrl.pathname, ''); + } + } catch (e) { + // Ignore URL parsing errors + } + } + + // Normalize path separators + cleanUrl = cleanUrl.replace(/\/+/g, '/'); + + // Ensure URL starts with / + if (!cleanUrl.startsWith('/')) { + cleanUrl = '/' + cleanUrl; + } + + // Convert old CollectionFS URLs to new format + if (type === 'attachment') { + cleanUrl = cleanUrl.replace('/cfs/files/attachments/', '/cdn/storage/attachments/'); + } else if (type === 'avatar') { + cleanUrl = cleanUrl.replace('/cfs/files/avatars/', '/cdn/storage/avatars/'); + } + + // Remove any query parameters that might cause issues + cleanUrl = cleanUrl.split('?')[0]; + cleanUrl = cleanUrl.split('#')[0]; + + return cleanUrl; +} + +/** + * Check if a URL is a universal file URL + * @param {string} url - The URL to check + * @param {string} type - The file type ('attachment' or 'avatar') + * @returns {boolean} - True if it's a universal file URL + */ +export function isUniversalFileUrl(url, type) { + if (!url) { + return false; + } + + if (type === 'attachment') { + return url.includes('/cdn/storage/attachments/') || url.includes('/cfs/files/attachments/'); + } else if (type === 'avatar') { + return url.includes('/cdn/storage/avatars/') || url.includes('/cfs/files/avatars/'); + } + + return false; +} + +/** + * Extract file ID from a universal file URL + * @param {string} url - The URL to extract from + * @param {string} type - The file type ('attachment' or 'avatar') + * @returns {string|null} - The file ID or null if not found + */ +export function extractFileIdFromUrl(url, type) { + if (!url) { + return null; + } + + let pattern; + if (type === 'attachment') { + pattern = /\/(?:cdn\/storage\/attachments|cfs\/files\/attachments)\/([^\/\?#]+)/; + } else if (type === 'avatar') { + pattern = /\/(?:cdn\/storage\/avatars|cfs\/files\/avatars)\/([^\/\?#]+)/; + } else { + return null; + } + + const match = url.match(pattern); + return match ? match[1] : null; +} + +/** + * Generate a fallback URL for when the primary URL fails + * @param {string} fileId - The file ID + * @param {string} type - The file type ('attachment' or 'avatar') + * @returns {string} - Fallback URL + */ +export function generateFallbackUrl(fileId, type) { + if (!fileId) { + return ''; + } + + // Try alternative route patterns + if (type === 'attachment') { + return `/attachments/${fileId}`; + } else if (type === 'avatar') { + return `/avatars/${fileId}`; + } + + return ''; +} + +/** + * Get all possible URLs for a file (for redundancy) + * @param {string} fileId - The file ID + * @param {string} type - The file type ('attachment' or 'avatar') + * @returns {Array} - Array of possible URLs + */ +export function getAllPossibleUrls(fileId, type) { + if (!fileId) { + return []; + } + + const urls = []; + + // Primary URL + urls.push(generateUniversalFileUrl(fileId, type)); + + // Fallback URL + urls.push(generateFallbackUrl(fileId, type)); + + // Legacy URLs for backward compatibility + if (type === 'attachment') { + urls.push(`/cfs/files/attachments/${fileId}`); + } else if (type === 'avatar') { + urls.push(`/cfs/files/avatars/${fileId}`); + } + + return urls.filter(url => url); // Remove empty URLs +} diff --git a/server/00checkStartup.js b/server/00checkStartup.js index d7035dca85..ed4dbabb3c 100644 --- a/server/00checkStartup.js +++ b/server/00checkStartup.js @@ -42,6 +42,12 @@ import './cronJobStorage'; // Import migrations import './migrations/fixMissingListsMigration'; +import './migrations/fixAvatarUrls'; +import './migrations/fixAllFileUrls'; +import './migrations/comprehensiveBoardMigration'; + +// Import file serving routes +import './routes/universalFileServer'; // Note: Automatic migrations are disabled - migrations only run when opening boards // import './boardMigrationDetector'; diff --git a/server/cors.js b/server/cors.js index 4badba9feb..f99258eaea 100644 --- a/server/cors.js +++ b/server/cors.js @@ -1,4 +1,19 @@ Meteor.startup(() => { + // Set Permissions-Policy header to suppress browser warnings about experimental features + WebApp.rawConnectHandlers.use(function(req, res, next) { + // Disable experimental advertising and privacy features that cause browser warnings + res.setHeader('Permissions-Policy', + 'browsing-topics=(), ' + + 'run-ad-auction=(), ' + + 'join-ad-interest-group=(), ' + + 'private-state-token-redemption=(), ' + + 'private-state-token-issuance=(), ' + + 'private-aggregation=(), ' + + 'attribution-reporting=()' + ); + return next(); + }); + if (process.env.CORS) { // Listen to incoming HTTP requests, can only be used on the server WebApp.rawConnectHandlers.use(function(req, res, next) { diff --git a/server/migrations/comprehensiveBoardMigration.js b/server/migrations/comprehensiveBoardMigration.js new file mode 100644 index 0000000000..f9ea7c5230 --- /dev/null +++ b/server/migrations/comprehensiveBoardMigration.js @@ -0,0 +1,767 @@ +/** + * 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 + * - v8.03+: Per-swimlane lists structure + */ + +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import { ReactiveCache } from '/imports/reactiveCache'; +import Boards from '/models/boards'; +import Lists from '/models/lists'; +import Cards from '/models/cards'; +import Swimlanes from '/models/swimlanes'; +import Attachments from '/models/attachments'; +import { generateUniversalAttachmentUrl, isUniversalFileUrl } from '/models/lib/universalUrlGenerator'; + +class ComprehensiveBoardMigration { + constructor() { + this.name = 'comprehensive-board-migration'; + this.version = 1; + this.migrationSteps = [ + 'analyze_board_structure', + 'fix_orphaned_cards', + 'convert_shared_lists', + 'ensure_per_swimlane_lists', + 'cleanup_empty_lists', + 'validate_migration' + ]; + } + + /** + * Check if migration is needed for a board + */ + needsMigration(boardId) { + try { + const board = ReactiveCache.getBoard(boardId); + if (!board) return false; + + // Check if board has already been processed + if (board.comprehensiveMigrationCompleted) { + return false; + } + + // Check for various issues that need migration + const issues = this.detectMigrationIssues(boardId); + return issues.length > 0; + + } catch (error) { + console.error('Error checking if migration is needed:', error); + return false; + } + } + + /** + * Detect all migration issues in a board + */ + detectMigrationIssues(boardId) { + const issues = []; + + try { + const cards = ReactiveCache.getCards({ boardId }); + const lists = ReactiveCache.getLists({ boardId }); + const swimlanes = ReactiveCache.getSwimlanes({ boardId }); + + // Issue 1: Cards with missing swimlaneId + const cardsWithoutSwimlane = cards.filter(card => !card.swimlaneId); + if (cardsWithoutSwimlane.length > 0) { + issues.push({ + type: 'cards_without_swimlane', + count: cardsWithoutSwimlane.length, + description: `${cardsWithoutSwimlane.length} cards missing swimlaneId` + }); + } + + // Issue 2: Cards with missing listId + const cardsWithoutList = cards.filter(card => !card.listId); + if (cardsWithoutList.length > 0) { + issues.push({ + type: 'cards_without_list', + count: cardsWithoutList.length, + description: `${cardsWithoutList.length} cards missing listId` + }); + } + + // Issue 3: Lists without swimlaneId (shared lists) + const sharedLists = lists.filter(list => !list.swimlaneId || list.swimlaneId === ''); + if (sharedLists.length > 0) { + issues.push({ + type: 'shared_lists', + count: sharedLists.length, + description: `${sharedLists.length} lists without swimlaneId (shared lists)` + }); + } + + // Issue 4: Cards with mismatched listId/swimlaneId + const listSwimlaneMap = new Map(); + lists.forEach(list => { + listSwimlaneMap.set(list._id, list.swimlaneId || ''); + }); + + const mismatchedCards = cards.filter(card => { + if (!card.listId || !card.swimlaneId) return false; + const listSwimlaneId = listSwimlaneMap.get(card.listId); + return listSwimlaneId && listSwimlaneId !== card.swimlaneId; + }); + + if (mismatchedCards.length > 0) { + issues.push({ + type: 'mismatched_cards', + count: mismatchedCards.length, + description: `${mismatchedCards.length} cards with mismatched listId/swimlaneId` + }); + } + + // Issue 5: Empty lists (lists with no cards) + const emptyLists = lists.filter(list => { + const listCards = cards.filter(card => card.listId === list._id); + return listCards.length === 0; + }); + + if (emptyLists.length > 0) { + issues.push({ + type: 'empty_lists', + count: emptyLists.length, + description: `${emptyLists.length} empty lists (no cards)` + }); + } + + } catch (error) { + console.error('Error detecting migration issues:', error); + issues.push({ + type: 'detection_error', + count: 1, + description: `Error detecting issues: ${error.message}` + }); + } + + return issues; + } + + /** + * Execute the comprehensive migration for a board + */ + async executeMigration(boardId, progressCallback = null) { + try { + if (process.env.DEBUG === 'true') { + console.log(`Starting comprehensive board migration for board ${boardId}`); + } + + const board = ReactiveCache.getBoard(boardId); + if (!board) { + throw new Error(`Board ${boardId} not found`); + } + + const results = { + boardId, + steps: {}, + totalCardsProcessed: 0, + totalListsProcessed: 0, + totalListsCreated: 0, + totalListsRemoved: 0, + errors: [] + }; + + const totalSteps = this.migrationSteps.length; + let currentStep = 0; + + // Helper function to update progress + const updateProgress = (stepName, stepProgress, stepStatus, stepDetails = null) => { + currentStep++; + const overallProgress = Math.round((currentStep / totalSteps) * 100); + + const progressData = { + overallProgress, + currentStep: currentStep, + totalSteps, + stepName, + stepProgress, + stepStatus, + stepDetails, + boardId + }; + + if (progressCallback) { + progressCallback(progressData); + } + + if (process.env.DEBUG === 'true') { + console.log(`Migration Progress: ${stepName} - ${stepStatus} (${stepProgress}%)`); + } + }; + + // Step 1: Analyze board structure + updateProgress('analyze_board_structure', 0, 'Starting analysis...'); + results.steps.analyze = await this.analyzeBoardStructure(boardId); + updateProgress('analyze_board_structure', 100, 'Analysis complete', { + 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) => { + updateProgress('fix_orphaned_cards', progress, status); + }); + results.totalCardsProcessed += results.steps.fixOrphanedCards.cardsFixed || 0; + updateProgress('fix_orphaned_cards', 100, 'Orphaned cards fixed', { + cardsFixed: results.steps.fixOrphanedCards.cardsFixed + }); + + // Step 3: Convert shared lists to per-swimlane lists + updateProgress('convert_shared_lists', 0, 'Converting shared lists...'); + results.steps.convertSharedLists = await this.convertSharedListsToPerSwimlane(boardId, (progress, status) => { + updateProgress('convert_shared_lists', progress, status); + }); + results.totalListsProcessed += results.steps.convertSharedLists.listsProcessed || 0; + results.totalListsCreated += results.steps.convertSharedLists.listsCreated || 0; + updateProgress('convert_shared_lists', 100, 'Shared lists converted', { + listsProcessed: results.steps.convertSharedLists.listsProcessed, + listsCreated: results.steps.convertSharedLists.listsCreated + }); + + // Step 4: Ensure all lists are per-swimlane + updateProgress('ensure_per_swimlane_lists', 0, 'Ensuring per-swimlane structure...'); + results.steps.ensurePerSwimlane = await this.ensurePerSwimlaneLists(boardId); + results.totalListsProcessed += results.steps.ensurePerSwimlane.listsProcessed || 0; + updateProgress('ensure_per_swimlane_lists', 100, 'Per-swimlane structure ensured', { + listsProcessed: results.steps.ensurePerSwimlane.listsProcessed + }); + + // Step 5: Cleanup empty lists + updateProgress('cleanup_empty_lists', 0, 'Cleaning up empty lists...'); + results.steps.cleanupEmpty = await this.cleanupEmptyLists(boardId); + results.totalListsRemoved += results.steps.cleanupEmpty.listsRemoved || 0; + updateProgress('cleanup_empty_lists', 100, 'Empty lists cleaned up', { + listsRemoved: results.steps.cleanupEmpty.listsRemoved + }); + + // Step 6: Validate migration + updateProgress('validate_migration', 0, 'Validating migration...'); + results.steps.validate = await this.validateMigration(boardId); + updateProgress('validate_migration', 100, 'Migration validated', { + migrationSuccessful: results.steps.validate.migrationSuccessful, + totalCards: results.steps.validate.totalCards, + totalLists: results.steps.validate.totalLists + }); + + // Step 7: Fix avatar URLs + updateProgress('fix_avatar_urls', 0, 'Fixing avatar URLs...'); + results.steps.fixAvatarUrls = await this.fixAvatarUrls(boardId); + updateProgress('fix_avatar_urls', 100, 'Avatar URLs fixed', { + avatarsFixed: results.steps.fixAvatarUrls.avatarsFixed + }); + + // Step 8: Fix attachment URLs + updateProgress('fix_attachment_urls', 0, 'Fixing attachment URLs...'); + results.steps.fixAttachmentUrls = await this.fixAttachmentUrls(boardId); + updateProgress('fix_attachment_urls', 100, 'Attachment URLs fixed', { + attachmentsFixed: results.steps.fixAttachmentUrls.attachmentsFixed + }); + + // Mark board as processed + Boards.update(boardId, { + $set: { + comprehensiveMigrationCompleted: true, + comprehensiveMigrationCompletedAt: new Date(), + comprehensiveMigrationResults: results + } + }); + + if (process.env.DEBUG === 'true') { + console.log(`Comprehensive board migration completed for board ${boardId}:`, results); + } + + return { + success: true, + results + }; + + } catch (error) { + console.error(`Error executing comprehensive migration for board ${boardId}:`, error); + throw error; + } + } + + /** + * Step 1: Analyze board structure + */ + async analyzeBoardStructure(boardId) { + const issues = this.detectMigrationIssues(boardId); + return { + issues, + issueCount: issues.length, + needsMigration: issues.length > 0 + }; + } + + /** + * Step 2: Fix orphaned cards (cards with missing swimlaneId or listId) + */ + async fixOrphanedCards(boardId, progressCallback = null) { + const cards = ReactiveCache.getCards({ boardId }); + const swimlanes = ReactiveCache.getSwimlanes({ boardId }); + const lists = ReactiveCache.getLists({ boardId }); + + let cardsFixed = 0; + const defaultSwimlane = swimlanes.find(s => s.title === 'Default') || swimlanes[0]; + const totalCards = cards.length; + + for (let i = 0; i < cards.length; i++) { + const card = cards[i]; + let needsUpdate = false; + const updates = {}; + + // Fix missing swimlaneId + if (!card.swimlaneId) { + updates.swimlaneId = defaultSwimlane._id; + needsUpdate = true; + } + + // Fix missing listId + if (!card.listId) { + // Find or create a default list for this swimlane + const swimlaneId = updates.swimlaneId || card.swimlaneId; + let defaultList = lists.find(list => + list.swimlaneId === swimlaneId && list.title === 'Default' + ); + + if (!defaultList) { + // Create a default list for this swimlane + const newListId = Lists.insert({ + title: 'Default', + boardId: boardId, + swimlaneId: swimlaneId, + sort: 0, + archived: false, + createdAt: new Date(), + modifiedAt: new Date(), + type: 'list' + }); + defaultList = { _id: newListId }; + } + + updates.listId = defaultList._id; + needsUpdate = true; + } + + if (needsUpdate) { + Cards.update(card._id, { + $set: { + ...updates, + modifiedAt: new Date() + } + }); + cardsFixed++; + } + + // Update progress + if (progressCallback && (i % 10 === 0 || i === totalCards - 1)) { + const progress = Math.round(((i + 1) / totalCards) * 100); + progressCallback(progress, `Processing card ${i + 1} of ${totalCards}...`); + } + } + + return { cardsFixed }; + } + + /** + * Step 3: Convert shared lists to per-swimlane lists + */ + async convertSharedListsToPerSwimlane(boardId, progressCallback = null) { + const cards = ReactiveCache.getCards({ boardId }); + const lists = ReactiveCache.getLists({ boardId }); + const swimlanes = ReactiveCache.getSwimlanes({ boardId }); + + let listsProcessed = 0; + let listsCreated = 0; + + // Group cards by swimlaneId + const cardsBySwimlane = new Map(); + cards.forEach(card => { + if (!cardsBySwimlane.has(card.swimlaneId)) { + cardsBySwimlane.set(card.swimlaneId, []); + } + cardsBySwimlane.get(card.swimlaneId).push(card); + }); + + const swimlaneEntries = Array.from(cardsBySwimlane.entries()); + const totalSwimlanes = swimlaneEntries.length; + + // Process each swimlane + for (let i = 0; i < swimlaneEntries.length; i++) { + const [swimlaneId, swimlaneCards] = swimlaneEntries[i]; + if (!swimlaneId) continue; + + if (progressCallback) { + const progress = Math.round(((i + 1) / totalSwimlanes) * 100); + progressCallback(progress, `Processing swimlane ${i + 1} of ${totalSwimlanes}...`); + } + + // Get existing lists for this swimlane + const existingLists = lists.filter(list => list.swimlaneId === swimlaneId); + const existingListTitles = new Set(existingLists.map(list => list.title)); + + // Group cards by their current listId + const cardsByListId = new Map(); + swimlaneCards.forEach(card => { + if (!cardsByListId.has(card.listId)) { + cardsByListId.set(card.listId, []); + } + cardsByListId.get(card.listId).push(card); + }); + + // For each listId used by cards in this swimlane + for (const [listId, cardsInList] of cardsByListId) { + const originalList = lists.find(l => l._id === listId); + if (!originalList) continue; + + // Check if this list's swimlaneId matches the card's swimlaneId + if (originalList.swimlaneId === swimlaneId) { + // List is already correctly assigned to this swimlane + listsProcessed++; + continue; + } + + // 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 = { + title: originalList.title, + boardId: boardId, + swimlaneId: swimlaneId, + sort: originalList.sort || 0, + archived: originalList.archived || false, + createdAt: new Date(), + modifiedAt: new Date(), + type: originalList.type || 'list' + }; + + // Copy other properties if they exist + if (originalList.color) newListData.color = originalList.color; + if (originalList.wipLimit) newListData.wipLimit = originalList.wipLimit; + if (originalList.wipLimitEnabled) newListData.wipLimitEnabled = originalList.wipLimitEnabled; + if (originalList.wipLimitSoft) newListData.wipLimitSoft = originalList.wipLimitSoft; + if (originalList.starred) newListData.starred = originalList.starred; + if (originalList.collapsed) newListData.collapsed = originalList.collapsed; + + // Insert the new list + const newListId = Lists.insert(newListData); + targetList = { _id: newListId, ...newListData }; + listsCreated++; + } + + // Update all cards in this group to use the correct listId + for (const card of cardsInList) { + Cards.update(card._id, { + $set: { + listId: targetList._id, + modifiedAt: new Date() + } + }); + } + + listsProcessed++; + } + } + + return { listsProcessed, listsCreated }; + } + + /** + * Step 4: Ensure all lists are per-swimlane + */ + async ensurePerSwimlaneLists(boardId) { + const lists = ReactiveCache.getLists({ boardId }); + const swimlanes = ReactiveCache.getSwimlanes({ boardId }); + const defaultSwimlane = swimlanes.find(s => s.title === 'Default') || swimlanes[0]; + + let listsProcessed = 0; + + for (const list of lists) { + if (!list.swimlaneId || list.swimlaneId === '') { + // Assign to default swimlane + Lists.update(list._id, { + $set: { + swimlaneId: defaultSwimlane._id, + modifiedAt: new Date() + } + }); + listsProcessed++; + } + } + + return { listsProcessed }; + } + + /** + * Step 5: Cleanup empty lists (lists with no cards) + */ + async cleanupEmptyLists(boardId) { + const lists = ReactiveCache.getLists({ boardId }); + const cards = ReactiveCache.getCards({ boardId }); + + let listsRemoved = 0; + + 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})`); + } + } + } + + return { listsRemoved }; + } + + /** + * Step 6: Validate migration + */ + async validateMigration(boardId) { + const issues = this.detectMigrationIssues(boardId); + const cards = ReactiveCache.getCards({ boardId }); + const lists = ReactiveCache.getLists({ boardId }); + + // Check that all cards have valid swimlaneId and listId + const validCards = cards.filter(card => card.swimlaneId && card.listId); + const invalidCards = cards.length - validCards.length; + + // Check that all lists have swimlaneId + const validLists = lists.filter(list => list.swimlaneId && list.swimlaneId !== ''); + const invalidLists = lists.length - validLists.length; + + return { + issuesRemaining: issues.length, + totalCards: cards.length, + validCards, + invalidCards, + totalLists: lists.length, + validLists, + invalidLists, + migrationSuccessful: issues.length === 0 && invalidCards === 0 && invalidLists === 0 + }; + } + + /** + * Step 7: Fix avatar URLs (remove problematic auth parameters and fix URL formats) + */ + async fixAvatarUrls(boardId) { + const users = ReactiveCache.getUsers({}); + let avatarsFixed = 0; + + for (const user of users) { + if (user.profile && user.profile.avatarUrl) { + 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 + cleanUrl = cleanUrl.replace(/[?&]auth=false/g, ''); + cleanUrl = cleanUrl.replace(/[?&]brokenIsFine=true/g, ''); + cleanUrl = cleanUrl.replace(/\?&/g, '?'); + 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 + if (!avatarUrl.startsWith('http') && !avatarUrl.startsWith('/')) { + cleanUrl = `/cdn/storage/avatars/${avatarUrl}`; + needsUpdate = true; + } + } + + if (needsUpdate) { + // Update user's avatar URL + Users.update(user._id, { + $set: { + 'profile.avatarUrl': cleanUrl, + modifiedAt: new Date() + } + }); + + avatarsFixed++; + } + } + } + + return { avatarsFixed }; + } + + /** + * Step 8: Fix attachment URLs (remove problematic auth parameters and fix URL formats) + */ + async fixAttachmentUrls(boardId) { + const attachments = ReactiveCache.getAttachments({}); + let attachmentsFixed = 0; + + for (const attachment of attachments) { + // Check if attachment has URL field that needs fixing + if (attachment.url) { + 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 + cleanUrl = cleanUrl.replace(/[?&]auth=false/g, ''); + cleanUrl = cleanUrl.replace(/[?&]brokenIsFine=true/g, ''); + cleanUrl = cleanUrl.replace(/\?&/g, '?'); + 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, { + $set: { + url: cleanUrl, + modifiedAt: new Date() + } + }); + + attachmentsFixed++; + } + } + } + + return { attachmentsFixed }; + } + + /** + * Get migration status for a board + */ + getMigrationStatus(boardId) { + try { + const board = ReactiveCache.getBoard(boardId); + if (!board) { + return { status: 'board_not_found' }; + } + + if (board.comprehensiveMigrationCompleted) { + return { + status: 'completed', + completedAt: board.comprehensiveMigrationCompletedAt, + results: board.comprehensiveMigrationResults + }; + } + + const needsMigration = this.needsMigration(boardId); + const issues = this.detectMigrationIssues(boardId); + + return { + status: needsMigration ? 'needed' : 'not_needed', + issues, + issueCount: issues.length + }; + + } catch (error) { + console.error('Error getting migration status:', error); + return { status: 'error', error: error.message }; + } + } +} + +// Export singleton instance +export const comprehensiveBoardMigration = new ComprehensiveBoardMigration(); + +// Meteor methods +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'); + } + + 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); + }, + + 'comprehensiveBoardMigration.fixAvatarUrls'(boardId) { + check(boardId, String); + + if (!this.userId) { + throw new Meteor.Error('not-authorized'); + } + + return comprehensiveBoardMigration.fixAvatarUrls(boardId); + } +}); diff --git a/server/migrations/fixAllFileUrls.js b/server/migrations/fixAllFileUrls.js new file mode 100644 index 0000000000..caba86e684 --- /dev/null +++ b/server/migrations/fixAllFileUrls.js @@ -0,0 +1,277 @@ +/** + * Fix All File URLs Migration + * Ensures all attachment and avatar URLs are universal and work regardless of ROOT_URL and PORT settings + */ + +import { ReactiveCache } from '/imports/reactiveCache'; +import Users from '/models/users'; +import Attachments from '/models/attachments'; +import Avatars from '/models/avatars'; +import { generateUniversalAttachmentUrl, generateUniversalAvatarUrl, cleanFileUrl, extractFileIdFromUrl, isUniversalFileUrl } from '/models/lib/universalUrlGenerator'; + +class FixAllFileUrlsMigration { + constructor() { + this.name = 'fixAllFileUrls'; + this.version = 1; + } + + /** + * Check if migration is needed + */ + needsMigration() { + // Check for problematic avatar URLs + const users = ReactiveCache.getUsers({}); + for (const user of users) { + if (user.profile && user.profile.avatarUrl) { + const avatarUrl = user.profile.avatarUrl; + if (this.hasProblematicUrl(avatarUrl)) { + return true; + } + } + } + + // Check for problematic attachment URLs in cards + const cards = ReactiveCache.getCards({}); + for (const card of cards) { + if (card.attachments) { + for (const attachment of card.attachments) { + if (attachment.url && this.hasProblematicUrl(attachment.url)) { + return true; + } + } + } + } + + return false; + } + + /** + * Check if a URL has problematic patterns + */ + 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 { + const rootUrl = new URL(process.env.ROOT_URL); + if (rootUrl.pathname && rootUrl.pathname !== '/' && url.includes(rootUrl.pathname)) { + return true; + } + } catch (e) { + // 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; + } + + /** + * Execute the migration + */ + async execute() { + let filesFixed = 0; + let errors = []; + + console.log(`Starting universal file URL migration...`); + + try { + // Fix avatar URLs + const avatarFixed = await this.fixAvatarUrls(); + filesFixed += avatarFixed; + + // Fix attachment URLs + const attachmentFixed = await this.fixAttachmentUrls(); + filesFixed += attachmentFixed; + + // Fix card attachment references + const cardFixed = await this.fixCardAttachmentUrls(); + filesFixed += cardFixed; + + } catch (error) { + console.error('Error during file URL migration:', error); + errors.push(error.message); + } + + console.log(`Universal file URL migration completed. Fixed ${filesFixed} file URLs.`); + + return { + success: errors.length === 0, + filesFixed, + errors + }; + } + + /** + * Fix avatar URLs in user profiles + */ + async fixAvatarUrls() { + const users = ReactiveCache.getUsers({}); + let avatarsFixed = 0; + + 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 + cleanUrl = generateUniversalAvatarUrl(fileId); + } else { + // Clean existing URL + cleanUrl = cleanFileUrl(avatarUrl, 'avatar'); + } + + if (cleanUrl && cleanUrl !== avatarUrl) { + // Update user's avatar URL + Users.update(user._id, { + $set: { + 'profile.avatarUrl': cleanUrl, + modifiedAt: new Date() + } + }); + + avatarsFixed++; + + if (process.env.DEBUG === 'true') { + console.log(`Fixed avatar URL for user ${user.username}: ${avatarUrl} -> ${cleanUrl}`); + } + } + } catch (error) { + console.error(`Error fixing avatar URL for user ${user.username}:`, error); + } + } + } + } + + return avatarsFixed; + } + + /** + * Fix attachment URLs in attachment records + */ + async fixAttachmentUrls() { + const attachments = ReactiveCache.getAttachments({}); + let attachmentsFixed = 0; + + for (const attachment of attachments) { + // Check if attachment has URL field that needs fixing + if (attachment.url && this.hasProblematicUrl(attachment.url)) { + try { + const fileId = attachment._id; + const cleanUrl = generateUniversalAttachmentUrl(fileId); + + if (cleanUrl && cleanUrl !== attachment.url) { + // Update attachment URL + Attachments.update(attachment._id, { + $set: { + url: cleanUrl, + modifiedAt: new Date() + } + }); + + attachmentsFixed++; + + if (process.env.DEBUG === 'true') { + console.log(`Fixed attachment URL: ${attachment.url} -> ${cleanUrl}`); + } + } + } catch (error) { + console.error(`Error fixing attachment URL for ${attachment._id}:`, error); + } + } + } + + return attachmentsFixed; + } + + /** + * Fix attachment URLs in card references + */ + async fixCardAttachmentUrls() { + const cards = ReactiveCache.getCards({}); + let cardsFixed = 0; + + for (const card of cards) { + if (card.attachments) { + let needsUpdate = false; + const updatedAttachments = card.attachments.map(attachment => { + if (attachment.url && this.hasProblematicUrl(attachment.url)) { + try { + const fileId = attachment._id || extractFileIdFromUrl(attachment.url, 'attachment'); + const cleanUrl = fileId ? generateUniversalAttachmentUrl(fileId) : cleanFileUrl(attachment.url, 'attachment'); + + if (cleanUrl && cleanUrl !== attachment.url) { + needsUpdate = true; + return { ...attachment, url: cleanUrl }; + } + } catch (error) { + console.error(`Error fixing card attachment URL:`, error); + } + } + return attachment; + }); + + if (needsUpdate) { + // Update card with fixed attachment URLs + Cards.update(card._id, { + $set: { + attachments: updatedAttachments, + modifiedAt: new Date() + } + }); + + cardsFixed++; + + if (process.env.DEBUG === 'true') { + console.log(`Fixed attachment URLs in card ${card._id}`); + } + } + } + } + + return cardsFixed; + } +} + +// Export singleton instance +export const fixAllFileUrlsMigration = new FixAllFileUrlsMigration(); + +// Meteor methods +Meteor.methods({ + 'fixAllFileUrls.execute'() { + if (!this.userId) { + throw new Meteor.Error('not-authorized'); + } + + return fixAllFileUrlsMigration.execute(); + }, + + 'fixAllFileUrls.needsMigration'() { + if (!this.userId) { + throw new Meteor.Error('not-authorized'); + } + + return fixAllFileUrlsMigration.needsMigration(); + } +}); diff --git a/server/migrations/fixAvatarUrls.js b/server/migrations/fixAvatarUrls.js new file mode 100644 index 0000000000..f542903ed6 --- /dev/null +++ b/server/migrations/fixAvatarUrls.js @@ -0,0 +1,128 @@ +/** + * Fix Avatar URLs Migration + * Removes problematic auth parameters from existing avatar URLs + */ + +import { ReactiveCache } from '/imports/reactiveCache'; +import Users from '/models/users'; +import { generateUniversalAvatarUrl, cleanFileUrl, extractFileIdFromUrl, isUniversalFileUrl } from '/models/lib/universalUrlGenerator'; + +class FixAvatarUrlsMigration { + constructor() { + this.name = 'fixAvatarUrls'; + this.version = 1; + } + + /** + * Check if migration is needed + */ + needsMigration() { + const users = ReactiveCache.getUsers({}); + + for (const user of users) { + if (user.profile && user.profile.avatarUrl) { + const avatarUrl = user.profile.avatarUrl; + if (avatarUrl.includes('auth=false') || avatarUrl.includes('brokenIsFine=true')) { + return true; + } + } + } + + return false; + } + + /** + * Execute the migration + */ + async execute() { + const users = ReactiveCache.getUsers({}); + let avatarsFixed = 0; + + console.log(`Starting avatar URL fix migration...`); + + for (const user of users) { + if (user.profile && user.profile.avatarUrl) { + 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 + cleanUrl = cleanUrl.replace(/[?&]auth=false/g, ''); + cleanUrl = cleanUrl.replace(/[?&]brokenIsFine=true/g, ''); + cleanUrl = cleanUrl.replace(/\?&/g, '?'); + 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 + if (!avatarUrl.startsWith('http') && !avatarUrl.startsWith('/')) { + cleanUrl = `/cdn/storage/avatars/${avatarUrl}`; + 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, { + $set: { + 'profile.avatarUrl': cleanUrl, + modifiedAt: new Date() + } + }); + + avatarsFixed++; + + if (process.env.DEBUG === 'true') { + console.log(`Fixed avatar URL for user ${user.username}: ${avatarUrl} -> ${cleanUrl}`); + } + } + } + } + + console.log(`Avatar URL fix migration completed. Fixed ${avatarsFixed} avatar URLs.`); + + return { + success: true, + avatarsFixed + }; + } +} + +// Export singleton instance +export const fixAvatarUrlsMigration = new FixAvatarUrlsMigration(); + +// Meteor method +Meteor.methods({ + 'fixAvatarUrls.execute'() { + if (!this.userId) { + throw new Meteor.Error('not-authorized'); + } + + return fixAvatarUrlsMigration.execute(); + }, + + 'fixAvatarUrls.needsMigration'() { + if (!this.userId) { + throw new Meteor.Error('not-authorized'); + } + + return fixAvatarUrlsMigration.needsMigration(); + } +}); diff --git a/server/routes/avatarServer.js b/server/routes/avatarServer.js new file mode 100644 index 0000000000..008ea573a2 --- /dev/null +++ b/server/routes/avatarServer.js @@ -0,0 +1,123 @@ +/** + * Avatar File Server + * Handles serving avatar files from the /cdn/storage/avatars/ path + */ + +import { Meteor } from 'meteor/meteor'; +import { WebApp } from 'meteor/webapp'; +import { ReactiveCache } from '/imports/reactiveCache'; +import Avatars from '/models/avatars'; +import { fileStoreStrategyFactory } from '/models/lib/fileStoreStrategy'; +import fs from 'fs'; +import path from 'path'; + +if (Meteor.isServer) { + // Handle avatar file downloads + WebApp.connectHandlers.use('/cdn/storage/avatars/([^/]+)', (req, res, next) => { + if (req.method !== 'GET') { + return next(); + } + + try { + const fileName = req.params[0]; + + if (!fileName) { + res.writeHead(400); + res.end('Invalid avatar file name'); + return; + } + + // Extract file ID from filename (format: fileId-original-filename) + const fileId = fileName.split('-original-')[0]; + + if (!fileId) { + res.writeHead(400); + res.end('Invalid avatar file format'); + return; + } + + // Get avatar file from database + const avatar = ReactiveCache.getAvatar(fileId); + if (!avatar) { + res.writeHead(404); + res.end('Avatar not found'); + return; + } + + // Check if user has permission to view this avatar + // For avatars, we allow viewing by any logged-in user + const userId = Meteor.userId(); + if (!userId) { + res.writeHead(401); + res.end('Authentication required'); + return; + } + + // Get file strategy + const strategy = fileStoreStrategyFactory.getFileStrategy(avatar, 'original'); + const readStream = strategy.getReadStream(); + + if (!readStream) { + res.writeHead(404); + res.end('Avatar file not found in storage'); + return; + } + + // Set appropriate headers + res.setHeader('Content-Type', avatar.type || 'image/jpeg'); + res.setHeader('Content-Length', avatar.size || 0); + res.setHeader('Cache-Control', 'public, max-age=31536000'); // Cache for 1 year + res.setHeader('ETag', `"${avatar._id}"`); + + // Handle conditional requests + const ifNoneMatch = req.headers['if-none-match']; + if (ifNoneMatch && ifNoneMatch === `"${avatar._id}"`) { + res.writeHead(304); + res.end(); + return; + } + + // Stream the file + res.writeHead(200); + readStream.pipe(res); + + readStream.on('error', (error) => { + console.error('Avatar stream error:', error); + if (!res.headersSent) { + res.writeHead(500); + res.end('Error reading avatar file'); + } + }); + + } catch (error) { + console.error('Avatar server error:', error); + if (!res.headersSent) { + res.writeHead(500); + res.end('Internal server error'); + } + } + }); + + // Handle legacy avatar URLs (from CollectionFS) + WebApp.connectHandlers.use('/cfs/files/avatars/([^/]+)', (req, res, next) => { + if (req.method !== 'GET') { + return next(); + } + + try { + const fileName = req.params[0]; + + // Redirect to new avatar URL format + const newUrl = `/cdn/storage/avatars/${fileName}`; + res.writeHead(301, { 'Location': newUrl }); + res.end(); + + } catch (error) { + console.error('Legacy avatar redirect error:', error); + res.writeHead(500); + res.end('Internal server error'); + } + }); + + console.log('Avatar server routes initialized'); +} diff --git a/server/routes/universalFileServer.js b/server/routes/universalFileServer.js new file mode 100644 index 0000000000..2a2cb2e392 --- /dev/null +++ b/server/routes/universalFileServer.js @@ -0,0 +1,393 @@ +/** + * Universal File Server + * Ensures all attachments and avatars are always visible regardless of ROOT_URL and PORT settings + * Handles both new Meteor-Files and legacy CollectionFS file serving + */ + +import { Meteor } from 'meteor/meteor'; +import { WebApp } from 'meteor/webapp'; +import { ReactiveCache } from '/imports/reactiveCache'; +import Attachments from '/models/attachments'; +import Avatars from '/models/avatars'; +import { fileStoreStrategyFactory } from '/models/lib/fileStoreStrategy'; +import { getAttachmentWithBackwardCompatibility, getOldAttachmentStream } from '/models/lib/attachmentBackwardCompatibility'; +import fs from 'fs'; +import path from 'path'; + +if (Meteor.isServer) { + console.log('Universal file server initializing...'); + + /** + * Helper function to set appropriate headers for file serving + */ + function setFileHeaders(res, fileObj, isAttachment = false) { + // Set content type + res.setHeader('Content-Type', fileObj.type || (isAttachment ? 'application/octet-stream' : 'image/jpeg')); + + // Set content length + res.setHeader('Content-Length', fileObj.size || 0); + + // Set cache headers + res.setHeader('Cache-Control', 'public, max-age=31536000'); // Cache for 1 year + res.setHeader('ETag', `"${fileObj._id}"`); + + // Set security headers for attachments + if (isAttachment) { + const isSvgFile = fileObj.name && fileObj.name.toLowerCase().endsWith('.svg'); + const disposition = isSvgFile ? 'attachment' : 'inline'; + res.setHeader('Content-Disposition', `${disposition}; filename="${fileObj.name}"`); + + // Add security headers for SVG files + if (isSvgFile) { + res.setHeader('Content-Security-Policy', "default-src 'none'; script-src 'none'; object-src 'none';"); + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'DENY'); + } + } + } + + /** + * Helper function to handle conditional requests + */ + function handleConditionalRequest(req, res, fileObj) { + const ifNoneMatch = req.headers['if-none-match']; + if (ifNoneMatch && ifNoneMatch === `"${fileObj._id}"`) { + res.writeHead(304); + res.end(); + return true; + } + return false; + } + + /** + * Helper function to stream file with error handling + */ + function streamFile(res, readStream, fileObj) { + readStream.on('error', (error) => { + console.error('File stream error:', error); + if (!res.headersSent) { + res.writeHead(500); + res.end('Error reading file'); + } + }); + + readStream.on('end', () => { + if (!res.headersSent) { + res.writeHead(200); + } + }); + + readStream.pipe(res); + } + + // ============================================================================ + // NEW METEOR-FILES ROUTES (URL-agnostic) + // ============================================================================ + + /** + * Serve attachments from new Meteor-Files structure + * Route: /cdn/storage/attachments/{fileId} or /cdn/storage/attachments/{fileId}/original/{filename} + */ + WebApp.connectHandlers.use('/cdn/storage/attachments/([^/]+)(?:/original/[^/]+)?', (req, res, next) => { + if (req.method !== 'GET') { + return next(); + } + + try { + const fileId = req.params[0]; + + if (!fileId) { + res.writeHead(400); + res.end('Invalid attachment file ID'); + return; + } + + // Get attachment from database + const attachment = ReactiveCache.getAttachment(fileId); + if (!attachment) { + res.writeHead(404); + res.end('Attachment not found'); + return; + } + + // Check permissions + const board = ReactiveCache.getBoard(attachment.meta.boardId); + if (!board) { + res.writeHead(404); + res.end('Board not found'); + return; + } + + // Check if user has permission to download + const userId = Meteor.userId(); + if (!board.isPublic() && (!userId || !board.hasMember(userId))) { + res.writeHead(403); + res.end('Access denied'); + return; + } + + // Handle conditional requests + if (handleConditionalRequest(req, res, attachment)) { + return; + } + + // Get file strategy and stream + const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original'); + const readStream = strategy.getReadStream(); + + if (!readStream) { + res.writeHead(404); + res.end('Attachment file not found in storage'); + return; + } + + // Set headers and stream file + setFileHeaders(res, attachment, true); + streamFile(res, readStream, attachment); + + } catch (error) { + console.error('Attachment server error:', error); + if (!res.headersSent) { + res.writeHead(500); + res.end('Internal server error'); + } + } + }); + + /** + * Serve avatars from new Meteor-Files structure + * Route: /cdn/storage/avatars/{fileId} or /cdn/storage/avatars/{fileId}/original/{filename} + */ + WebApp.connectHandlers.use('/cdn/storage/avatars/([^/]+)(?:/original/[^/]+)?', (req, res, next) => { + if (req.method !== 'GET') { + return next(); + } + + try { + const fileId = req.params[0]; + + if (!fileId) { + res.writeHead(400); + res.end('Invalid avatar file ID'); + return; + } + + // Get avatar from database + const avatar = ReactiveCache.getAvatar(fileId); + if (!avatar) { + res.writeHead(404); + res.end('Avatar not found'); + return; + } + + // Check if user has permission to view this avatar + // For avatars, we allow viewing by any logged-in user + const userId = Meteor.userId(); + if (!userId) { + res.writeHead(401); + res.end('Authentication required'); + return; + } + + // Handle conditional requests + if (handleConditionalRequest(req, res, avatar)) { + return; + } + + // Get file strategy and stream + const strategy = fileStoreStrategyFactory.getFileStrategy(avatar, 'original'); + const readStream = strategy.getReadStream(); + + if (!readStream) { + res.writeHead(404); + res.end('Avatar file not found in storage'); + return; + } + + // Set headers and stream file + setFileHeaders(res, avatar, false); + streamFile(res, readStream, avatar); + + } catch (error) { + console.error('Avatar server error:', error); + if (!res.headersSent) { + res.writeHead(500); + res.end('Internal server error'); + } + } + }); + + // ============================================================================ + // LEGACY COLLECTIONFS ROUTES (Backward compatibility) + // ============================================================================ + + /** + * Serve legacy attachments from CollectionFS structure + * Route: /cfs/files/attachments/{attachmentId} + */ + WebApp.connectHandlers.use('/cfs/files/attachments/([^/]+)', (req, res, next) => { + if (req.method !== 'GET') { + return next(); + } + + try { + const attachmentId = req.params[0]; + + if (!attachmentId) { + res.writeHead(400); + res.end('Invalid attachment ID'); + return; + } + + // Try to get attachment with backward compatibility + const attachment = getAttachmentWithBackwardCompatibility(attachmentId); + if (!attachment) { + res.writeHead(404); + res.end('Attachment not found'); + return; + } + + // Check permissions + const board = ReactiveCache.getBoard(attachment.meta.boardId); + if (!board) { + res.writeHead(404); + res.end('Board not found'); + return; + } + + // Check if user has permission to download + const userId = Meteor.userId(); + if (!board.isPublic() && (!userId || !board.hasMember(userId))) { + res.writeHead(403); + res.end('Access denied'); + return; + } + + // Handle conditional requests + if (handleConditionalRequest(req, res, attachment)) { + return; + } + + // For legacy attachments, try to get GridFS stream + const fileStream = getOldAttachmentStream(attachmentId); + if (fileStream) { + setFileHeaders(res, attachment, true); + streamFile(res, fileStream, attachment); + } else { + res.writeHead(404); + res.end('Legacy attachment file not found in GridFS'); + } + + } catch (error) { + console.error('Legacy attachment server error:', error); + if (!res.headersSent) { + res.writeHead(500); + res.end('Internal server error'); + } + } + }); + + /** + * Serve legacy avatars from CollectionFS structure + * Route: /cfs/files/avatars/{avatarId} + */ + WebApp.connectHandlers.use('/cfs/files/avatars/([^/]+)', (req, res, next) => { + if (req.method !== 'GET') { + return next(); + } + + try { + const avatarId = req.params[0]; + + if (!avatarId) { + res.writeHead(400); + res.end('Invalid avatar ID'); + return; + } + + // Try to get avatar from database (new structure first) + let avatar = ReactiveCache.getAvatar(avatarId); + + // If not found in new structure, try to handle legacy format + if (!avatar) { + // For legacy avatars, we might need to handle different ID formats + // This is a fallback for old CollectionFS avatars + res.writeHead(404); + res.end('Avatar not found'); + return; + } + + // Check if user has permission to view this avatar + const userId = Meteor.userId(); + if (!userId) { + res.writeHead(401); + res.end('Authentication required'); + return; + } + + // Handle conditional requests + if (handleConditionalRequest(req, res, avatar)) { + return; + } + + // Get file strategy and stream + const strategy = fileStoreStrategyFactory.getFileStrategy(avatar, 'original'); + const readStream = strategy.getReadStream(); + + if (!readStream) { + res.writeHead(404); + res.end('Avatar file not found in storage'); + return; + } + + // Set headers and stream file + setFileHeaders(res, avatar, false); + streamFile(res, readStream, avatar); + + } catch (error) { + console.error('Legacy avatar server error:', error); + if (!res.headersSent) { + res.writeHead(500); + res.end('Internal server error'); + } + } + }); + + // ============================================================================ + // ALTERNATIVE ROUTES (For different URL patterns) + // ============================================================================ + + /** + * Alternative attachment route for different URL patterns + * Route: /attachments/{fileId} + */ + WebApp.connectHandlers.use('/attachments/([^/]+)', (req, res, next) => { + if (req.method !== 'GET') { + return next(); + } + + // Redirect to standard route + const fileId = req.params[0]; + const newUrl = `/cdn/storage/attachments/${fileId}`; + res.writeHead(301, { 'Location': newUrl }); + res.end(); + }); + + /** + * Alternative avatar route for different URL patterns + * Route: /avatars/{fileId} + */ + WebApp.connectHandlers.use('/avatars/([^/]+)', (req, res, next) => { + if (req.method !== 'GET') { + return next(); + } + + // Redirect to standard route + const fileId = req.params[0]; + const newUrl = `/cdn/storage/avatars/${fileId}`; + res.writeHead(301, { 'Location': newUrl }); + res.end(); + }); + + console.log('Universal file server initialized successfully'); +} From 0fc2ad97cd10fe473f4aec11f9a1bf92a2d2132e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 20:32:10 +0000 Subject: [PATCH 15/17] Bump actions/upload-artifact from 4 to 5 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test_suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index b3498f613d..1160226dd0 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -136,7 +136,7 @@ jobs: run: sh ./test-wekan.sh -cv - name: Upload coverage - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: coverage-folder path: .coverage/ From 3204311ac1c7a6778faa0a2ce613cf4670f0cb6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 23:12:09 +0000 Subject: [PATCH 16/17] Bump actions/download-artifact from 5 to 6 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test_suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index b3498f613d..3730decfd8 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -150,7 +150,7 @@ jobs: uses: actions/checkout@v5 - name: Download coverage - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: coverage-folder path: .coverage/ From 5079c853a7280a29797385cf25b234a4a3974730 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Wed, 29 Oct 2025 02:58:00 +0200 Subject: [PATCH 17/17] Updated translations. --- imports/i18n/data/ko.i18n.json | 18 ++-- imports/i18n/data/sv.i18n.json | 144 +++++++++++++++--------------- imports/i18n/data/zh-TW.i18n.json | 8 +- 3 files changed, 85 insertions(+), 85 deletions(-) diff --git a/imports/i18n/data/ko.i18n.json b/imports/i18n/data/ko.i18n.json index 62f508b82a..1796716bad 100644 --- a/imports/i18n/data/ko.i18n.json +++ b/imports/i18n/data/ko.i18n.json @@ -190,9 +190,9 @@ "board-view-collapse": "접기", "board-view-gantt": "간트", "board-view-lists": "목록들", - "bucket-example": "Like \"Bucket List\" for example", - "calendar-previous-month-label": "Previous Month", - "calendar-next-month-label": "Next Month", + "bucket-example": "예: \"꼭 하고싶은 일 목록\"", + "calendar-previous-month-label": "이전 월", + "calendar-next-month-label": "다음 월", "cancel": "취소", "card-archived": "이 카드는 보관함으로 이동 되었습니다.", "board-archived": "이 보드는 보관함으로 이동 되었습니다.", @@ -356,7 +356,7 @@ "custom-field-text": "텍스트", "custom-fields": "사용자정의 항목", "date": "날짜", - "date-format": "Date Format", + "date-format": "날짜 형식", "date-format-yyyy-mm-dd": "YYYY-MM-DD", "date-format-dd-mm-yyyy": "DD-MM-YYYY", "date-format-mm-dd-yyyy": "MM-DD-YYYY", @@ -385,7 +385,7 @@ "editNotificationPopup-title": "알림 수정", "editProfilePopup-title": "프로필 변경", "email": "이메일", - "email-address": "Email Address", + "email-address": "이메일 주소", "email-enrollAccount-subject": "__siteName__에 계정 생성이 완료되었습니다.", "email-enrollAccount-text": "안녕하세요. __user__님,\n\n시작하려면 아래링크를 클릭해 주세요.\n\n__url__\n\n감사합니다.", "email-fail": "이메일 전송 실패", @@ -755,8 +755,8 @@ "delete-board-confirm-popup": "모든 목록, 카드, 레이블 및 활동이 삭제되고 보드 내용을 복구할 수 없습니다. 실행 취소는 불가능합니다.", "boardDeletePopup-title": "보드 삭제?", "delete-board": "보드 삭제", - "delete-duplicate-lists": "Delete Duplicate Lists", - "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", + "delete-duplicate-lists": "중복 목록 삭제", + "delete-duplicate-lists-confirm": "확실합니까? 이렇게 하면 이름이 같고 카드가 없는 모든 중복 목록이 삭제됩니다.", "default-subtasks-board": "Subtasks for __board__ board", "default": "기본", "defaultdefault": "기본", @@ -1018,8 +1018,8 @@ "dueCardsViewChange-choice-me": "Me", "dueCardsViewChange-choice-all": "All Users", "dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.", - "dueCards-noResults-title": "No Due Cards Found", - "dueCards-noResults-description": "You don't have any cards with due dates at the moment.", + "dueCards-noResults-title": "만료된 카드를 찾을 수 없음", + "dueCards-noResults-description": "현재 마감일이 있는 카드가 없습니다.", "broken-cards": "Broken Cards", "board-title-not-found": "보드 %s 을 찾을 수 없습니다.", "swimlane-title-not-found": "Swimlane %s 을 찾을 수 없습니다.", diff --git a/imports/i18n/data/sv.i18n.json b/imports/i18n/data/sv.i18n.json index 6a4e1616dc..2297e547bd 100644 --- a/imports/i18n/data/sv.i18n.json +++ b/imports/i18n/data/sv.i18n.json @@ -1334,70 +1334,70 @@ "avatars-path": "Sökväg för avatarer", "avatars-path-description": "Sökväg där avatarfiler lagras", "board-archive-failed": "Misslyckades att schemalägga arkivering av tavla", - "board-archive-scheduled": "Board archive scheduled successfully", - "board-backup-failed": "Failed to schedule board backup", - "board-backup-scheduled": "Board backup scheduled successfully", - "board-cleanup-failed": "Failed to schedule board cleanup", - "board-cleanup-scheduled": "Board cleanup scheduled successfully", - "board-operations": "Board Operations", - "cron-jobs": "Scheduled Jobs", - "cron-migrations": "Scheduled Migrations", - "cron-job-delete-confirm": "Are you sure you want to delete this scheduled job?", - "cron-job-delete-failed": "Failed to delete scheduled job", - "cron-job-deleted": "Scheduled job deleted successfully", - "cron-job-pause-failed": "Failed to pause scheduled job", - "cron-job-paused": "Scheduled job paused successfully", - "filesystem-path-description": "Base path for file storage", - "gridfs-enabled": "GridFS Enabled", - "gridfs-enabled-description": "Use MongoDB GridFS for file storage", - "migration-pause-failed": "Failed to pause migrations", - "migration-paused": "Migrations paused successfully", - "migration-progress": "Migration Progress", - "migration-start-failed": "Failed to start migrations", - "migration-started": "Migrations started successfully", - "migration-status": "Migration Status", - "migration-stop-confirm": "Are you sure you want to stop all migrations?", - "migration-stop-failed": "Failed to stop migrations", - "migration-stopped": "Migrations stopped successfully", - "mongodb-gridfs-storage": "MongoDB GridFS Storage", - "pause-all-migrations": "Pause All Migrations", - "s3-access-key": "S3 Access Key", - "s3-access-key-description": "AWS S3 access key for authentication", - "s3-access-key-placeholder": "Enter S3 access key", - "s3-bucket": "S3 Bucket", - "s3-bucket-description": "S3 bucket name for storing files", - "s3-connection-failed": "S3 connection failed", - "s3-connection-success": "S3 connection successful", - "s3-enabled": "S3 Enabled", - "s3-enabled-description": "Use AWS S3 or MinIO for file storage", - "s3-endpoint": "S3 Endpoint", - "s3-endpoint-description": "S3 endpoint URL (e.g., s3.amazonaws.com or minio.example.com)", - "s3-minio-storage": "S3/MinIO Storage", - "s3-port": "S3 Port", - "s3-port-description": "S3 endpoint port number", - "s3-region": "S3 Region", - "s3-region-description": "AWS S3 region (e.g., us-east-1)", - "s3-secret-key": "S3 Secret Key", - "s3-secret-key-description": "AWS S3 secret key for authentication", - "s3-secret-key-placeholder": "Enter S3 secret key", - "s3-secret-key-required": "S3 secret key is required", - "s3-settings-save-failed": "Failed to save S3 settings", - "s3-settings-saved": "S3 settings saved successfully", - "s3-ssl-enabled": "S3 SSL Enabled", - "s3-ssl-enabled-description": "Use SSL/TLS for S3 connections", - "save-s3-settings": "Save S3 Settings", - "schedule-board-archive": "Schedule Board Archive", - "schedule-board-backup": "Schedule Board Backup", - "schedule-board-cleanup": "Schedule Board Cleanup", - "scheduled-board-operations": "Scheduled Board Operations", - "start-all-migrations": "Start All Migrations", - "stop-all-migrations": "Stop All Migrations", - "test-s3-connection": "Test S3 Connection", - "writable-path": "Writable Path", - "writable-path-description": "Base directory path for file storage", - "add-job": "Add Job", - "attachment-migration": "Attachment Migration", - "attachment-monitoring": "Attachment Monitoring", + "board-archive-scheduled": "Tavlans arkivering har schemalagts", + "board-backup-failed": "Misslyckades med att schemalägga säkerhetskopiering av tavlan", + "board-backup-scheduled": "Säkerhetskopiering av tavlan har schemalagts", + "board-cleanup-failed": "Misslyckades med att schemalägga rensning av tavlan", + "board-cleanup-scheduled": "Rensning av tavlan har schemalagts", + "board-operations": "Tavlans åtgärder", + "cron-jobs": "Schemalagda jobb", + "cron-migrations": "Schemalagda migreringar", + "cron-job-delete-confirm": "Är du säker på att du vill ta bort det här schemalagda jobbet?", + "cron-job-delete-failed": "Misslyckades med att ta bort schemalagt jobb", + "cron-job-deleted": "Schemalagt jobb borttaget", + "cron-job-pause-failed": "Misslyckades med att pausa schemalagt jobb", + "cron-job-paused": "Schemalagt jobb pausat", + "filesystem-path-description": "Basväg för fillagring", + "gridfs-enabled": "GridFS aktiverat", + "gridfs-enabled-description": "Använd MongoDB GridFS för fillagring", + "migration-pause-failed": "Misslyckades med att pausa migreringar", + "migration-paused": "Migreringar har pausats", + "migration-progress": "Migreringsförlopp", + "migration-start-failed": "Misslyckades med att starta migreringar", + "migration-started": "Migreringar har startats", + "migration-status": "Migreringsstatus", + "migration-stop-confirm": "Är du säker på att du vill stoppa alla migreringar?", + "migration-stop-failed": "Misslyckades med att stoppa migreringar", + "migration-stopped": "Migreringar har stoppats", + "mongodb-gridfs-storage": "MongoDB GridFS-lagring", + "pause-all-migrations": "Pausa alla migreringar", + "s3-access-key": "S3-åtkomstnyckel", + "s3-access-key-description": "AWS S3-åtkomstnyckel för autentisering", + "s3-access-key-placeholder": "Ange S3-åtkomstnyckel", + "s3-bucket": "S3-hink", + "s3-bucket-description": "Namn på S3-hink för lagring av filer", + "s3-connection-failed": "S3-anslutning misslyckades", + "s3-connection-success": "S3-anslutning lyckades", + "s3-enabled": "S3 aktiverat", + "s3-enabled-description": "Använd AWS S3 eller MinIO för fillagring", + "s3-endpoint": "S3-endpunkt", + "s3-endpoint-description": "S3-endpunkts-URL (t.ex. s3.amazonaws.com eller minio.example.com)", + "s3-minio-storage": "S3/MinIO-lagring", + "s3-port": "S3-port", + "s3-port-description": "Portnummer för S3-endpunkt", + "s3-region": "S3-region", + "s3-region-description": "AWS S3-region (t.ex. us-east-1)", + "s3-secret-key": "S3-hemlig nyckel", + "s3-secret-key-description": "AWS S3-hemlig nyckel för autentisering", + "s3-secret-key-placeholder": "Ange S3-hemlig nyckel", + "s3-secret-key-required": "S3-hemlig nyckel krävs", + "s3-settings-save-failed": "Misslyckades med att spara S3-inställningar", + "s3-settings-saved": "S3-inställningar har sparats", + "s3-ssl-enabled": "S3 SSL aktiverat", + "s3-ssl-enabled-description": "Använd SSL/TLS för S3-anslutningar", + "save-s3-settings": "Spara S3-inställningar", + "schedule-board-archive": "Schemalägg tavlarkivering", + "schedule-board-backup": "Schemalägg säkerhetskopiering av tavlan", + "schedule-board-cleanup": "Schemalägg rensning av tavlan", + "scheduled-board-operations": "Schemalagda tavlans åtgärder", + "start-all-migrations": "Starta alla migreringar", + "stop-all-migrations": "Stoppa alla migreringar", + "test-s3-connection": "Testa S3-anslutning", + "writable-path": "Skrivbar sökväg", + "writable-path-description": "Baskatalogsökväg för fillagring", + "add-job": "Lägg till jobb", + "attachment-migration": "Migrering av bilagor", + "attachment-monitoring": "Övervakning av bilagor", "attachment-settings": "Attachment Settings", "attachment-storage-settings": "Storage Settings", "automatic-migration": "Automatic Migration", @@ -1420,14 +1420,14 @@ "days-old": "Days Old", "duration": "Duration", "errors": "Errors", - "estimated-time-remaining": "Estimated time remaining", - "every-1-day": "Every 1 day", - "every-1-hour": "Every 1 hour", - "every-1-minute": "Every 1 minute", - "every-10-minutes": "Every 10 minutes", - "every-30-minutes": "Every 30 minutes", - "every-5-minutes": "Every 5 minutes", - "every-6-hours": "Every 6 hours", + "estimated-time-remaining": "Beräknad återstående tid", + "every-1-day": "Var 1 dag", + "every-1-hour": "Var 1 timme", + "every-1-minute": "Var 1 minut", + "every-10-minutes": "Var 10 minuter", + "every-30-minutes": "Var 30 minuter", + "every-5-minutes": "Var 5 minuter", + "every-6-hours": "Var 6 timmar", "export-monitoring": "Export Monitoring", "filesystem-attachments": "Filesystem Attachments", "filesystem-size": "Filesystem Size", diff --git a/imports/i18n/data/zh-TW.i18n.json b/imports/i18n/data/zh-TW.i18n.json index 754785cd26..a6f66caed0 100644 --- a/imports/i18n/data/zh-TW.i18n.json +++ b/imports/i18n/data/zh-TW.i18n.json @@ -187,7 +187,7 @@ "enter-zoom-level": "輸入縮放層級 (50-300%):", "board-view-cal": "日曆", "board-view-swimlanes": "泳道", - "board-view-collapse": "損毀", + "board-view-collapse": "折疊", "board-view-gantt": "甘特圖", "board-view-lists": "清單", "bucket-example": "例如「人生清單」", @@ -755,8 +755,8 @@ "delete-board-confirm-popup": "所有清單、卡片、標籤和活動都會被刪除,將無法恢覆看板內容。不支援撤銷。", "boardDeletePopup-title": "刪除看板?", "delete-board": "刪除看板", - "delete-duplicate-lists": "Delete Duplicate Lists", - "delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.", + "delete-duplicate-lists": "刪除重複的清單", + "delete-duplicate-lists-confirm": "您確定嗎?這將會刪除所有相同名稱但不包含卡片的重複清單。", "default-subtasks-board": "__board__ 看板的子任務", "default": "預設值", "defaultdefault": "預設值", @@ -1284,7 +1284,7 @@ "show-week-of-year": "顯示年度週數 (ISO 8601)", "convert-to-markdown": "轉換為 Markdown", "import-board-zip": "新增包含看板 JSON 檔案與帶有附件的看板名稱子目錄的 .zip 檔案", - "collapse": "損毀", + "collapse": "折疊", "uncollapse": "展開", "hideCheckedChecklistItems": "隱藏已勾選的待辦清單項目", "hideAllChecklistItems": "隱藏所有待辦清單項目",