mirror of
https://github.com/wekan/wekan.git
synced 2026-11-08 11:25:19 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef7771febb | ||
|
|
12cba0e148 | ||
|
|
c3a4052227 | ||
|
|
82f048ccef | ||
|
|
7a585a3dfb | ||
|
|
8d3b53f51d | ||
|
|
d73e006935 | ||
|
|
9536e60bd1 | ||
|
|
678ca978a3 | ||
|
|
39420877fd | ||
|
|
6ea03cfba3 | ||
|
|
9214b56aea | ||
|
|
699b4c464f | ||
|
|
9fa54a3148 | ||
|
|
f2019b1059 | ||
|
|
714bbd0fb0 | ||
|
|
80777b4663 | ||
|
|
9473c1fe41 | ||
|
|
98f141d62f | ||
|
|
85dd213b14 |
No files matched your search
@@ -19,6 +19,42 @@ Fixing other platforms In Progress.
|
||||
|
||||
[Upgrade WeKan](https://wekan.fi/upgrade/)
|
||||
|
||||
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.09 2025-10-21 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Fix Admin Panel / People editing and layout](https://github.com/wekan/wekan/commit/7a585a3dfb080af51f88669ea5928f715779cee4).
|
||||
Thanks to xet7.
|
||||
- [Fix upgrade to 8.08 duplicates lists](https://github.com/wekan/wekan/commit/c3a405222782a4a91eb8725faaa8309f0926dcc4).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.08 2025-10-21 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Fix opening board migration of Shared Lists to Per-Swimlane lists to use ReactiveCache correctly without errors](https://github.com/wekan/wekan/commit/9536e60bd1c77c8a22e89d2eb2968e11da3a28cd).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.07 2025-10-20 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Fix Snap Candidate WeKan 8.00-8.06 commit ae01ea5 database directory from /var/snap/wekan/common/wekan back to 8.07 /var/snap/wekan/common](https://github.com/wekan/wekan/commit/98f141d62f3b6d4371d024c72eae6688d0f4e516).
|
||||
Thanks to xet7.
|
||||
- [When opening board, add missing lists](https://github.com/wekan/wekan/commit/80777b46638ed15b8194105751499ada4b066d19).
|
||||
Thanks to xet7.
|
||||
- [If Snap Candidate MongoDB raw database files were at SNAP_COMMON/wekan, migrate them back to SNAP_COMMON](https://github.com/wekan/wekan/commit/f2019b1059c8d6f4cd9a46c3db7e004c4928cebb).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.06 2025-10-20 WeKan ® release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
+3
-3
@@ -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.06/wekan-8.06-amd64.zip"
|
||||
unzip wekan-8.06-amd64.zip
|
||||
rm wekan-8.06-amd64.zip
|
||||
wget "https://github.com/wekan/wekan/releases/download/v8.09/wekan-8.09-amd64.zip"
|
||||
unzip wekan-8.09-amd64.zip
|
||||
rm wekan-8.09-amd64.zip
|
||||
mv /home/wekan/app/bundle /build
|
||||
|
||||
# Put back the original tar
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v8.06.0"
|
||||
appVersion: "v8.09.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -115,6 +115,9 @@ BlazeComponent.extendComponent({
|
||||
// Convert shared lists to per-swimlane lists if needed
|
||||
await this.convertSharedListsToPerSwimlane(boardId);
|
||||
|
||||
// Fix missing lists migration (for cards with wrong listId references)
|
||||
await this.fixMissingLists(boardId);
|
||||
|
||||
// Start attachment migration in background if needed
|
||||
this.startAttachmentMigrationIfNeeded(boardId);
|
||||
} catch (error) {
|
||||
@@ -200,7 +203,7 @@ BlazeComponent.extendComponent({
|
||||
boardId: boardId,
|
||||
swimlaneId: swimlane._id,
|
||||
sort: sharedList.sort || 0,
|
||||
archived: sharedList.archived || false,
|
||||
archived: sharedList.archived || false, // Preserve archived state from original list
|
||||
createdAt: new Date(),
|
||||
modifiedAt: new Date()
|
||||
};
|
||||
@@ -212,16 +215,24 @@ BlazeComponent.extendComponent({
|
||||
if (sharedList.wipLimitSoft) newListData.wipLimitSoft = sharedList.wipLimitSoft;
|
||||
|
||||
Lists.insert(newListData);
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
const archivedStatus = sharedList.archived ? ' (archived)' : ' (active)';
|
||||
console.log(`Created list "${sharedList.title}"${archivedStatus} for swimlane ${swimlane.title || swimlane._id}`);
|
||||
}
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`List "${sharedList.title}" already exists in swimlane ${swimlane.title || swimlane._id}, skipping`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Archive or remove the original shared list
|
||||
Lists.update(sharedList._id, {
|
||||
$set: {
|
||||
archived: true,
|
||||
modifiedAt: new Date()
|
||||
}
|
||||
});
|
||||
// Remove the original shared list completely
|
||||
Lists.remove(sharedList._id);
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Removed shared list "${sharedList.title}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark board as processed
|
||||
@@ -236,6 +247,61 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
},
|
||||
|
||||
async fixMissingLists(boardId) {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) return;
|
||||
|
||||
// Check if board has already been processed for missing lists fix
|
||||
if (board.fixMissingListsCompleted) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has already been processed for missing lists fix`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if migration is needed
|
||||
const needsMigration = await new Promise((resolve, reject) => {
|
||||
Meteor.call('fixMissingListsMigration.needsMigration', boardId, (error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!needsMigration) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} does not need missing lists fix`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Starting fix missing lists migration for board ${boardId}`);
|
||||
}
|
||||
|
||||
// Execute the migration
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
Meteor.call('fixMissingListsMigration.execute', boardId, (error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (result && result.success) {
|
||||
console.log(`Successfully fixed missing lists for board ${boardId}: created ${result.createdLists} lists, updated ${result.updatedCards} cards`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fixing missing lists:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async startAttachmentMigrationIfNeeded(boardId) {
|
||||
try {
|
||||
// Check if board has already been migrated
|
||||
|
||||
@@ -93,6 +93,29 @@
|
||||
max-height: inherit;
|
||||
}
|
||||
|
||||
/* Admin edit popups: use full height */
|
||||
.pop-over[data-popup="editUser"],
|
||||
.pop-over[data-popup="editOrg"],
|
||||
.pop-over[data-popup="editTeam"] {
|
||||
height: calc(100vh - 20px) !important;
|
||||
max-height: calc(100vh - 20px) !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editUser"] .content-wrapper,
|
||||
.pop-over[data-popup="editOrg"] .content-wrapper,
|
||||
.pop-over[data-popup="editTeam"] .content-wrapper {
|
||||
max-height: calc(100vh - 80px) !important; /* Subtract header height */
|
||||
height: calc(100vh - 80px) !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editUser"] .content-container,
|
||||
.pop-over[data-popup="editOrg"] .content-container,
|
||||
.pop-over[data-popup="editTeam"] .content-container {
|
||||
max-height: calc(100vh - 80px) !important; /* Subtract header height */
|
||||
height: calc(100vh - 80px) !important;
|
||||
}
|
||||
|
||||
/* Ensure language popup list can scroll properly */
|
||||
.pop-over .pop-over-list {
|
||||
max-height: none;
|
||||
|
||||
@@ -71,6 +71,13 @@ table tr:nth-child(even) {
|
||||
left: 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Make checkbox column fit content */
|
||||
table th:first-child,
|
||||
table td:first-child {
|
||||
width: auto;
|
||||
min-width: auto;
|
||||
}
|
||||
#divAddOrRemoveTeam {
|
||||
background: #008000;
|
||||
display: none;
|
||||
@@ -141,7 +148,7 @@ table tr:nth-child(even) {
|
||||
}
|
||||
|
||||
.account-active-status {
|
||||
width: 20px;
|
||||
width: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -170,7 +177,7 @@ table tr:nth-child(even) {
|
||||
}
|
||||
|
||||
.account-status {
|
||||
width: 20px;
|
||||
width: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ template(name="people")
|
||||
option(value="locked") {{_ 'admin-people-filter-locked'}}
|
||||
option(value="active") {{_ 'admin-people-filter-active'}}
|
||||
option(value="inactive") {{_ 'admin-people-filter-inactive'}}
|
||||
option(value="admin") Admin
|
||||
button#unlockAllUsers.unlock-all-btn
|
||||
| 🔓
|
||||
| {{_ 'accounts-lockout-unlock-all'}}
|
||||
@@ -57,7 +58,7 @@ template(name="people")
|
||||
| {{_ 'add'}} / {{_ 'delete'}} {{_ 'teams'}}
|
||||
else if lockedUsersSetting.get
|
||||
span
|
||||
| 🔒.text-red
|
||||
span.text-red 🔒
|
||||
unless isMiniScreen
|
||||
| {{_ 'accounts-lockout-locked-users'}}
|
||||
|
||||
@@ -78,7 +79,7 @@ template(name="people")
|
||||
| {{_ 'people'}}
|
||||
li
|
||||
a.js-locked-users-menu(data-id="locked-users-setting")
|
||||
| 🔒.text-red
|
||||
span.text-red 🔒
|
||||
| {{_ 'accounts-lockout-locked-users'}}
|
||||
.main-body
|
||||
if loading.get
|
||||
@@ -100,7 +101,6 @@ template(name="orgGeneral")
|
||||
th {{_ 'displayName'}}
|
||||
th {{_ 'description'}}
|
||||
th {{_ 'shortName'}}
|
||||
th {{_ 'autoAddUsersWithDomainName'}}
|
||||
th {{_ 'website'}}
|
||||
th {{_ 'createdAt'}}
|
||||
th {{_ 'active'}}
|
||||
@@ -140,16 +140,9 @@ template(name="peopleGeneral")
|
||||
th {{_ 'admin-people-active-status'}}
|
||||
th {{_ 'username'}}
|
||||
th {{_ 'fullname'}}
|
||||
th {{_ 'initials'}}
|
||||
th {{_ 'admin'}}
|
||||
th {{_ 'email'}}
|
||||
th {{_ 'verified'}}
|
||||
th {{_ 'createdAt'}}
|
||||
th {{_ 'active'}}
|
||||
th {{_ 'authentication-method'}}
|
||||
th {{_ 'import-usernames'}}
|
||||
th {{_ 'organizations'}}
|
||||
th {{_ 'teams'}}
|
||||
th
|
||||
+newUserRow
|
||||
tbody
|
||||
@@ -190,10 +183,6 @@ template(name="orgRow")
|
||||
td {{ orgData.orgShortName }}
|
||||
else
|
||||
td <s>{{ orgData.orgShortName }}</s>
|
||||
if orgData.orgIsActive
|
||||
td {{ orgData.orgAutoAddUsersWithDomainName }}
|
||||
else
|
||||
td <s>{{ orgData.orgAutoAddUsersWithDomainName }}</s>
|
||||
if orgData.orgIsActive
|
||||
td {{ orgData.orgWebsite }}
|
||||
else
|
||||
@@ -258,14 +247,14 @@ template(name="peopleRow")
|
||||
input.selectUserChkBox(type="checkbox", id="{{userData._id}}")
|
||||
td.account-status
|
||||
if isUserLocked
|
||||
| 🔒.text-red.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="true", title="{{_ 'accounts-lockout-click-to-unlock'}}")
|
||||
span.text-red.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="true", title="{{_ 'accounts-lockout-click-to-unlock'}}") 🔒
|
||||
else
|
||||
| 🔓.text-green.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="false", title="{{_ 'accounts-lockout-user-unlocked'}}")
|
||||
span.text-green.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="false", title="{{_ 'accounts-lockout-user-unlocked'}}") 🔓
|
||||
td.account-active-status
|
||||
if userData.loginDisabled
|
||||
| 🚫.text-red.js-toggle-active-status(data-user-id=userData._id, data-is-active="false", title="{{_ 'admin-people-user-inactive'}}")
|
||||
span.text-red.js-toggle-active-status(data-user-id=userData._id, data-is-active="false", title="{{_ 'admin-people-user-inactive'}}") 🚫
|
||||
else
|
||||
| ✅.text-green.js-toggle-active-status(data-user-id=userData._id, data-is-active="true", title="{{_ 'admin-people-user-active'}}")
|
||||
span.text-green.js-toggle-active-status(data-user-id=userData._id, data-is-active="true", title="{{_ 'admin-people-user-active'}}") ✅
|
||||
if userData.loginDisabled
|
||||
td.username <s>{{ userData.username }}</s>
|
||||
else if isUserLocked
|
||||
@@ -276,10 +265,6 @@ template(name="peopleRow")
|
||||
td <s>{{ userData.profile.fullname }}</s>
|
||||
else
|
||||
td {{ userData.profile.fullname }}
|
||||
if userData.loginDisabled
|
||||
td <s>{{ userData.profile.initials }}</s>
|
||||
else
|
||||
td {{ userData.profile.initials }}
|
||||
if userData.loginDisabled
|
||||
td
|
||||
if userData.isAdmin
|
||||
@@ -296,43 +281,10 @@ template(name="peopleRow")
|
||||
td <s>{{ userData.emails.[0].address }}</s>
|
||||
else
|
||||
td {{ userData.emails.[0].address }}
|
||||
if userData.loginDisabled
|
||||
td
|
||||
if userData.emails.[0].verified
|
||||
| <s>{{_ 'yes'}}</s>
|
||||
else
|
||||
| <s>{{_ 'no'}}</s>
|
||||
else
|
||||
td
|
||||
if userData.emails.[0].verified
|
||||
| {{_ 'yes'}}
|
||||
else
|
||||
| {{_ 'no'}}
|
||||
if userData.loginDisabled
|
||||
td <s>{{ moment userData.createdAt 'LLL' }}</s>
|
||||
else
|
||||
td {{ moment userData.createdAt 'LLL' }}
|
||||
td
|
||||
if userData.loginDisabled
|
||||
| {{_ 'no'}}
|
||||
else
|
||||
| {{_ 'yes'}}
|
||||
if userData.loginDisabled
|
||||
td <s>{{_ userData.authenticationMethod }}</s>
|
||||
else
|
||||
td {{_ userData.authenticationMethod }}
|
||||
if userData.loginDisabled
|
||||
td <s>{{ userData.importUsernamesString }}</s>
|
||||
else
|
||||
td {{ userData.importUsernamesString }}
|
||||
if userData.loginDisabled
|
||||
td <s>{{ userData.orgsUserBelongs }}</s>
|
||||
else
|
||||
td {{ userData.orgsUserBelongs }}
|
||||
if userData.loginDisabled
|
||||
td <s>{{ userData.teamsUserBelongs }}</s>
|
||||
else
|
||||
td {{ userData.teamsUserBelongs }}
|
||||
td
|
||||
a.edit-user
|
||||
| ✏️
|
||||
@@ -448,8 +400,8 @@ template(name="editUserPopup")
|
||||
option(value="{{value}}") {{_ value}}
|
||||
label
|
||||
| {{_ 'organizations'}}
|
||||
| ➕#addUserOrg
|
||||
| ➖#removeUserOrg
|
||||
span#addUserOrg ➕
|
||||
span#removeUserOrg ➖
|
||||
select.js-orgs#jsOrgs
|
||||
option(value="-1") {{_ 'organizations'}} :
|
||||
each value in orgsDatas
|
||||
@@ -458,8 +410,8 @@ template(name="editUserPopup")
|
||||
input#jsUserOrgIdsInPut.js-userOrgIds.hide(type="hidden" value=user.orgIdsUserBelongs)
|
||||
label
|
||||
| {{_ 'teams'}}
|
||||
| ➕#addUserTeam
|
||||
| ➖#removeUserTeam
|
||||
span#addUserTeam ➕
|
||||
span#removeUserTeam ➖
|
||||
select.js-teams#jsTeams
|
||||
option(value="-1") {{_ 'teams'}} :
|
||||
each value in teamsDatas
|
||||
@@ -591,8 +543,8 @@ template(name="newUserPopup")
|
||||
option(value="{{value}}") {{_ value}}
|
||||
label
|
||||
| {{_ 'organizations'}}
|
||||
| ➕#addUserOrgNewUser
|
||||
| ➖#removeUserOrgNewUser
|
||||
span#addUserOrgNewUser ➕
|
||||
span#removeUserOrgNewUser ➖
|
||||
select.js-orgsNewUser#jsOrgsNewUser
|
||||
option(value="-1") {{_ 'organizations'}} :
|
||||
each value in orgsDatas
|
||||
@@ -601,8 +553,8 @@ template(name="newUserPopup")
|
||||
input#jsUserOrgIdsInPutNewUser.js-userOrgIdsNewUser.hide(type="text" value=user.orgIdsUserBelongs)
|
||||
label
|
||||
| {{_ 'teams'}}
|
||||
| ➕#addUserTeamNewUser
|
||||
| ➖#removeUserTeamNewUser
|
||||
span#addUserTeamNewUser ➕
|
||||
span#removeUserTeamNewUser ➖
|
||||
select.js-teamsNewUser#jsTeamsNewUser
|
||||
option(value="-1") {{_ 'teams'}} :
|
||||
each value in teamsDatas
|
||||
|
||||
@@ -172,6 +172,10 @@ BlazeComponent.extendComponent({
|
||||
// Show only inactive users (loginDisabled is true)
|
||||
query['loginDisabled'] = true;
|
||||
break;
|
||||
case 'admin':
|
||||
// Show only admin users (isAdmin is true)
|
||||
query['isAdmin'] = true;
|
||||
break;
|
||||
case 'all':
|
||||
default:
|
||||
// Show all users, no additional filter
|
||||
|
||||
@@ -218,6 +218,22 @@ window.Popup = new (class {
|
||||
const viewportHeight = $(window).height();
|
||||
const popupWidth = Math.min(380, viewportWidth * 0.55) + 15; // Add 15px for margin
|
||||
|
||||
// Check if this is an admin panel edit popup
|
||||
const isAdminEditPopup = $element.hasClass('edit-user') ||
|
||||
$element.hasClass('edit-org') ||
|
||||
$element.hasClass('edit-team');
|
||||
|
||||
if (isAdminEditPopup) {
|
||||
// Center the popup horizontally and use full height
|
||||
const centeredLeft = (viewportWidth - popupWidth) / 2;
|
||||
|
||||
return {
|
||||
left: Math.max(10, centeredLeft), // Ensure popup doesn't go off screen
|
||||
top: 10, // Start from top with small margin
|
||||
maxHeight: viewportHeight - 20, // Use full height minus small margins
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate available height for popup
|
||||
const popupTop = offset.top + $element.outerHeight();
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
|
||||
|
||||
Right click and download files 1-4:
|
||||
|
||||
1. [wekan-8.06-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.06/wekan-8.06-amd64-windows.zip)
|
||||
1. [wekan-8.09-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.09/wekan-8.09-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.06-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files:
|
||||
7. Unzip `wekan-8.09-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files:
|
||||
|
||||
```
|
||||
bundle (directory)
|
||||
|
||||
@@ -190,9 +190,9 @@
|
||||
"board-view-collapse": "Contraer",
|
||||
"board-view-gantt": "Gantt",
|
||||
"board-view-lists": "Listas",
|
||||
"bucket-example": "Like \"Bucket List\" for example",
|
||||
"calendar-previous-month-label": "Previous Month",
|
||||
"calendar-next-month-label": "Next Month",
|
||||
"bucket-example": "Mes anterior",
|
||||
"calendar-previous-month-label": "Mes anterior",
|
||||
"calendar-next-month-label": "Mes siguiente",
|
||||
"cancel": "Cancelar",
|
||||
"card-archived": "Se archivó esta tarjeta",
|
||||
"board-archived": "Se archivó este tablero",
|
||||
@@ -356,7 +356,7 @@
|
||||
"custom-field-text": "Texto",
|
||||
"custom-fields": "Campos personalizados",
|
||||
"date": "Fecha",
|
||||
"date-format": "Date Format",
|
||||
"date-format": "Formato de fecha",
|
||||
"date-format-yyyy-mm-dd": "YYYY-MM-DD HH:MM",
|
||||
"date-format-dd-mm-yyyy": "DD-MM-YYYY HH:MM",
|
||||
"date-format-mm-dd-yyyy": "MM-DD-YYYY HH:MM",
|
||||
|
||||
@@ -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 HH:MM",
|
||||
"date-format-dd-mm-yyyy": "DD-MM-YYYY HH:MM",
|
||||
"date-format-mm-dd-yyyy": "MM-DD-YYYY HH:MM",
|
||||
|
||||
@@ -190,9 +190,9 @@
|
||||
"board-view-collapse": "Expandir",
|
||||
"board-view-gantt": "Gantt",
|
||||
"board-view-lists": "Listas",
|
||||
"bucket-example": "Like \"Bucket List\" for example",
|
||||
"calendar-previous-month-label": "Previous Month",
|
||||
"calendar-next-month-label": "Next Month",
|
||||
"bucket-example": "Como \"Lista de Desejos\", por exemplo",
|
||||
"calendar-previous-month-label": "Mês Anterior",
|
||||
"calendar-next-month-label": "Próximo Mês",
|
||||
"cancel": "Cancelar",
|
||||
"card-archived": "Este cartão está Arquivado.",
|
||||
"board-archived": "Este quadro está Arquivado.",
|
||||
@@ -356,10 +356,10 @@
|
||||
"custom-field-text": "Texto",
|
||||
"custom-fields": "Campos customizados",
|
||||
"date": "Data",
|
||||
"date-format": "Date Format",
|
||||
"date-format-yyyy-mm-dd": "YYYY-MM-DD HH:MM",
|
||||
"date-format-dd-mm-yyyy": "DD-MM-YYYY HH:MM",
|
||||
"date-format-mm-dd-yyyy": "MM-DD-YYYY HH:MM",
|
||||
"date-format": "Formato da Data",
|
||||
"date-format-yyyy-mm-dd": "AAAA-MM-DD HH:MM",
|
||||
"date-format-dd-mm-yyyy": "DD-MM-AAAA HH:MM",
|
||||
"date-format-mm-dd-yyyy": "MM-DD-AAAA HH:MM",
|
||||
"decline": "Rejeitar",
|
||||
"default-avatar": "Avatar padrão",
|
||||
"delete": "Excluir",
|
||||
|
||||
@@ -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 HH:MM",
|
||||
"date-format-dd-mm-yyyy": "DD-MM-YYYY HH:MM",
|
||||
"date-format-mm-dd-yyyy": "MM-DD-YYYY HH:MM",
|
||||
|
||||
@@ -803,13 +803,6 @@ Boards.helpers({
|
||||
{
|
||||
boardId: this._id,
|
||||
archived: false,
|
||||
// Get lists for all swimlanes in this board, plus lists without swimlaneId for backward compatibility
|
||||
$or: [
|
||||
{ swimlaneId: { $in: this.swimlanes().map(s => s._id) } },
|
||||
{ swimlaneId: { $exists: false } },
|
||||
{ swimlaneId: '' },
|
||||
{ swimlaneId: null }
|
||||
],
|
||||
},
|
||||
{ sort: sortKey },
|
||||
);
|
||||
@@ -819,13 +812,6 @@ Boards.helpers({
|
||||
return ReactiveCache.getLists(
|
||||
{
|
||||
boardId: this._id,
|
||||
// Get lists for all swimlanes in this board, plus lists without swimlaneId for backward compatibility
|
||||
$or: [
|
||||
{ swimlaneId: { $in: this.swimlanes().map(s => s._id) } },
|
||||
{ swimlaneId: { $exists: false } },
|
||||
{ swimlaneId: '' },
|
||||
{ swimlaneId: null }
|
||||
]
|
||||
},
|
||||
{ sort: { sort: 1 } }
|
||||
);
|
||||
|
||||
+4
-16
@@ -211,33 +211,20 @@ Swimlanes.helpers({
|
||||
return this.draggableLists();
|
||||
},
|
||||
newestLists() {
|
||||
// sorted lists from newest to the oldest, by its creation date or its cards' last modification date
|
||||
// Include lists without swimlaneId for backward compatibility (they belong to default swimlane)
|
||||
// Revert to shared lists across swimlanes: filter by board only
|
||||
return ReactiveCache.getLists(
|
||||
{
|
||||
boardId: this.boardId,
|
||||
$or: [
|
||||
{ swimlaneId: this._id },
|
||||
{ swimlaneId: { $exists: false } },
|
||||
{ swimlaneId: '' },
|
||||
{ swimlaneId: null }
|
||||
],
|
||||
archived: false,
|
||||
},
|
||||
{ sort: { modifiedAt: -1 } },
|
||||
);
|
||||
},
|
||||
draggableLists() {
|
||||
// Include lists without swimlaneId for backward compatibility (they belong to default swimlane)
|
||||
// Revert to shared lists across swimlanes: filter by board only
|
||||
return ReactiveCache.getLists(
|
||||
{
|
||||
boardId: this.boardId,
|
||||
$or: [
|
||||
{ swimlaneId: this._id },
|
||||
{ swimlaneId: { $exists: false } },
|
||||
{ swimlaneId: '' },
|
||||
{ swimlaneId: null }
|
||||
],
|
||||
//archived: false,
|
||||
},
|
||||
{ sort: ['sort'] },
|
||||
@@ -245,7 +232,8 @@ Swimlanes.helpers({
|
||||
},
|
||||
|
||||
myLists() {
|
||||
return ReactiveCache.getLists({ swimlaneId: this._id });
|
||||
// Revert to shared lists: provide lists by board for this swimlane's board
|
||||
return ReactiveCache.getLists({ boardId: this.boardId });
|
||||
},
|
||||
|
||||
allCards() {
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wekan",
|
||||
"version": "v8.06.0",
|
||||
"version": "v8.09.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wekan",
|
||||
"version": "v8.06.0",
|
||||
"version": "v8.09.0",
|
||||
"description": "Open-Source kanban",
|
||||
"private": true,
|
||||
"repository": {
|
||||
|
||||
@@ -12,4 +12,12 @@ if [ $# -ne 2 ]
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed -i 's|$1|$2|g' package.json package-lock.json snapcraft.yaml docs/Platforms/Propietary/Windows/Offline.md Dockerfile Stackerfile.yml sandstorm-pkgdef.capnp
|
||||
# Remove dots from version numbers for sandstorm-pkgdef.capnp
|
||||
OLD_VERSION_NO_DOTS=$(echo "$1" | tr -d '.')
|
||||
NEW_VERSION_NO_DOTS=$(echo "$2" | tr -d '.')
|
||||
|
||||
# Update all files except sandstorm-pkgdef.capnp with regular version format
|
||||
sed -i "s|$1|$2|g" package.json package-lock.json snapcraft.yaml docs/Platforms/Propietary/Windows/Offline.md Dockerfile Stackerfile.yml
|
||||
|
||||
# Update sandstorm-pkgdef.capnp with version format without dots
|
||||
sed -i "s|$OLD_VERSION_NO_DOTS|$NEW_VERSION_NO_DOTS|g" 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 = 806,
|
||||
appVersion = 809,
|
||||
# Increment this for every release.
|
||||
|
||||
appMarketingVersion = (defaultText = "8.06.0~2025-10-20"),
|
||||
appMarketingVersion = (defaultText = "8.09.0~2025-10-21"),
|
||||
# Human-readable presentation of the app version.
|
||||
|
||||
minUpgradableAppVersion = 0,
|
||||
|
||||
@@ -40,6 +40,9 @@ if (errors.length > 0) {
|
||||
// Import cron job storage for persistent job tracking
|
||||
import './cronJobStorage';
|
||||
|
||||
// Import migrations
|
||||
import './migrations/fixMissingListsMigration';
|
||||
|
||||
// Note: Automatic migrations are disabled - migrations only run when opening boards
|
||||
// import './boardMigrationDetector';
|
||||
// import './cronMigrationManager';
|
||||
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* Fix Missing Lists Migration
|
||||
*
|
||||
* This migration fixes the issue where cards have incorrect listId references
|
||||
* due to the per-swimlane lists change. It detects cards with mismatched
|
||||
* listId/swimlaneId and creates the missing lists.
|
||||
*
|
||||
* Issue: When upgrading from v7.94 to v8.02, cards that were in different
|
||||
* swimlanes but shared the same list now have wrong listId references.
|
||||
*
|
||||
* Example:
|
||||
* - Card1: listId: 'HB93dWNnY5bgYdtxc', swimlaneId: 'sK69SseWkh3tMbJvg'
|
||||
* - Card2: listId: 'HB93dWNnY5bgYdtxc', swimlaneId: 'XeecF9nZxGph4zcT4'
|
||||
*
|
||||
* Card2 should have a different listId that corresponds to its swimlane.
|
||||
*/
|
||||
|
||||
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';
|
||||
|
||||
class FixMissingListsMigration {
|
||||
constructor() {
|
||||
this.name = 'fix-missing-lists';
|
||||
this.version = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.fixMissingListsCompleted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if there are cards with mismatched listId/swimlaneId
|
||||
const cards = ReactiveCache.getCards({ boardId });
|
||||
const lists = ReactiveCache.getLists({ boardId });
|
||||
|
||||
// Create a map of listId -> swimlaneId for existing lists
|
||||
const listSwimlaneMap = new Map();
|
||||
lists.forEach(list => {
|
||||
listSwimlaneMap.set(list._id, list.swimlaneId || '');
|
||||
});
|
||||
|
||||
// Check for cards with mismatched listId/swimlaneId
|
||||
for (const card of cards) {
|
||||
const expectedSwimlaneId = listSwimlaneMap.get(card.listId);
|
||||
if (expectedSwimlaneId && expectedSwimlaneId !== card.swimlaneId) {
|
||||
console.log(`Found mismatched card: ${card._id}, listId: ${card.listId}, card swimlaneId: ${card.swimlaneId}, list swimlaneId: ${expectedSwimlaneId}`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error checking if migration is needed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the migration for a board
|
||||
*/
|
||||
async executeMigration(boardId) {
|
||||
try {
|
||||
console.log(`Starting fix missing lists migration for board ${boardId}`);
|
||||
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) {
|
||||
throw new Error(`Board ${boardId} not found`);
|
||||
}
|
||||
|
||||
const cards = ReactiveCache.getCards({ boardId });
|
||||
const lists = ReactiveCache.getLists({ boardId });
|
||||
const swimlanes = ReactiveCache.getSwimlanes({ boardId });
|
||||
|
||||
// Create maps for efficient lookup
|
||||
const listSwimlaneMap = new Map();
|
||||
const swimlaneListsMap = new Map();
|
||||
|
||||
lists.forEach(list => {
|
||||
listSwimlaneMap.set(list._id, list.swimlaneId || '');
|
||||
if (!swimlaneListsMap.has(list.swimlaneId || '')) {
|
||||
swimlaneListsMap.set(list.swimlaneId || '', []);
|
||||
}
|
||||
swimlaneListsMap.get(list.swimlaneId || '').push(list);
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
let createdLists = 0;
|
||||
let updatedCards = 0;
|
||||
|
||||
// Process each swimlane
|
||||
for (const [swimlaneId, swimlaneCards] of cardsBySwimlane) {
|
||||
if (!swimlaneId) continue;
|
||||
|
||||
// Get existing lists for this swimlane
|
||||
const existingLists = swimlaneListsMap.get(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
|
||||
const listSwimlaneId = listSwimlaneMap.get(listId);
|
||||
if (listSwimlaneId === swimlaneId) {
|
||||
// List is already correctly assigned to this swimlane
|
||||
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 };
|
||||
createdLists++;
|
||||
|
||||
console.log(`Created new list "${originalList.title}" for swimlane ${swimlaneId}`);
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
});
|
||||
updatedCards++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark board as processed
|
||||
Boards.update(boardId, {
|
||||
$set: {
|
||||
fixMissingListsCompleted: true,
|
||||
fixMissingListsCompletedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Fix missing lists migration completed for board ${boardId}: created ${createdLists} lists, updated ${updatedCards} cards`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
createdLists,
|
||||
updatedCards
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error executing fix missing lists migration for board ${boardId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get migration status for a board
|
||||
*/
|
||||
getMigrationStatus(boardId) {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) {
|
||||
return { status: 'board_not_found' };
|
||||
}
|
||||
|
||||
if (board.fixMissingListsCompleted) {
|
||||
return {
|
||||
status: 'completed',
|
||||
completedAt: board.fixMissingListsCompletedAt
|
||||
};
|
||||
}
|
||||
|
||||
const needsMigration = this.needsMigration(boardId);
|
||||
return {
|
||||
status: needsMigration ? 'needed' : 'not_needed'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error getting migration status:', error);
|
||||
return { status: 'error', error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const fixMissingListsMigration = new FixMissingListsMigration();
|
||||
|
||||
// Meteor methods
|
||||
Meteor.methods({
|
||||
'fixMissingListsMigration.check'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
return fixMissingListsMigration.getMigrationStatus(boardId);
|
||||
},
|
||||
|
||||
'fixMissingListsMigration.execute'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
return fixMissingListsMigration.executeMigration(boardId);
|
||||
},
|
||||
|
||||
'fixMissingListsMigration.needsMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
return fixMissingListsMigration.needsMigration(boardId);
|
||||
}
|
||||
});
|
||||
@@ -43,7 +43,7 @@ fi
|
||||
export MONGO_LOG_DESTINATION="snapcommon"
|
||||
|
||||
# Set MongoDB data directory
|
||||
export MONGO_DATA_DIR="${SNAP_COMMON}/wekan"
|
||||
export MONGO_DATA_DIR="${SNAP_COMMON}"
|
||||
|
||||
# Create MongoDB data directory if it doesn't exist
|
||||
if [ ! -d "$MONGO_DATA_DIR" ]; then
|
||||
@@ -130,4 +130,4 @@ echo $$ > "${SNAP_COMMON}/mongodb.pid"
|
||||
exec /snap/${SNAP_NAME}/current/bin/mongod \
|
||||
--dbpath="$MONGO_DATA_DIR" \
|
||||
--logpath="$MONGO_LOG_FILE" \
|
||||
--logappend $BIND_OPTIONS
|
||||
--logappend $BIND_OPTIONS
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
name: wekan
|
||||
version: '8.06'
|
||||
version: '8.09'
|
||||
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.06/wekan-8.06-amd64.zip
|
||||
unzip wekan-8.06-amd64.zip
|
||||
rm wekan-8.06-amd64.zip
|
||||
wget https://github.com/wekan/wekan/releases/download/v8.09/wekan-8.09-amd64.zip
|
||||
unzip wekan-8.09-amd64.zip
|
||||
rm wekan-8.09-amd64.zip
|
||||
cd ..
|
||||
##cd .build/bundle
|
||||
##find . -type d -name '*-garbage*' | xargs rm -rf
|
||||
|
||||
Reference in new issue
Block a user