Compare commits

...

199 commits
v8.05 ... main

Author SHA1 Message Date
Lauri Ojansivu
614cb44b55 Updated translations.
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
2025-12-16 21:56:51 +02:00
Lauri Ojansivu
0ce8e8b74d
Merge pull request #6043 from wekan/dependabot/github_actions/actions/cache-5
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
Bump actions/cache from 4 to 5
2025-12-16 05:47:09 +02:00
Lauri Ojansivu
4ea53af76e
Merge pull request #6042 from wekan/dependabot/github_actions/actions/download-artifact-7
Bump actions/download-artifact from 6 to 7
2025-12-16 05:46:50 +02:00
Lauri Ojansivu
016f17d663
Merge pull request #6041 from wekan/dependabot/github_actions/actions/upload-artifact-6
Bump actions/upload-artifact from 5 to 6
2025-12-16 05:46:26 +02:00
dependabot[bot]
07f69950a7
Bump actions/cache from 4 to 5
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 20:13:04 +00:00
dependabot[bot]
cec625607d
Bump actions/download-artifact from 6 to 7
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 20:13:00 +00:00
dependabot[bot]
a290c7b34b
Bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 20:12:55 +00:00
Lauri Ojansivu
5b77ac1b44 Updated translations
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
2025-12-11 03:33:17 +02:00
Lauri Ojansivu
41c635afb5
Merge pull request #6029 from MialLewis/add_archive_card_to_api
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
Add archive card to api
2025-12-04 11:58:44 +02:00
Lauri Ojansivu
adbf729cb2
Merge pull request #6032 from wekan/dependabot/github_actions/docker/metadata-action-5.10.0
Bump docker/metadata-action from 5.9.0 to 5.10.0
2025-12-04 11:58:02 +02:00
dependabot[bot]
88ea716d63
Bump docker/metadata-action from 5.9.0 to 5.10.0
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.9.0 to 5.10.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](318604b99e...c299e40c65)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: 5.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 23:35:03 +00:00
Mial Lewis
003a07ebce change restore to unarchive 2025-11-27 22:00:43 +00:00
Mial Lewis
d3c237bc66 fix more indenting 2025-11-27 08:29:36 +00:00
Mial Lewis
bac0fa81fc correce indent 2025-11-27 08:27:38 +00:00
Mial Lewis
a42915614a add restore to wekan.yml 2025-11-27 08:25:59 +00:00
Mial Lewis
5ff9bf331f add restore to api 2025-11-27 08:23:56 +00:00
Mial Lewis
36d7b0f8a7 correct return values 2025-11-27 00:52:28 +00:00
Mial Lewis
67c8a98f20 add route to wekan.yml 2025-11-27 00:05:53 +00:00
Mial Lewis
a81a603031 update bool to boolean 2025-11-26 23:59:00 +00:00
Mial Lewis
e30ce78053 add archive card to api 2025-11-26 23:57:49 +00:00
Lauri Ojansivu
3d70de94c6
Merge pull request #6028 from wekan/dependabot/github_actions/actions/checkout-6
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
Bump actions/checkout from 5 to 6
2025-11-26 18:04:09 +02:00
dependabot[bot]
70975c2944
Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 20:03:19 +00:00
Lauri Ojansivu
960e2126b4 Updated ChangeLog.
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
2025-11-21 03:02:41 +02:00
Lauri Ojansivu
3db1305e58 Updated build script for Linux arm64 bundle.
Thanks to xet7 !
2025-11-21 02:44:50 +02:00
Lauri Ojansivu
f16780b5e3 Updated translations.
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
2025-11-19 09:34:57 +02:00
Lauri Ojansivu
37a3065f3c Updated translations.
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
2025-11-15 16:35:31 +02:00
Lauri Ojansivu
7ff1649d89 Updated security.md
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
2025-11-14 07:47:31 +02:00
Lauri Ojansivu
a39ae31b45
Merge pull request #6012 from wekan/dependabot/github_actions/docker/metadata-action-5.9.0
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
2025-11-13 17:19:00 +02:00
dependabot[bot]
6302a48221
Bump docker/metadata-action from 5.8.0 to 5.9.0
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.8.0 to 5.9.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](c1e51972af...318604b99e)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: 5.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 20:02:19 +00:00
Lauri Ojansivu
c277bee9d2
Merge pull request #6009 from brlin-tw/patch-issue-6008
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
Fix Broken Strikethroughs in Markdown to HTML conversion.
2025-11-10 04:58:29 +02:00
Buo-ren Lin (OSSII)
c5f5ce126d
Fix Broken Strikethroughs in Markdown to HTML conversion.
Allow the s tag to be rendered.

Fixes #6008.

Signed-off-by: Buo-ren Lin (OSSII) <buoren.lin@ossii.com.tw>
2025-11-10 10:49:26 +08:00
Lauri Ojansivu
0004ae716b v8.17
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
2025-11-06 04:00:04 +02:00
Lauri Ojansivu
7f53dfac3c Updated ChangeLog. 2025-11-06 03:33:46 +02:00
Lauri Ojansivu
18003900c2 Fix Worker Permissions does not allow for cards to be moved. - v8.15.
Thanks to xet7 !

Fixes #5990
2025-11-06 03:31:14 +02:00
Lauri Ojansivu
fe104791b5 Updated ChangeLog. 2025-11-06 03:08:51 +02:00
Lauri Ojansivu
6244657ca5 Fix Workspaces at All Boards to have correct count of remaining etc, while starred also at Starred/Favorites.
Thanks to xet7 !
2025-11-06 03:06:16 +02:00
Lauri Ojansivu
46866dac85 Updated ChangeLog. 2025-11-06 02:46:52 +02:00
Lauri Ojansivu
c829c073cf Remove not working Bookmark menu option.
Thanks to xet7 !
2025-11-06 02:44:30 +02:00
Lauri Ojansivu
0772ca4036 Updated ChangeLog. 2025-11-06 02:36:10 +02:00
Lauri Ojansivu
581733d605 Fix Regression - Show calendar popup at set due date.
Thanks to xet7 !

Fixes #5978
2025-11-06 02:32:34 +02:00
Lauri Ojansivu
b02af27ac3 Updated ChangeLog. 2025-11-06 01:06:19 +02:00
Lauri Ojansivu
20af0a2ef5 Try to fix Edit Custom Fields button not working. Removed duplicate option from Boards Settings.
Thanks to xet7 !

Fixes #5988
2025-11-06 01:04:20 +02:00
Lauri Ojansivu
c58ab5b07d Updated ChangeLog. 2025-11-06 00:37:42 +02:00
Lauri Ojansivu
e5e711c938 Fix Card emoji issues.
Thanks to xet7 !

Fixes #5995
2025-11-06 00:35:49 +02:00
Lauri Ojansivu
42594abe4e Updated ChangeLog. 2025-11-06 00:30:08 +02:00
Lauri Ojansivu
0afbdc95b4 Feature: Workspaces, at All Boards page.
Thanks to xet7 !
2025-11-06 00:26:35 +02:00
Lauri Ojansivu
16a74bb748 Updated ChangeLog.
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
2025-11-05 20:51:44 +02:00
Lauri Ojansivu
8711b476be Fix star board.
Thanks to xet7 !
2025-11-05 20:50:28 +02:00
Lauri Ojansivu
df9fba4765 Updated translations. 2025-11-05 20:26:29 +02:00
Lauri Ojansivu
7d27139aa9 Updated ChangeLog. 2025-11-05 20:25:07 +02:00
Lauri Ojansivu
e4638d5fbc Fixed sidebar migrations to be per-board, not global. Clarified translations.
Thanks to xet7 !
2025-11-05 20:22:56 +02:00
Lauri Ojansivu
bc5854dd29 Updated ChangeLog. 2025-11-05 19:04:47 +02:00
Lauri Ojansivu
ba49d4d140 Remove old translations and code not in use anymore.
Thanks to xet7 !
2025-11-05 19:03:21 +02:00
Lauri Ojansivu
71b7dcffb5 Updated ChangeLog. 2025-11-05 18:46:56 +02:00
Lauri Ojansivu
7713e613b4 Fix 8.16 Lists with no items are deleted every time when board is opened. Moved migrations to right sidebar.
Thanks to xet7 !

Fixes #5994
2025-11-05 18:44:48 +02:00
Lauri Ojansivu
91a0aa7387 Updated ChangeLog. 2025-11-05 17:08:52 +02:00
Lauri Ojansivu
fbd6b920ef Updated ChangeLog. 2025-11-05 17:08:10 +02:00
Lauri Ojansivu
1b25d1d572 Moved migrations from opening board to right sidebar / Migrations.
Thanks to xet7 !
2025-11-05 17:06:26 +02:00
Lauri Ojansivu
e93e72234c Updated ChangeLog. 2025-11-05 16:38:10 +02:00
Lauri Ojansivu
15d9b0ae3a Updated ChangeLog. 2025-11-05 16:38:03 +02:00
Lauri Ojansivu
550d87ac6c Fix 8.16: Switching Board View fails with 403 error.
Thanks to xet7 !
2025-11-05 16:35:29 +02:00
Lauri Ojansivu
f8e576e890 Try to fix Snap.
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
Thanks to xet7 !
2025-11-02 22:23:16 +02:00
Lauri Ojansivu
fb8ef4d978 Try to fix Snap.
Thanks to xet7 !
2025-11-02 21:36:17 +02:00
Lauri Ojansivu
5127e87898 Try to fix Snap.
Thanks to xet7 !
2025-11-02 21:33:06 +02:00
Lauri Ojansivu
3f2d4444e4 Try to fix Snap. Part 2.
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
Thanks to xet7 !
2025-11-02 16:14:45 +02:00
Lauri Ojansivu
9c7badb0eb Merge branch 'main' of github.com:wekan/wekan 2025-11-02 16:04:16 +02:00
Lauri Ojansivu
9d9f77a731 Try to fix Snap.
Thanks to xet7 !
2025-11-02 16:02:53 +02:00
Lauri Ojansivu
c400ce74b1 v8.16 2025-11-02 12:09:27 +02:00
Lauri Ojansivu
c2e20ee4a3 Updated ChangeLog.
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
2025-11-02 11:43:33 +02:00
Lauri Ojansivu
ccd9034339 Fix SECURITY ISSUE 5: Attachment API uses bearer value as userId and DoS (Low).
Thanks to Siam Thanat Hack (STH) and xet7 !
2025-11-02 11:42:07 +02:00
Lauri Ojansivu
0a1a075f31 Fix SECURITY ISSUE 4: Members can forge others’ votes (Low). Bonus: Similar fixes to planning poker too done by xet7.
Thanks to Siam Thanat Hack (STH) and xet7 !
2025-11-02 11:12:41 +02:00
Lauri Ojansivu
4aaeec9515 Updated ChangeLog. 2025-11-02 10:17:33 +02:00
Lauri Ojansivu
ea310d7508 Fix SECURITY ISSUE 3: Unauthenticated (or any) user can update board sort.
Thanks to Siam Thanat Hack (STH) !
2025-11-02 10:13:45 +02:00
Lauri Ojansivu
0a2e6a0c38 Updated ChangeLog. 2025-11-02 09:20:28 +02:00
Lauri Ojansivu
f26d582018 Fix SECURITY ISSUE 2: Access to boards of any Orgs/Teams, and avatar permissions.
Thanks to Siam Thanat Hack (STH) !
2025-11-02 09:11:50 +02:00
Lauri Ojansivu
e9a727301d Fix SECURITY ISSUE 1: File Attachments enables stored XSS (High).
Thanks to Siam Thanat Hack (STH) !
2025-11-02 08:36:29 +02:00
Lauri Ojansivu
d64d2f9c42 Updated translations. 2025-11-02 07:30:24 +02:00
Lauri Ojansivu
5c0d122e84 Updated funding 2025-11-02 06:15:08 +02:00
Lauri Ojansivu
5079c853a7 Updated translations.
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
2025-10-29 02:58:00 +02:00
Lauri Ojansivu
b039ba12a2
Merge pull request #5984 from wekan/dependabot/github_actions/actions/download-artifact-6
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
Bump actions/download-artifact from 5 to 6
2025-10-28 06:00:19 +02:00
Lauri Ojansivu
3323ac6ac1
Merge pull request #5983 from wekan/dependabot/github_actions/actions/upload-artifact-5
Bump actions/upload-artifact from 4 to 5
2025-10-28 05:59:59 +02:00
dependabot[bot]
3204311ac1
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] <support@github.com>
2025-10-27 23:12:09 +00:00
dependabot[bot]
0fc2ad97cd
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] <support@github.com>
2025-10-27 20:32:10 +00:00
Lauri Ojansivu
30620d0ca4 Some migrations and mobile fixes.
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
Thanks to xet7 !
2025-10-25 21:09:07 +03:00
Lauri Ojansivu
bccc22c5fe Updated ChangeLog. 2025-10-25 19:25:04 +03:00
Lauri Ojansivu
ecf2418347 Fix changing swimlane color to not reload webpage.
Thanks to xet7 !
2025-10-25 19:23:35 +03:00
Lauri Ojansivu
0c99cb3103 Updated ChangeLog. 2025-10-25 19:19:35 +03:00
Lauri Ojansivu
034dc08269 Disabled migrations that happen when opening board. Defaulting to per-swimlane lists and drag drop list to same or different swimlane.
Thanks to xet7 !
2025-10-25 19:17:09 +03:00
Lauri Ojansivu
d1a51b42f6 Updated translations.
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
2025-10-24 18:43:21 +03:00
Lauri Ojansivu
92bfbb2d0c Updated ChangeLog.
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
2025-10-23 05:54:25 +03:00
Lauri Ojansivu
91b846e2cd List menu / More / Delete duplicate lists that do not have any cards.
Thanks to xet7 !
2025-10-23 05:50:43 +03:00
Lauri Ojansivu
7fe7fb4c15 v8.15 2025-10-23 04:41:34 +03:00
Lauri Ojansivu
0cebd8aa4d Fix drag lists did not work. Part 2.
Thanks to xet7 !
2025-10-23 04:35:33 +03:00
Lauri Ojansivu
8662c96d1c Fix drag lists did not work.
Thanks to xet7 !
2025-10-23 04:33:34 +03:00
Lauri Ojansivu
0cbc9402f3 v8.14 2025-10-23 04:09:14 +03:00
Lauri Ojansivu
940df02456 Updated translations. 2025-10-23 04:08:49 +03:00
Lauri Ojansivu
b4b598f542 Fix board reloading page every second.
Thanks to xet7 !
2025-10-23 04:03:52 +03:00
Lauri Ojansivu
ef19c35b5a v8.12 2025-10-23 03:29:23 +03:00
Lauri Ojansivu
fc98120269 Updated translations. 2025-10-23 03:24:28 +03:00
Lauri Ojansivu
b8a3d6deaf Updated ChangeLog. 2025-10-23 03:17:13 +03:00
Lauri Ojansivu
45537ede87 Fix UI issues of Right Sidebar / Subtasks Settings and Card Settings.
Thanks to xet7 !

Fixes #5971
2025-10-23 03:15:26 +03:00
Lauri Ojansivu
29a9c5bc7b Updated ChangeLog. 2025-10-23 01:02:15 +03:00
Lauri Ojansivu
7ca81285b1 Fix opened card Date Format to be used at dates popups.
Thanks to xet7 !

Related #5971
2025-10-23 01:00:11 +03:00
Lauri Ojansivu
49a865cdbf Updated ChangeLog. 2025-10-23 00:48:31 +03:00
Lauri Ojansivu
a0c30c35ed Removed not needed | at left side of minicard badges.
Thanks to xet7 !
2025-10-23 00:47:18 +03:00
Lauri Ojansivu
de20424885 Updated translations. 2025-10-23 00:38:34 +03:00
Lauri Ojansivu
f7e09ae89c Updated ChangeLog. 2025-10-23 00:36:17 +03:00
Lauri Ojansivu
c6d4600683 Fix unable to add members to board.
Fixes #5972
2025-10-23 00:34:19 +03:00
Lauri Ojansivu
bd1837ee36 Updated ChangeLog. 2025-10-23 00:16:27 +03:00
Lauri Ojansivu
544b24ceb1 Fix Regression - unable to rearrange tasks within a checklist - v8.11.
Thanks to xet7 !

Fixes #5973
2025-10-23 00:14:30 +03:00
Lauri Ojansivu
0825374183 Updated translations.
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
2025-10-22 23:56:23 +03:00
Lauri Ojansivu
b053fb8e61 Updated ChangeLog. 2025-10-22 23:33:38 +03:00
Lauri Ojansivu
ae11e80bde Fix Regression - unable to view cards by due date v8.11.
Thanks to xet7 !

Fixes #5964
2025-10-22 23:31:36 +03:00
Lauri Ojansivu
8e296231ba Updated translations. 2025-10-22 22:59:35 +03:00
Lauri Ojansivu
49891eff36 Updated ChangeLog.
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
2025-10-21 15:34:07 +03:00
Lauri Ojansivu
58df525b49 Fix duplicated lists and do not show debug messages when env DEBUG is not true. Part 3.
Thanks to xet7 !

Fixes #5952
2025-10-21 15:31:34 +03:00
Lauri Ojansivu
1761f43afa Merge newest changes.
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
2025-10-21 15:22:55 +03:00
Lauri Ojansivu
37d7d938c5 Updated ChangeLog. 2025-10-21 15:21:25 +03:00
Lauri Ojansivu
b7ca2310b2 Fix duplicated lists.
Thanks to xet7 !

Fixes #5952
2025-10-21 15:19:19 +03:00
Lauri Ojansivu
c562b3969a v8.11 2025-10-21 15:17:53 +03:00
Lauri Ojansivu
d1d553e8d7 Updated ChangeLog. 2025-10-21 15:15:15 +03:00
Lauri Ojansivu
b6e7b258e0 Fix duplicated lists.
Thanks to xet7 !

Fixes #5952
2025-10-21 15:14:01 +03:00
Lauri Ojansivu
c7bbe47221 Updated ChangeLog. 2025-10-21 15:10:07 +03:00
Lauri Ojansivu
347fa9e5cd Fix Regression - due date taking a while to load all cards v8.06.
Thanks to xet7 !

Fixes #5955
2025-10-21 15:08:50 +03:00
Lauri Ojansivu
07ce151508 Updated ChangeLog. 2025-10-21 15:04:01 +03:00
Lauri Ojansivu
665c9b5e52 Verify that due background colors are correct also at My Due Cards.
Thanks to xet7 !
2025-10-21 15:02:39 +03:00
Lauri Ojansivu
9399a0c545 Updated ChangeLog. 2025-10-21 14:59:48 +03:00
Lauri Ojansivu
a540b12895 Fix My Due Cards to be sorted by due date, oldest first.
Thanks to xet7 !

Fixes #5956
2025-10-21 14:57:57 +03:00
Lauri Ojansivu
e29d9dcd17 Updated ChangeLog. 2025-10-21 14:49:59 +03:00
Lauri Ojansivu
1aa0d84977 Fix due dates to use colors: red = overdue, amber = due soon, no shade = not due yet.
Thanks to xet7 !

Fixes #5963
2025-10-21 14:47:57 +03:00
Lauri Ojansivu
7f31d7c812 v8.10 2025-10-21 14:15:16 +03:00
Lauri Ojansivu
4987a95d8e Prevent opened board re-migrating and reloading every 5 seconds.
Thanks to xet7 !
2025-10-21 14:12:12 +03:00
Lauri Ojansivu
ef7771febb v8.09 2025-10-21 13:54:37 +03:00
Lauri Ojansivu
12cba0e148 Updated ChangeLog. 2025-10-21 13:36:43 +03:00
Lauri Ojansivu
c3a4052227 Fix upgrade to 8.08 duplicates lists.
Thanks to xet7 !

Fixes #5962,
fixes #5952
2025-10-21 13:34:39 +03:00
Lauri Ojansivu
82f048ccef Updated ChangeLog. 2025-10-21 13:26:41 +03:00
Lauri Ojansivu
7a585a3dfb Fix Admin Panel / People editing and layout.
Thanks to xet7 !

Fixes #5961
2025-10-21 13:22:58 +03:00
Lauri Ojansivu
8d3b53f51d v8.08 2025-10-21 11:01:40 +03:00
Lauri Ojansivu
d73e006935 Updated ChangeLog. 2025-10-21 10:48:45 +03:00
Lauri Ojansivu
9536e60bd1 Fix opening board migration of Shared Lists to Per-Swimlane lists to use ReactiveCache correctly without errors.
Thanks to xet7 !

Fixes #5960
2025-10-21 10:46:37 +03:00
Lauri Ojansivu
678ca978a3 Merge branch 'main' of github.com:wekan/wekan
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
2025-10-20 19:13:10 +03:00
Lauri Ojansivu
39420877fd Updated ChangeLog. 2025-10-20 19:12:07 +03:00
Lauri Ojansivu
6ea03cfba3 Revert moving mongodb raw database files.
Thanks to xet7 !
2025-10-20 18:08:52 +03:00
Lauri Ojansivu
9214b56aea v8.07 2025-10-20 17:43:18 +03:00
Lauri Ojansivu
699b4c464f Updated translations. 2025-10-20 17:30:42 +03:00
Lauri Ojansivu
9fa54a3148 Updated ChangeLog. 2025-10-20 17:29:29 +03:00
Lauri Ojansivu
f2019b1059 If Snap Candidate MongoDB raw database files were at SNAP_COMMON/wekan, migrate them back to SNAP_COMMON.
Thanks to xet7 !
2025-10-20 17:26:53 +03:00
Lauri Ojansivu
714bbd0fb0 Updated ChangeLog. 2025-10-20 17:10:30 +03:00
Lauri Ojansivu
80777b4663 When opening board, add missing lists.
Thanks to xet7 !

Fixes #5926
2025-10-20 17:06:42 +03:00
Lauri Ojansivu
9473c1fe41 Updated ChangeLog. 2025-10-20 16:50:35 +03:00
Lauri Ojansivu
98f141d62f 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.
Thanks to xet7 !
2025-10-20 16:42:28 +03:00
Lauri Ojansivu
85dd213b14 Updated translations.
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
2025-10-20 06:21:02 +03:00
Lauri Ojansivu
3cf00911f7 v8.06 2025-10-20 03:29:24 +03:00
Lauri Ojansivu
bddaad8346 Updated ChangeLog. 2025-10-20 03:13:32 +03:00
Lauri Ojansivu
5df4efd7ba Have all iPhone use mobile view by default, while still having possibility to use mobile/desktop switch button for desktop mode.
Thanks to xet7 !
2025-10-20 03:11:02 +03:00
Lauri Ojansivu
59df6aad05 Updated ChangeLog. 2025-10-20 03:00:21 +03:00
Lauri Ojansivu
c4af4d03ac Some mobile view fixes.
Thanks to xet7 !
2025-10-20 02:58:30 +03:00
Lauri Ojansivu
62679819d9 Updated ChangeLog. 2025-10-20 02:32:08 +03:00
Lauri Ojansivu
46d46e313c Fix Bug Member settings drops to the second line and overlaps when many boards are starred as favourites.
Thanks to xet7 !

Fixes #5943
2025-10-20 02:30:03 +03:00
Lauri Ojansivu
27e9d3ce47 Updated ChangeLog. 2025-10-20 01:59:21 +03:00
Lauri Ojansivu
b6b0c5fe6d Fix Bug: Scale of Minicard icons is linked to horizontal screensize.
Thanks to xet7 !

Fixes #5947
2025-10-20 01:56:54 +03:00
Lauri Ojansivu
87b934a955 Updated translations. 2025-10-20 01:52:37 +03:00
Lauri Ojansivu
8cc6e9b812 Updated translations. 2025-10-20 01:45:00 +03:00
Lauri Ojansivu
b7da17ff31 Updated ChangeLog. 2025-10-20 01:39:29 +03:00
Lauri Ojansivu
2dd3916f7e Added Date Format setting to Opened Card.
Thanks to xet7 !

Fixes #2011,
fixes #1176
2025-10-20 01:36:44 +03:00
Lauri Ojansivu
516552cce6 Updated ChangeLog. 2025-10-20 01:23:43 +03:00
Lauri Ojansivu
2d44881619 Fix card popup to use HTML date, not anymore JQuery date.
Thanks to xet7 !
2025-10-20 01:21:54 +03:00
Lauri Ojansivu
0acbf30b03 Fix migrations.
Thanks to xet7 !
2025-10-20 01:20:28 +03:00
Lauri Ojansivu
e61f6b1c89 Updated ChangeLog. 2025-10-20 01:03:34 +03:00
Lauri Ojansivu
973a49526f Fix Broken Hyperlinks in Markdown to HTML conversion.
Thanks to xet7 !

Fixes #5932
2025-10-20 01:01:55 +03:00
Lauri Ojansivu
e1902d58c1 Updated ChangeLog. 2025-10-20 00:35:33 +03:00
Lauri Ojansivu
1e53125499 Fix opened card attachments button text to be at tooltip, not at opened card.
Thanks to xet7 !
2025-10-20 00:33:02 +03:00
Lauri Ojansivu
91fb7d9e70 Updated ChangeLog. 2025-10-20 00:29:56 +03:00
Lauri Ojansivu
eb6b42c4c9 Fix syntax error at migrations.
Thanks to xet7 !
2025-10-20 00:28:19 +03:00
Lauri Ojansivu
679d210667 Updated ChangeLog. 2025-10-20 00:24:46 +03:00
Lauri Ojansivu
1e6252de7f When opening board, migrate from Shared Lists to Per-Swimlane Lists.
Thanks to xet7 !

Fixes #5952
2025-10-20 00:22:26 +03:00
Lauri Ojansivu
48b645ee1e Updated ChangeLog. 2025-10-19 23:48:15 +03:00
Lauri Ojansivu
951d2e4937 Legacy Lists button at one board view to restore missing lists/cards.
Thanks to xet7 !

Fixes #5952
2025-10-19 23:40:02 +03:00
Lauri Ojansivu
1658883b78 Updated ChangeLog. 2025-10-19 23:19:54 +03:00
Lauri Ojansivu
3514335247 At Public Board, drag resize list width and swimlane height. For logged in users, fix adding labels.
Thanks to xet7 !

Fixes #5922
2025-10-19 23:15:55 +03:00
Lauri Ojansivu
55bec31a3f Fix drag drop lists. Part 2.
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
Thanks to xet7 !

Fixes #5951
2025-10-19 22:22:21 +03:00
Lauri Ojansivu
0d36abee4e Updated ChangeLog. 2025-10-19 21:47:51 +03:00
Lauri Ojansivu
caa6e615ff Removed extra pipe characters.
Thanks to xet7 !
2025-10-19 21:46:14 +03:00
Lauri Ojansivu
cd3576b995 Updated ChangeLog. 2025-10-19 21:41:44 +03:00
Lauri Ojansivu
324f3f7794 Fix drag drop lists. Part 1.
Thanks to xet7 !

Related #5951
2025-10-19 21:38:55 +03:00
Lauri Ojansivu
3257110673 Updated ChangeLog. 2025-10-19 20:07:39 +03:00
Lauri Ojansivu
66b444e2b0 Fix unable to see My Due Cards.
Thanks to xet7 !

Fixes #5948
2025-10-19 20:05:36 +03:00
Lauri Ojansivu
23860b1ee8 Updated ChangeLog. 2025-10-19 18:58:36 +03:00
Lauri Ojansivu
101048339b Fix Due dates to be color coded and have icons. Part 2.
Thanks to xet7 !

Fixes #5950
2025-10-19 18:55:44 +03:00
Lauri Ojansivu
dc78e3b7a0 Updated ChangeLog. 2025-10-19 18:45:32 +03:00
Lauri Ojansivu
d965faa317 Fix Due dates to be color coded and have icons.
Thanks to xet7 !

Fixes #5950
2025-10-19 18:42:37 +03:00
Lauri Ojansivu
5d2bfab0f5 Updated ChangeLog. 2025-10-19 18:12:25 +03:00
Lauri Ojansivu
841a6eaf8c Merge branch 'helioguardabaxo-master' 2025-10-19 18:11:05 +03:00
Lauri Ojansivu
db59bb4aa4 Merge branch 'master' of github.com:helioguardabaxo/wekan into helioguardabaxo-master 2025-10-19 18:01:30 +03:00
helioguardabaxo
61f7099106 Fix stared, archive and clone icons 2025-10-19 09:24:18 -03:00
Lauri Ojansivu
ef828bdd38 Updated translations.
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
2025-10-19 14:19:51 +03:00
Lauri Ojansivu
1134b45b05 Updated ChangeLog. 2025-10-19 11:07:49 +03:00
Lauri Ojansivu
b06daff4c7 Fix add and drag drop attachments to minicards and card.
Thanks to xet7 !

Fixes #5946,
fixes #5436,
fixes #2936,
fixes #1926,
fixes #300,
fixes #142
2025-10-19 10:57:36 +03:00
Lauri Ojansivu
cea414b589 Updated translations.
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
2025-10-17 21:55:49 +03:00
267 changed files with 26535 additions and 4576 deletions

1
.github/FUNDING.yml vendored
View file

@ -1,3 +1,4 @@
# These are supported funding model platforms
github: wekan
custom: ['https://wekan.fi/commercial-support/']

View file

@ -9,6 +9,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4

View file

@ -32,7 +32,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
@ -48,7 +48,7 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

View file

@ -15,6 +15,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Build the Docker image
run: docker build . --file Dockerfile --tag wekan:$(date +%s)

View file

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0

View file

@ -18,7 +18,7 @@ jobs:
# runs-on: ubuntu-latest
# steps:
# - name: checkout
# uses: actions/checkout@v5
# uses: actions/checkout@v6
#
# - name: setup node
# uses: actions/setup-node@v1
@ -42,7 +42,7 @@ jobs:
# needs: [lintcode]
# steps:
# - name: checkout
# uses: actions/checkout@v5
# uses: actions/checkout@v6
#
# - name: setup node
# uses: actions/setup-node@v1
@ -65,7 +65,7 @@ jobs:
# needs: [lintcode,lintstyle]
# steps:
# - name: checkout
# uses: actions/checkout@v5
# uses: actions/checkout@v6
#
# - name: setup node
# uses: actions/setup-node@v1
@ -90,12 +90,12 @@ jobs:
# CHECKOUTS
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
# CACHING
- name: Install Meteor
id: cache-meteor-install
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.meteor
key: v1-meteor-${{ hashFiles('.meteor/versions') }}
@ -104,7 +104,7 @@ jobs:
- name: Cache NPM dependencies
id: cache-meteor-npm
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.npm
key: v1-npm-${{ hashFiles('package-lock.json') }}
@ -113,7 +113,7 @@ jobs:
- name: Cache Meteor build
id: cache-meteor-build
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
.meteor/local/resolver-result-cache.json
@ -136,7 +136,7 @@ jobs:
run: sh ./test-wekan.sh -cv
- name: Upload coverage
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: coverage-folder
path: .coverage/
@ -147,10 +147,10 @@ jobs:
needs: [tests]
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download coverage
uses: actions/download-artifact@v5
uses: actions/download-artifact@v7
with:
name: coverage-folder
path: .coverage/

View file

@ -19,6 +19,243 @@ 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.
# Upcoming WeKan ® release
This release adds the following updates:
- [Update GitHub docker/metadata-action from 5.8.0 to 5.9.0](https://github.com/wekan/wekan/pull/6012).
Thanks to dependabot.
- [Updated security.md](https://github.com/wekan/wekan/commit/7ff1649d8909917cae590c68def6eecac0442f91).
Thanks to xet7.
- [Updated build script for Linux arm64 bundle](https://github.com/wekan/wekan/commit/3db1305e58168f7417023ccd8d54995026844b18).
Thanks to xet7.
and fixes the following bugs:
- [Fix Broken Strikethroughs in Markdown to HTML conversion](https://github.com/wekan/wekan/pull/6009).
Thanks to brlin-tw.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.17 2025-11-06 WeKan ® release
This release adds the following new feature:
- [Feature: Workspaces, at All Boards page](https://github.com/wekan/wekan/commit/0afbdc95b49537e06b4f9cf98f51a669ef249384).
Thanks to xet7.
and fixes the following bugs:
- [Fix 8.16: Switching Board View fails with 403 error](https://github.com/wekan/wekan/commit/550d87ac6cb3ec946600616485afdbd242983ab4).
Thanks to xet7.
- [Moved migrations from opening board to right sidebar / Migrations](https://github.com/wekan/wekan/commit/1b25d1d5720d4f486a10d2acce37e315cf9b6057).
Thanks to xet7.
- [Fix 8.16 Lists with no items are deleted every time when board is opened. Moved migrations to right sidebar](https://github.com/wekan/wekan/commit/7713e613b431e44dc13cee72e7a1e5f031473fa6).
Thanks to xet7.
- [Remove old translations and code not in use anymore](https://github.com/wekan/wekan/commit/ba49d4d140bc0d4cfb5a96db9ab077bc85db58f1).
Thanks to xet7.
- [Fixed sidebar migrations to be per-board, not global. Clarified translations](https://github.com/wekan/wekan/commit/e4638d5fbcbe004ac393462331805cac3ba25097).
Thanks to xet7.
- [Fix star board](https://github.com/wekan/wekan/commit/8711b476be30496b96b845529b5717bb6e685c27).
Thanks to xet7.
- [Fix Card emoji issues](https://github.com/wekan/wekan/commit/e5e711c938edcca23c974c3eec97296898bcf24e).
Thanks to xet7.
- [Try to fix Edit Custom Fields button not working. Removed duplicate option from Boards Settings](https://github.com/wekan/wekan/commit/20af0a2ef55b11e7205845859ee92a929616ce91).
Thanks to xet7.
- [Fix Regression - calendar popup to set due date has gone](https://github.com/wekan/wekan/commit/581733d605b7e0494e72229c45947cff134f6dd6).
Thanks to xet7.
- [Remove not working Bookmark menu option](https://github.com/wekan/wekan/commit/c829c073cf822e48b7cd84bbfb79d42867412517).
Thanks to xet7.
- [Fix Workspaces at All Boards to have correct count of remaining etc, while starred also at Starred/Favorites](https://github.com/wekan/wekan/commit/6244657ca53a54646ec01e702851a51d89bd0d55).
Thanks to xet7.
- [Fix Worker Permissions does not allow for cards to be moved. - v8.15. Removed buttons Worker should not use](https://github.com/wekan/wekan/commit/18003900c2d497c129793d1653d4d9872a2f19da).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.16 2025-11-02 WeKan ® release
This release fixes SpaceBleed that is the following CRITICAL SECURITY ISSUES:
- [Fix SECURITY ISSUE 1: File Attachments enables stored XSS (High)](https://github.com/wekan/wekan/commit/e9a727301d7b4f1689a703503df668c0f4f4cab8).
Thanks to Siam Thanat Hack (STH) and xet7.
- [Fix SECURITY ISSUE 2: Access to boards of any Orgs/Teams, and avatar permissions](https://github.com/wekan/wekan/commit/f26d58201855e861bab1cd1fda4d62c664efdb81).
Thanks to Siam Thanat Hack (STH) and xet7.
- [Fix SECURITY ISSUE 3: Unauthenticated (or any) user can update board sort](https://github.com/wekan/wekan/commit/ea310d7508b344512e5de0dfbc9bdfd38145c5c5).
Thanks to Siam Thanat Hack (STH) and xet7.
- [Fix SECURITY ISSUE 4: Members can forge others votes (Low). Bonus: Similar fixes to planning poker too done by xet7](https://github.com/wekan/wekan/commit/0a1a075f3153e71d9a858576f1c68d2925230d9c).
Thanks to Siam Thanat Hack (STH) and xet7.
- [Fix SECURITY ISSUE 5: Attachment API uses bearer value as userId and DoS (Low)](https://github.com/wekan/wekan/commit/ccd90343394f433b287733ad0a33c08e0a71f53c).
Thanks to Siam Thanat Hack (STH) and xet7.
and 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.
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
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:
- [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:
- [Fix Regression - unable to view cards by due date v8.11](https://github.com/wekan/wekan/commit/ae11e80bde79d9ad412d185f20e5a7f802685260).
Thanks to xet7.
- [Fix Regression - unable to rearrange tasks within a checklist - v8.11](https://github.com/wekan/wekan/commit/544b24ceb1687e5b568d8c7b74403a5a2e3f6bc6).
Thanks to xet7.
- [Fix unable to add members to board](https://github.com/wekan/wekan/commit/c6d46006837a29fb311e444f94fa65f236e23bc7).
Thanks to xet7.
- [Removed not needed | at left side of minicard badges](https://github.com/wekan/wekan/commit/a0c30c35ed57113df041ef1020d3e9e5449f35e4).
Thanks to xet7.
- [Fix opened card Date Format to be used at dates popups](https://github.com/wekan/wekan/commit/7ca81285b14d1ec60d6e7e9c191d1194950f18c8).
Thanks to xet7.
- [Fix UI issues of Right Sidebar / Subtasks Settings and Card Settings](https://github.com/wekan/wekan/commit/45537ede870eca59ad72cd7ad013a12f60032df4).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.11 2025-10-21 WeKan ® release
This release fixes the following bugs:
- [Fix due dates to use colors: red = overdue, amber = due soon, no shade = not due yet](https://github.com/wekan/wekan/commit/1aa0d849775fbd0dfc83fa8e4cdca84d22a15042).
Thanks to xet7.
- [Fix My Due Cards to be sorted by due date, oldest first](https://github.com/wekan/wekan/commit/a540b12895520f398bce10bd244f733d221975d4).
Thanks to xet7.
- [Verify that due background colors are correct also at My Due Cards](https://github.com/wekan/wekan/commit/665c9b5e522e73115a1515ced066037110db84e1).
Thanks to xet7.
- [Fix Regression - due date taking a while to load all cards v8.06](https://github.com/wekan/wekan/commit/347fa9e5cd89d064ebb8ab544e20a41f52206db6).
Thanks to xet7.
- Fix duplicated lists.
[Part 1](https://github.com/wekan/wekan/commit/b6e7b258e0e8caecafc553dceb5771985992a0f9),
[Part 2](https://github.com/wekan/wekan/commit/b7ca2310b2cdec7db204229b2d5b9f95b6da8c7d),
[Part 3](https://github.com/wekan/wekan/commit/58df525b4915a99d0f603cc2536fd1fad1d20b29).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.10 2025-10-21 WeKan ® release
This release fixes the following bugs:
- [Prevent opened board re-migrating and reloading every 5 seconds](https://github.com/wekan/wekan/commit/4987a95d8e35fc4cd30010fd17722ee94037d7f2).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# 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:
- [At Public Board, drag resize list width and swimlane height. For logged in users, fix adding labels](https://github.com/wekan/wekan/commit/351433524708e9a7ccb4795d9ca31a78904943ea).
Thanks to xet7.
- [When opening board, migrate from Shared Lists to Per-Swimlane Lists](https://github.com/wekan/wekan/commit/1e6252de7f26f3af14a99fb63b5dac27ba0576f3).
Thanks to xet7.
- [Added Date Format setting to Opened Card](https://github.com/wekan/wekan/commit/2dd3916f7ee3df10bd88643cf2c796cb166b3044).
Thanks to xet7.
and fixes the following bugs:
- [Fix add and drag drop attachments to minicards and card](https://github.com/wekan/wekan/commit/b06daff4c7e63453643459f7d8798fde97e3200c).
Thanks to xet7.
- [Fix starred, archive and clone icons](https://github.com/wekan/wekan/pull/5953).
Thanks to helioguardabaxo.
- Fix Due dates to be color coded and have unicode icons.
[Part 1](https://github.com/wekan/wekan/commit/d965faa3174dc81636106e6f81435b2750b0625f),
[Part 2](https://github.com/wekan/wekan/commit/101048339bdd1e45f876aeb1aa5ec32ceda28139).
Thanks to xet7.
- [Fix unable to see My Due Cards](https://github.com/wekan/wekan/commit/66b444e2b0c9b2ed5f98cd1ff0cd9222b2d0c624).
Thanks to xet7.
- Fix drag drop lists.
[Part 1](https://github.com/wekan/wekan/commit/324f3f7794aace800022a24deb5fd5fb36ebd384),
[Part 2](https://github.com/wekan/wekan/commit/ff516ec696ef499f11b04b30053eeb9d3f96d8d1).
Thanks to xet7.
- [Removed extra pipe characters](https://github.com/wekan/wekan/commit/caa6e615ff3c3681bf2b470a625eb39c6009b825).
Thanks to xet7.
- [Fix syntax error at migrations](https://github.com/wekan/wekan/commit/eb6b42c4c9f99894fd93e62c9b3fceda3429c96c).
Thanks to xet7.
- [Fix opened card attachments button text to be at tooltip, not at opened card](https://github.com/wekan/wekan/commit/1e53125499ef563ca3c65f786ac3525e5f50274c).
Thanks to xet7.
- [Fix Broken Hyperlinks in Markdown to HTML conversion](https://github.com/wekan/wekan/commit/973a49526fdf22c143468d3d9db64269b1defa7d).
Thanks to xet7.
- [Fix migrations](https://github.com/wekan/wekan/commit/0acbf30b0346f49c0ee8f5161fb00b4eca8e1a0c).
Thanks to xet7.
- [Fix card popup to use HTML date, not anymore JQuery date](https://github.com/wekan/wekan/commit/2d44881619d78e8ef4c5060d17e9035f5babd778).
Thanks to xet7.
- [Fix Bug: Scale of Minicard icons is linked to horizontal screensize](https://github.com/wekan/wekan/commit/b6b0c5fe6d7dbd37926c662f96f2e3653cabd867).
Thanks to xet7.
- [Fix Bug Member settings drops to the second line and overlaps when many boards are starred as favourites](https://github.com/wekan/wekan/commit/46d46e313cbb8d9c3e4a976ec27b5141c266050f).
Thanks to xet7.
- [Some mobile view fixes](https://github.com/wekan/wekan/commit/c4af4d03acc02f3e54e91f2a65bce2f88742b1a6).
Thanks to xet7.
- [Have all iPhone use mobile view by default, while still having possibility to use mobile/desktop switch button for desktop mode](https://github.com/wekan/wekan/commit/5df4efd7ba06e618e454f068df05885306283bb1).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.05 2025-10-17 WeKan ® release
This release fixes the following bugs:

View file

@ -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.05/wekan-8.05-amd64.zip"
unzip wekan-8.05-amd64.zip
rm wekan-8.05-amd64.zip
wget "https://github.com/wekan/wekan/releases/download/v8.17/wekan-8.17-amd64.zip"
unzip wekan-8.17-amd64.zip
rm wekan-8.17-amd64.zip
mv /home/wekan/app/bundle /build
# Put back the original tar

View file

@ -1,12 +1,20 @@
About money, see [CONTRIBUTING.md](CONTRIBUTING.md)
Security is very important to us. If you discover any issue regarding security, please disclose
the information responsibly by sending an email from Protonmail to security@wekan.fi
that is Protomail email address, or by using this PGP key
[security-at-wekan.fi.asc](security-at-wekan.fi.asc) to security@wekan.fi
and not by creating a GitHub issue. We will respond swiftly to fix verifiable security issues.
## Responsible Security Disclosure
We thank you with a place at our hall of fame page, that is at https://wekan.fi/hall-of-fame
- To send email, use [ProtonMail](https://proton.me) email address or use PGP key [security-at-wekan.fi.asc](security-at-wekan.fi.asc)
- Send info about security issue ONLY to security@wekan.fi (that is Protomail email address). NOT TO ANYWHERE ELSE. NO CC, NO BCC.
- Wait for new WeKan release that fixes security issue
- If you approve, we thank you by adding you to Hall of Fame: https://wekan.fi/hall-of-fame/
## Bonus Points
- If you include code for fixing security issue
## Losing Points
- If you ask about [bounty](CONTRIBUTING.md). There is no bounty. WeKan is NOT Big Tech. WeKan is FLOSS.
- If you forget to include vulnerability details.
- If you send info about security issue to somewhere else than security@wekan.fi
## How should reports be formatted?
@ -26,7 +34,7 @@ CWSS (optional): %cwss
Anyone who reports a unique security issue in scope and does not disclose it to
a third party before we have patched and updated may be upon their approval
added to the Wekan Hall of Fame.
added to the WeKan Hall of Fame https://wekan.fi/hall-of-fame/
## Which domains are in scope?
@ -63,11 +71,6 @@ and by by companies that have 30k users.
- If you are thinking about TLS MITM, look at https://github.com/caddyserver/caddy/issues/2530
- Let's Encrypt TLS requires publicly accessible webserver, that Let's Encrypt TLS validation servers check.
- If firewall limits to only allowed IP addresses, you may need non-Let's Encrypt TLS cert.
- For On Premise:
- https://caddyserver.com/docs/automatic-https#local-https
- https://github.com/wekan/wekan/wiki/Caddy-Webserver-Config
- https://github.com/wekan/wekan/wiki/Azure
- https://github.com/wekan/wekan/wiki/Traefik-and-self-signed-SSL-certs
## XSS
@ -172,6 +175,57 @@ Meteor.startup(() => {
- https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
- https://wekan.github.io/hall-of-fame/filebleed/
### Attachments: Forced download to prevent stored XSS
- To prevent browser-side execution of uploaded content under the app origin, all attachment downloads are served with safe headers:
- `Content-Type: application/octet-stream`
- `Content-Disposition: attachment`
- `X-Content-Type-Options: nosniff`
- A restrictive `Content-Security-Policy` with `sandbox`
- This means attachments are downloaded instead of rendered inline by default. This mitigates HTML/JS/SVG based stored XSS vectors.
- Avatars and inline images remain supported but SVG uploads are blocked and never rendered inline.
## Users: Client update restrictions
- Client-side updates to user documents are limited to safe fields only:
- `username`
- `profile.*`
- Sensitive fields are blocked from any client updates and can only be modified by server methods with authorization:
- `orgs`, `teams`, `roles`, `isAdmin`, `createdThroughApi`, `loginDisabled`, `authenticationMethod`, `services.*`, `emails.*`, `sessionData.*`
- Attempts to update forbidden fields from the client are denied.
- Admin operations like managing org/team membership or toggling flags must use server methods that check permissions.
## Voting: integrity and authorization
- Client updates to card `vote` fields are blocked to prevent forged votes and inconsistent policy enforcement.
- Voting is performed via a server method that enforces:
- Authentication and board membership, or an explicit per-card flag allowing non-members to vote.
- Only the caller's own userId is added/removed from `vote.positive`/`vote.negative`.
- This prevents members from fabricating other users' votes and ensures non-members cannot vote unless explicitly allowed.
## Planning Poker: integrity and authorization
- Client updates to card `poker` fields are blocked. All poker actions go through server methods that enforce:
- Authentication and board membership for configuration and results.
- For casting a poker vote, either board membership or an explicit per-card flag allowing non-members to participate.
- Only the caller's own userId is added/removed from the selected estimation bucket (e.g., one, two, five, etc.).
- Methods cover setting/unsetting poker question/end, casting votes, replaying, and setting final estimation.
## Attachment API: authentication and DoS prevention
- The attachment API (`/api/attachment/*`) requires proper authentication using `X-User-Id` and `X-Auth-Token` headers.
- Authentication validates tokens by hashing with `Accounts._hashLoginToken` and matching against stored login tokens, preventing identity spoofing.
- Request handlers implement:
- 30-second timeout to prevent hanging connections.
- Request body size limits (50MB for uploads, 10MB for metadata operations).
- Proper error handling and guaranteed response completion.
- Request error event handlers to clean up failed connections.
- This prevents:
- DoS attacks via concurrent unauthenticated or malformed requests.
- Identity spoofing by using arbitrary bearer tokens or user IDs.
- Resource exhaustion from hanging connections or excessive payloads.
- Access control: all attachment operations verify board membership before allowing access.
## Brute force login protection
- https://github.com/wekan/wekan/commit/23e5e1e3bd081699ce39ce5887db7e612616014d
@ -218,9 +272,4 @@ Typical already known or "no impact" bugs such as:
- Email spoofing, SPF, DMARC & DKIM. Wekan does not include email server.
Wekan is Open Source with MIT license, and free to use also for commercial use.
We welcome all fixes to improve security by email to security@wekan.team
## Bonus Points
If your Responsible Security Disclosure includes code for fixing security issue,
you get bonus points, as seen on [Hall of Fame](https://wekan.github.io/hall-of-fame).
We welcome all fixes to improve security by email to security@wekan.fi

View file

@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v8.05.0"
appVersion: "v8.17.0"
files:
userUploads:
- README.md

View file

@ -15,3 +15,50 @@ import '/client/components/migrationProgress';
// Import cron settings
import '/client/components/settings/cronSettings';
// Mirror Meteor login token into a cookie for server-side file route auth
// This enables cookie-based auth for /cdn/storage/* without leaking ROOT_URL
// Token already lives in localStorage; cookie adds same-origin send-on-request semantics
Meteor.startup(() => {
const COOKIE_NAME = 'meteor_login_token';
const cookieAttrs = () => {
const attrs = ['Path=/', 'SameSite=Lax'];
try {
if (window.location && window.location.protocol === 'https:') {
attrs.push('Secure');
}
} catch (_) {}
return attrs.join('; ');
};
const setCookie = (name, value) => {
if (!value) return;
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; ${cookieAttrs()}`;
};
const clearCookie = (name) => {
document.cookie = `${encodeURIComponent(name)}=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; ${cookieAttrs()}`;
};
const syncCookie = () => {
try {
const token = Accounts && typeof Accounts._storedLoginToken === 'function' ? Accounts._storedLoginToken() : null;
if (token) setCookie(COOKIE_NAME, token); else clearCookie(COOKIE_NAME);
} catch (e) {
// ignore
}
};
// Initial sync on startup
syncCookie();
// Keep cookie in sync on login/logout
if (Accounts && typeof Accounts.onLogin === 'function') Accounts.onLogin(syncCookie);
if (Accounts && typeof Accounts.onLogout === 'function') Accounts.onLogout(syncCookie);
// Sync across tabs/windows when localStorage changes
window.addEventListener('storage', (ev) => {
if (ev && typeof ev.key === 'string' && ev.key.indexOf('Meteor.loginToken') !== -1) {
syncCookie();
}
});
});

View file

@ -269,56 +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) {
@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 {
@ -496,3 +511,10 @@
font-size: 25px;
cursor: pointer;
}
/* Global file drag over state for board canvas */
.board-canvas.file-drag-over {
background-color: rgba(0, 123, 255, 0.05) !important;
border: 2px dashed #007bff !important;
transition: all 0.2s ease;
}

View file

@ -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';
@ -17,6 +18,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 +31,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)) {
@ -56,10 +71,17 @@ BlazeComponent.extendComponent({
const swimlanes = board.swimlanes();
if (swimlanes.length === 0) {
const swimlaneId = Swimlanes.insert({
title: 'Default',
boardId: boardId,
});
// Check if any swimlane exists in the database to avoid race conditions
const existingSwimlanes = ReactiveCache.getSwimlanes({ boardId });
if (existingSwimlanes.length === 0) {
const swimlaneId = Swimlanes.insert({
title: 'Default',
boardId: boardId,
});
if (process.env.DEBUG === 'true') {
console.log(`Created default swimlane ${swimlaneId} for board ${boardId}`);
}
}
this._swimlaneCreated.add(boardId);
} else {
this._swimlaneCreated.add(boardId);
@ -77,43 +99,10 @@ BlazeComponent.extendComponent({
return;
}
// Check if board needs migration based on migration version
const needsMigration = !board.migrationVersion || board.migrationVersion < 1;
// Automatic migration disabled - migrations must be run manually from sidebar
// Board admins can run migrations from the sidebar Migrations menu
this.isBoardReady.set(true);
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);
}
}
// Start attachment migration in background if needed
this.startAttachmentMigrationIfNeeded(boardId);
} catch (error) {
console.error('Error during board conversion check:', error);
this.isConverting.set(false);
@ -122,6 +111,136 @@ 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: '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
@ -139,6 +258,227 @@ BlazeComponent.extendComponent({
}
},
async convertSharedListsToPerSwimlane(boardId) {
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) return;
// Check if board has already been processed for shared lists conversion
if (board.hasSharedListsConverted) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been processed for shared lists conversion`);
}
return;
}
// Get all lists for this board
const allLists = board.lists();
const swimlanes = board.swimlanes();
if (swimlanes.length === 0) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has no swimlanes, skipping shared lists conversion`);
}
return;
}
// Find shared lists (lists with empty swimlaneId or null swimlaneId)
const sharedLists = allLists.filter(list => !list.swimlaneId || list.swimlaneId === '');
if (sharedLists.length === 0) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has no shared lists to convert`);
}
// Mark as processed even if no shared lists
Boards.update(boardId, { $set: { hasSharedListsConverted: true } });
return;
}
if (process.env.DEBUG === 'true') {
console.log(`Converting ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`);
}
// Convert each shared list to per-swimlane lists
for (const sharedList of sharedLists) {
// Create a copy of the list for each swimlane
for (const swimlane of swimlanes) {
// Check if this list already exists in this swimlane
const existingList = Lists.findOne({
boardId: boardId,
swimlaneId: swimlane._id,
title: sharedList.title
});
if (!existingList) {
// Double-check to avoid race conditions
const doubleCheckList = ReactiveCache.getList({
boardId: boardId,
swimlaneId: swimlane._id,
title: sharedList.title
});
if (!doubleCheckList) {
// Create a new list in this swimlane
const newListData = {
title: sharedList.title,
boardId: boardId,
swimlaneId: swimlane._id,
sort: sharedList.sort || 0,
archived: sharedList.archived || false, // Preserve archived state from original list
createdAt: new Date(),
modifiedAt: new Date()
};
// Copy other properties if they exist
if (sharedList.color) newListData.color = sharedList.color;
if (sharedList.wipLimit) newListData.wipLimit = sharedList.wipLimit;
if (sharedList.wipLimitEnabled) newListData.wipLimitEnabled = sharedList.wipLimitEnabled;
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} (double-check), skipping`);
}
}
} else {
if (process.env.DEBUG === 'true') {
console.log(`List "${sharedList.title}" already exists in swimlane ${swimlane.title || swimlane._id}, skipping`);
}
}
}
// 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
Boards.update(boardId, { $set: { hasSharedListsConverted: true } });
if (process.env.DEBUG === 'true') {
console.log(`Successfully converted ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`);
}
} catch (error) {
console.error('Error converting shared lists to per-swimlane:', error);
}
},
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) {
if (process.env.DEBUG === 'true') {
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 fixDuplicateLists(boardId) {
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) return;
// Check if board has already been processed for duplicate lists fix
if (board.fixDuplicateListsCompleted) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been processed for duplicate lists fix`);
}
return;
}
if (process.env.DEBUG === 'true') {
console.log(`Starting duplicate lists fix for board ${boardId}`);
}
// Execute the duplicate lists fix
const result = await new Promise((resolve, reject) => {
Meteor.call('fixDuplicateLists.fixBoard', boardId, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
if (result && result.fixed > 0) {
if (process.env.DEBUG === 'true') {
console.log(`Successfully fixed ${result.fixed} duplicate lists for board ${boardId}: ${result.fixedSwimlanes} swimlanes, ${result.fixedLists} lists`);
}
// Mark board as processed
Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } });
} else if (process.env.DEBUG === 'true') {
console.log(`No duplicate lists found for board ${boardId}`);
// Still mark as processed to avoid repeated checks
Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } });
} else {
// Still mark as processed to avoid repeated checks
Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } });
}
} catch (error) {
console.error('Error fixing duplicate lists:', error);
}
},
async startAttachmentMigrationIfNeeded(boardId) {
try {
// Check if board has already been migrated
@ -204,39 +544,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}`);
}
}
},
@ -550,22 +901,20 @@ BlazeComponent.extendComponent({
// Always reset dragscroll on view switch
dragscroll.reset();
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$swimlanesDom.sortable({
handle: '.js-swimlane-header-handle',
});
} else {
$swimlanesDom.sortable({
handle: '.swimlane-header',
});
}
if ($swimlanesDom.data('uiSortable') || $swimlanesDom.data('sortable')) {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$swimlanesDom.sortable('option', 'handle', '.js-swimlane-header-handle');
} else {
$swimlanesDom.sortable('option', 'handle', '.swimlane-header');
}
// Disable drag-dropping if the current user is not a board member
$swimlanesDom.sortable(
'option',
'disabled',
!ReactiveCache.getCurrentUser()?.isBoardAdmin(),
);
// Disable drag-dropping if the current user is not a board member
$swimlanesDom.sortable(
'option',
'disabled',
!ReactiveCache.getCurrentUser()?.isBoardAdmin(),
);
}
});
// If there is no data in the board (ie, no lists) we autofocus the list
@ -721,6 +1070,31 @@ BlazeComponent.extendComponent({
}
},
'click .js-empty-board-add-swimlane': Popup.open('swimlaneAdd'),
// Global drag and drop file upload handlers for better visual feedback
'dragover .board-canvas'(event) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
// Add visual indicator that files can be dropped
$('.board-canvas').addClass('file-drag-over');
}
},
'dragleave .board-canvas'(event) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
// Only remove class if we're leaving the board canvas entirely
if (!event.currentTarget.contains(event.relatedTarget)) {
$('.board-canvas').removeClass('file-drag-over');
}
}
},
'drop .board-canvas'(event) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
$('.board-canvas').removeClass('file-drag-over');
}
},
},
];
},
@ -997,9 +1371,13 @@ BlazeComponent.extendComponent({
const firstSwimlane = currentBoard.swimlanes()[0];
Meteor.call('createCardWithDueDate', currentBoard._id, firstList._id, myTitle, startDate.toDate(), firstSwimlane._id, function(error, result) {
if (error) {
console.log(error);
if (process.env.DEBUG === 'true') {
console.log(error);
}
} else {
console.log("Card Created", result);
if (process.env.DEBUG === 'true') {
console.log("Card Created", result);
}
}
});
closeModal();

View file

@ -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;
}

View file

@ -16,13 +16,6 @@ template(name="boardHeaderBar")
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
| ✏️
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}" aria-label="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}")
| {{#if isStarred}}⭐{{else}}☆{{/if}}
if showStarCounter
span
= currentBoard.stars
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
@ -38,6 +31,13 @@ template(name="boardHeaderBar")
if $eq watchLevel "muted"
| 🔕
span {{_ watchLevel}}
a.board-header-btn.js-star-board(title="{{_ 'star-board'}}")
if isStarred
| ⭐
else
| ☆
if showStarCounter
span.board-star-counter {{currentBoard.stars}}
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
| {{sortCardsIcon}}
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
@ -61,10 +61,6 @@ template(name="boardHeaderBar")
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
| ✏️
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
| {{#if isStarred}}⭐{{else}}☆{{/if}}
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
@ -78,6 +74,11 @@ template(name="boardHeaderBar")
| 🔔
if $eq watchLevel "muted"
| 🔕
a.board-header-btn.js-star-board(title="{{_ 'star-board'}}")
if isStarred
| ⭐
else
| ☆
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
| {{sortCardsIcon}}
if isSortActive
@ -237,6 +238,65 @@ template(name="createBoard")
| /
a.js-board-template {{_ 'template'}}
template(name="createBoardPopup")
form
label
| {{_ 'title'}}
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
if visibilityMenuIsOpen.get
+boardVisibilityList
else
p.quiet
if $eq visibility.get 'public'
span 🌐
= " "
| {{{_ 'board-public-info'}}}
else
span 🔒
= " "
| {{{_ 'board-private-info'}}}
a.js-change-visibility {{_ 'change'}}.
a.flex.js-toggle-add-template-container
.materialCheckBox#add-template-container
span {{_ 'add-template-container'}}
input.primary.wide(type="submit" value="{{_ 'create'}}")
span.quiet
| {{_ 'or'}}
a.js-import-board {{_ 'import'}}
span.quiet
| /
a.js-board-template {{_ 'template'}}
// New popup for Template Container creation; shares the same form content
template(name="createTemplateContainerPopup")
form
label
| {{_ 'title'}}
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
if visibilityMenuIsOpen.get
+boardVisibilityList
else
p.quiet
if $eq visibility.get 'public'
span 🌐
= " "
| {{{_ 'board-public-info'}}}
else
span 🔒
= " "
| {{{_ 'board-private-info'}}}
a.js-change-visibility {{_ 'change'}}.
a.flex.js-toggle-add-template-container
.materialCheckBox#add-template-container
span {{_ 'add-template-container'}}
input.primary.wide(type="submit" value="{{_ 'create'}}")
span.quiet
| {{_ 'or'}}
a.js-import-board {{_ 'import'}}
span.quiet
| /
a.js-board-template {{_ 'template'}}
//template(name="listsortPopup")
// h2
// | {{_ 'list-sort-by'}}

View file

@ -72,7 +72,10 @@ BlazeComponent.extendComponent({
{
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
'click .js-star-board'() {
ReactiveCache.getCurrentUser().toggleBoardStar(Session.get('currentBoard'));
const boardId = Session.get('currentBoard');
if (boardId) {
Meteor.call('toggleBoardStar', boardId);
}
},
'click .js-open-board-menu': Popup.open('boardMenu'),
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
@ -82,18 +85,26 @@ BlazeComponent.extendComponent({
},
'click .js-toggle-board-view': Popup.open('boardChangeView'),
'click .js-toggle-sidebar'() {
console.log('Hamburger menu clicked');
if (process.env.DEBUG === 'true') {
console.log('Hamburger menu clicked');
}
// Use the same approach as keyboard shortcuts
if (typeof Sidebar !== 'undefined' && Sidebar && typeof Sidebar.toggle === 'function') {
console.log('Using Sidebar.toggle()');
if (process.env.DEBUG === 'true') {
console.log('Using Sidebar.toggle()');
}
Sidebar.toggle();
} else {
console.warn('Sidebar not available, trying alternative approach');
if (process.env.DEBUG === 'true') {
console.warn('Sidebar not available, trying alternative approach');
}
// Try to trigger the sidebar through the global Blaze helper
if (typeof Blaze !== 'undefined' && Blaze._globalHelpers && Blaze._globalHelpers.Sidebar) {
const sidebar = Blaze._globalHelpers.Sidebar();
if (sidebar && typeof sidebar.toggle === 'function') {
console.log('Using Blaze helper Sidebar.toggle()');
if (process.env.DEBUG === 'true') {
console.log('Using Blaze helper Sidebar.toggle()');
}
sidebar.toggle();
}
}
@ -155,6 +166,7 @@ BlazeComponent.extendComponent({
},
];
},
}).register('boardHeaderBar');
Template.boardHeaderBar.helpers({
@ -282,6 +294,15 @@ const CreateBoard = BlazeComponent.extendComponent({
},
);
// Assign to space if one was selected
const spaceId = Session.get('createBoardInWorkspace');
if (spaceId) {
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
if (err) console.error('Error assigning board to space:', err);
});
Session.set('createBoardInWorkspace', null); // Clear after use
}
Utils.goBoardId(this.boardId.get());
} else {
@ -300,6 +321,15 @@ const CreateBoard = BlazeComponent.extendComponent({
boardId: this.boardId.get(),
});
// Assign to space if one was selected
const spaceId = Session.get('createBoardInWorkspace');
if (spaceId) {
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
if (err) console.error('Error assigning board to space:', err);
});
Session.set('createBoardInWorkspace', null); // Clear after use
}
Utils.goBoardId(this.boardId.get());
}
},
@ -321,6 +351,13 @@ const CreateBoard = BlazeComponent.extendComponent({
},
}).register('createBoardPopup');
(class CreateTemplateContainerPopup extends CreateBoard {
onRendered() {
// Always pre-check the template container checkbox for this popup
$('#add-template-container').addClass('is-checked');
}
}).register('createTemplateContainerPopup');
(class HeaderBarCreateBoard extends CreateBoard {
onSubmit(event) {
super.onSubmit(event);

View file

@ -8,6 +8,273 @@
padding: 1vh 0;
}
/* Two-column layout for All Boards */
.boards-layout {
display: grid;
grid-template-columns: 260px 1fr;
gap: 16px;
}
.boards-left-menu {
border-right: 1px solid #e0e0e0;
padding-right: 12px;
}
.boards-left-menu ul.menu {
list-style: none;
padding: 0;
margin: 0 0 12px 0;
}
.boards-left-menu .menu-item {
margin: 4px 0;
}
.boards-left-menu .menu-item a {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
border-radius: 4px;
cursor: pointer;
}
.boards-left-menu .menu-item .menu-label {
flex: 1;
}
.boards-left-menu .menu-item .menu-count {
background: #ddd;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
margin-left: 8px;
}
.boards-left-menu .menu-item.active a,
.boards-left-menu .menu-item a:hover {
background: #f0f0f0;
}
.boards-left-menu .menu-item.active .menu-count {
background: #bbb;
}
/* Drag-over state for menu items (for dropping boards on Remaining) */
.boards-left-menu .menu-item a.drag-over {
background: #d0e8ff;
border: 2px dashed #2196F3;
}
.workspaces-header {
display: flex;
align-items: center;
justify-content: space-between;
font-weight: bold;
margin-top: 12px;
}
.workspaces-header .js-add-space {
text-decoration: none;
font-weight: bold;
border: 1px solid #ccc;
padding: 2px 8px;
border-radius: 4px;
}
.workspace-tree {
list-style: none;
padding-left: 10px;
}
.workspace-node {
margin: 2px 0;
position: relative;
}
.workspace-node-content {
display: flex;
align-items: center;
gap: 4px;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s;
}
.workspace-node.dragging > .workspace-node-content {
opacity: 0.5;
background: #e0e0e0;
}
.workspace-node.drag-over > .workspace-node-content {
background: #d0e8ff;
border: 2px dashed #2196F3;
}
.workspace-drag-handle {
cursor: grab;
color: #999;
font-size: 14px;
padding: 0 4px;
user-select: none;
}
.workspace-drag-handle:active {
cursor: grabbing;
}
.workspace-node .js-select-space {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
flex: 1;
text-decoration: none;
}
.workspace-node .workspace-icon {
font-size: 16px;
line-height: 1;
}
.workspace-node .workspace-name {
flex: 1;
}
.workspace-node .workspace-count {
background: #ddd;
padding: 2px 6px;
border-radius: 10px;
font-size: 11px;
font-weight: bold;
min-width: 20px;
text-align: center;
}
.workspace-node .js-edit-space,
.workspace-node .js-add-subspace {
padding: 2px 6px;
border-radius: 3px;
cursor: pointer;
text-decoration: none;
font-size: 14px;
opacity: 0.6;
transition: opacity 0.2s;
}
.workspace-node .js-edit-space:hover,
.workspace-node .js-add-subspace:hover {
opacity: 1;
background: #e0e0e0;
}
.workspace-node.active > .workspace-node-content .js-select-space,
.workspace-node > .workspace-node-content:hover .js-select-space {
background: #f0f0f0;
}
.workspace-node.active .workspace-count {
background: #bbb;
}
.boards-right-grid {
min-height: 200px;
}
.boards-path-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 12px 16px;
margin-bottom: 16px;
background: #f5f5f5;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
}
.boards-path-header .path-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.boards-path-header .multiselection-hint {
background: #FFF3CD;
color: #856404;
padding: 4px 12px;
border-radius: 4px;
font-size: 13px;
font-weight: normal;
border: 1px solid #FFE69C;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.boards-path-header .path-right {
display: flex;
align-items: center;
gap: 8px;
}
.boards-path-header .path-icon {
font-size: 18px;
}
.boards-path-header .path-text {
color: #333;
}
.boards-path-header .board-header-btn {
padding: 6px 12px;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
transition: all 0.2s;
}
.boards-path-header .board-header-btn:hover {
background: #f0f0f0;
border-color: #bbb;
}
.boards-path-header .board-header-btn.emphasis {
background: #2196F3;
color: #fff;
border-color: #2196F3;
font-weight: bold;
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.5);
transform: scale(1.05);
}
.boards-path-header .board-header-btn.emphasis:hover {
background: #1976D2;
box-shadow: 0 3px 12px rgba(33, 150, 243, 0.7);
}
.boards-path-header .board-header-btn-close {
padding: 4px 10px;
background: #f44336;
color: #000;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-left: 10px; /* Extra space between MultiSelection toggle and Remove Filter */
}
.boards-path-header .board-header-btn-close:hover {
background: #d32f2f;
}
.zoom-controls {
display: flex;
align-items: center;
@ -103,26 +370,38 @@
transform: rotate(4deg);
display: block !important;
}
.board-list li.starred .fa-star,
.board-list li.starred .fa-star-o {
.board-list li.starred .is-star-active,
.board-list li.starred .is-not-star-active {
opacity: 1;
color: #ffd700;
}
/* Show star icon on hover even for non-starred boards */
.board-list li:hover .is-star-active,
.board-list li:hover .is-not-star-active {
opacity: 1;
}
.board-list .board-list-item {
overflow: hidden;
background-color: #999;
background-color: inherit; /* Inherit board color from parent li.js-board */
color: #f6f6f6;
min-height: 100px;
font-size: 16px;
line-height: 22px;
border-radius: 3px;
border-radius: 0; /* No border-radius - parent .js-board has it */
display: block;
font-weight: 700;
padding: 8px;
margin: 8px;
padding: 36px 8px 32px 8px; /* Top padding for drag handle, bottom for checkbox */
margin: 0; /* No margin - moved to parent .js-board */
position: relative;
text-decoration: none;
word-wrap: break-word;
}
.board-list .board-list-item > .js-open-board {
text-decoration: none;
color: inherit;
display: block;
}
.board-list .board-list-item.template-container {
border: 4px solid #fff;
}
@ -150,13 +429,20 @@
.board-list .js-add-board .label {
font-weight: normal;
line-height: 56px;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
background-color: #999; /* Darker background for better text contrast */
border-radius: 3px;
padding: 36px 8px 32px 8px;
}
.board-list .js-add-board :hover {
background-color: #939393;
.board-list .js-add-board .label:hover {
background-color: #808080; /* Even darker on hover */
}
.board-list .fa-star,
.board-list .fa-star-o {
bottom: 0;
.board-list .is-star-active,
.board-list .is-not-star-active {
top: 0;
font-size: 14px;
height: 18px;
line-height: 18px;
@ -164,7 +450,6 @@
padding: 9px 9px;
position: absolute;
right: 0;
top: 0;
transition-duration: 0.15s;
transition-property: color, font-size, background;
}
@ -212,32 +497,121 @@
transition-duration: 0.15s;
transition-property: color, font-size, background;
}
.board-list li:hover a:hover .fa-star,
.board-list li:hover a:hover .is-star-active,
.board-list li:hover a:hover .fa-clone,
.board-list li:hover a:hover .fa-archive,
.board-list li:hover a:hover .fa-star-o {
.board-list li:hover a:hover .is-not-star-active {
color: #fff;
}
.board-list li:hover a .fa-star,
.board-list li:hover a .is-star-active,
.board-list li:hover a .fa-clone,
.board-list li:hover a .fa-archive,
.board-list li:hover a .fa-star-o {
.board-list li:hover a .is-not-star-active {
color: #fff;
opacity: 0.75;
}
.board-list li:hover a .fa-star:hover,
.board-list li:hover a .is-star-active:hover,
.board-list li:hover a .fa-clone:hover,
.board-list li:hover a .fa-archive:hover,
.board-list li:hover a .fa-star-o:hover {
.board-list li:hover a .is-not-star-active:hover {
font-size: 18px;
opacity: 1;
}
.board-list li:hover a .fa-star.is-star-active,
.board-list li:hover a .fa-clone.is-star-active,
.board-list li:hover a .fa-archive.is-star-active,
.board-list li:hover a .fa-star-o.is-star-active {
.board-list li:hover a .is-star-active,
.board-list li:hover a .fa-clone,
.board-list li:hover a .fa-archive,
.board-list li:hover a .is-not-star-active {
opacity: 1;
}
/* Board drag handle - always visible and positioned at top */
.board-list .board-handle {
position: absolute;
padding: 4px 6px;
top: 4px;
left: 50%;
transform: translateX(-50%);
font-size: 14px;
color: #fff;
background: rgba(0,0,0,0.4);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
transition: background-color 0.2s ease;
cursor: grab;
opacity: 1;
user-select: none;
}
.board-list .board-handle:active {
cursor: grabbing;
}
.board-list .board-handle:hover {
background: rgba(255, 255, 0, 0.8) !important;
color: #000;
}
/* Multiselection checkbox on board items */
.board-list .board-list-item .multi-selection-checkbox {
position: absolute !important;
bottom: 4px !important;
left: 4px !important;
top: auto !important;
width: 24px;
height: 24px;
border: 3px solid #fff;
background: rgba(0,0,0,0.5);
border-radius: 4px;
cursor: pointer;
z-index: 11;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
transform: none !important;
margin: 0 !important;
}
.board-list .board-list-item .multi-selection-checkbox:hover {
background: rgba(0,0,0,0.7);
transform: scale(1.15) !important;
box-shadow: 0 3px 6px rgba(0,0,0,0.5);
}
.board-list .board-list-item .multi-selection-checkbox.is-checked {
background: #2196F3;
border-color: #2196F3;
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.6);
width: 24px !important;
height: 24px !important;
top: auto !important;
left: 4px !important;
transform: none !important;
border-radius: 4px !important;
}
.board-list .board-list-item .multi-selection-checkbox.is-checked::after {
content: '✓';
color: #fff;
font-size: 16px;
font-weight: bold;
}
.board-list.is-multiselection-active .js-board.is-checked {
outline: 4px solid #2196F3;
outline-offset: -4px;
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4);
}
/* Visual hint when multiselection is active */
.board-list.is-multiselection-active .board-list-item {
border: 2px dashed rgba(33, 150, 243, 0.3);
}
.board-backgrounds-list .board-background-select {
box-sizing: border-box;
display: block;
@ -361,6 +735,18 @@
min-height: 100vh; /* Force content to be tall enough to scroll */
}
/* Hide archive and clone board buttons in mobile view */
.board-list.mobile-view .js-archive-board,
.board-list.mobile-view .js-clone-board {
display: none !important;
}
/* Change board drag handle to up-down arrow in mobile view */
.board-list.mobile-view .board-handle.fa-arrows::before {
content: "↕️" !important;
font-family: inherit !important;
}
.board-list.mobile-view::after {
content: '';
display: block;
@ -371,7 +757,8 @@
screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) {
screen and (max-width: 800px) and (orientation: landscape),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
.board-list {
height: 100%;
overflow-y: auto;
@ -457,7 +844,8 @@
screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) {
screen and (max-width: 800px) and (orientation: landscape),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
.wrapper {
font-size: 2em !important; /* 2x bigger base font size for All Boards page */
}
@ -725,9 +1113,62 @@
#resetBtn {
display: inline;
}
#resetBtn.filter-reset-btn {
background: #f44336;
color: #000;
border: none;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 6px;
transition: background 0.2s;
}
#resetBtn.filter-reset-btn:hover {
background: #d32f2f;
}
#resetBtn.filter-reset-btn .reset-icon {
font-size: 14px;
}
.js-board {
display: block;
background-color: #999; /* Default gray background if no color class is applied */
border-radius: 3px; /* Rounded corners for board items */
overflow: hidden; /* Ensure children respect rounded corners */
margin: 8px; /* Space between board items */
}
/* Reset background for add-board button */
.js-add-board {
background-color: transparent !important;
margin: 8px !important; /* Keep margin for add-board */
}
/* Apply board colors to li.js-board parent instead of just the link */
.board-list .board-color-nephritis { background-color: #27ae60; }
.board-list .board-color-pomegranate { background-color: #c0392b; }
.board-list .board-color-belize { background-color: #2980b9; }
.board-list .board-color-wisteria { background-color: #8e44ad; }
.board-list .board-color-midnight { background-color: #2c3e50; }
.board-list .board-color-pumpkin { background-color: #e67e22; }
.board-list .board-color-moderatepink { background-color: #cd5a91; }
.board-list .board-color-strongcyan { background-color: #00aecc; }
.board-list .board-color-limegreen { background-color: #4bbf6b; }
.board-list .board-color-dark { background-color: #2c3e51; }
.board-list .board-color-relax { background-color: #27ae61; }
.board-list .board-color-corteza { background-color: #568ba2; }
.board-list .board-color-clearblue { background-color: #3498db; }
.board-list .board-color-natural { background-color: #596557; }
.board-list .board-color-modern { background-color: #2a80b8; }
.board-list .board-color-moderndark { background-color: #2a2a2a; }
.board-list .board-color-exodark { background-color: #222; }
.minicard-members {
padding: 6px 0 6px 8px;
width: 100%;
@ -757,7 +1198,8 @@
screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) {
screen and (max-width: 800px) and (orientation: landscape),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
.wrapper {
overflow: hidden;
height: 100vh;
@ -824,5 +1266,17 @@
#content {
overflow: hidden;
}
/* Hide archive and clone board buttons in mobile view */
.board-list .js-archive-board,
.board-list .js-clone-board {
display: none !important;
}
/* Change board drag handle to up-down arrow in mobile view */
.board-list .board-handle.fa-arrows::before {
content: "↕️" !important;
font-family: inherit !important;
}
}

View file

@ -2,148 +2,160 @@ template(name="boardList")
.wrapper
.board-list-header
ul.AllBoardTeamsOrgs
li.AllBoardTeams
if userHasTeams
select.js-AllBoardTeams#jsAllBoardTeams("multiple")
option(value="-1") {{_ 'teams'}} :
each teamsDatas
option(value="{{teamId}}") {{_ teamDisplayName}}
.boards-layout
// Left menu
.boards-left-menu
ul.menu
li(class="menu-item {{#if isSelectedMenu 'starred'}}active{{/if}}")
a.js-select-menu(data-type="starred")
span.menu-label ⭐ {{_ 'allboards.starred'}}
span.menu-count {{menuItemCount 'starred'}}
li(class="menu-item {{#if isSelectedMenu 'templates'}}active{{/if}}")
a.js-select-menu(data-type="templates")
span.menu-label 📋 {{_ 'allboards.templates'}}
span.menu-count {{menuItemCount 'templates'}}
li(class="menu-item {{#if isSelectedMenu 'remaining'}}active{{/if}}")
a.js-select-menu(data-type="remaining")
span.menu-label 📂 {{_ 'allboards.remaining'}}
span.menu-count {{menuItemCount 'remaining'}}
.workspaces-header
span 🗂️ {{_ 'allboards.workspaces'}}
a.js-add-workspace(title="{{_ 'allboards.add-workspace'}}") +
// Workspaces tree
+workspaceTree(nodes=workspacesTree selectedWorkspaceId=selectedWorkspaceId)
li.AllBoardOrgs
if userHasOrgs
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
option(value="-1") {{_ 'organizations'}} :
each orgsDatas
option(value="{{orgId}}") {{orgDisplayName}}
// Existing filter by orgs/teams (kept)
ul.AllBoardTeamsOrgs
li.AllBoardTeams
if userHasTeams
select.js-AllBoardTeams#jsAllBoardTeams("multiple")
option(value="-1") {{_ 'teams'}} :
each teamsDatas
option(value="{{teamId}}") {{_ teamDisplayName}}
//li.AllBoardTemplates
// if userHasTemplates
// select.js-AllBoardTemplates#jsAllBoardTemplates("multiple")
// option(value="-1") {{_ 'templates'}} :
// each templatesDatas
// option(value="{{templateId}}") {{_ templateDisplayName}}
li.AllBoardOrgs
if userHasOrgs
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
option(value="-1") {{_ 'organizations'}} :
each orgsDatas
option(value="{{orgId}}") {{orgDisplayName}}
li.AllBoardBtns
div.AllBoardButtonsContainer
if userHasOrgsOrTeams
i.fa.fa-filter
input#filterBtn(type="button" value="{{_ 'filter'}}")
input#resetBtn(type="button" value="{{_ 'filter-clear'}}")
li.AllBoardBtns
div.AllBoardButtonsContainer
if userHasOrgsOrTeams
span 🔍
input#filterBtn(type="button" value="{{_ 'filter'}}")
button#resetBtn.filter-reset-btn
span.reset-icon ❌
span {{_ 'filter-clear'}}
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}}")
li.js-add-board
a.board-list-item.label(title="{{_ 'add-board'}}")
| {{_ 'add-board'}}
each boards
li(class="{{_id}}" class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
if isInvited
.board-list-item
span.details
span.board-list-item-name= title
i.fa.js-star-board(
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
title="{{_ 'star-board-title'}}")
p.board-list-item-desc {{_ 'just-invited'}}
button.js-accept-invite.primary {{_ 'accept'}}
button.js-decline-invite {{_ 'decline'}}
else
if $eq type "template-container"
a.js-open-board.template-container.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
span.details
span.board-list-item-name(title="{{_ 'template-container'}}")
+viewer
= title
i.fa.js-star-board(
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
title="{{_ 'star-board-title'}}")
p.board-list-item-desc
+viewer
= description
if hasSpentTimeCards
i.fa.js-has-spenttime-cards(
class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
if isTouchScreenOrShowDesktopDragHandles
i.fa.board-handle(
class="fa-arrows"
title="{{_ 'drag-board'}}")
else
if isSandstorm
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
else if isAdministrable
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
else if currentUser.isAdmin
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
// Right boards grid
.boards-right-grid
.boards-path-header
.path-left
span.path-icon {{currentMenuPath.icon}}
span.path-text {{currentMenuPath.text}}
if BoardMultiSelection.isActive
span.multiselection-hint 📌 {{_ 'multi-selection-active'}}
.path-right
if canModifyBoards
if hasBoardsSelected
button.js-archive-selected-boards.board-header-btn
span 📦
span {{_ 'archive-board'}}
button.js-duplicate-selected-boards.board-header-btn
span 📋
span {{_ 'duplicate-board'}}
a.board-header-btn.js-multiselection-activate(
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
| ☑️
if BoardMultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
| ✖
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
li.js-add-board
if isSelectedMenu 'templates'
a.board-list-item.label(title="{{_ 'add-template-container'}}")
| {{_ 'add-template-container'}}
else
a.js-open-board.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
span.details
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
+viewer
= title
unless currentSetting.hideBoardMemberList
if allowsBoardMemberList
.minicard-members
each member in boardMembers _id
a.name
+userAvatar(userId=member noRemove=true)
unless currentSetting.hideCardCounterList
if allowsCardCounterList
.minicard-lists.flex.flex-wrap
each list in boardLists _id
.item
| {{ list }}
i.fa.js-star-board(
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
title="{{_ 'star-board-title'}}")
p.board-list-item-desc
+viewer
= description
if hasSpentTimeCards
i.fa.js-has-spenttime-cards(
class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
if isTouchScreenOrShowDesktopDragHandles
i.fa.board-handle(
class="fa-arrows"
title="{{_ 'drag-board'}}")
else
if isSandstorm
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
else if isAdministrable
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
else if currentUser.isAdmin
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
a.board-list-item.label(title="{{_ 'add-board'}}")
| {{_ 'add-board'}}
each boards
li.js-board(class="{{_id}} {{#if isStarred}}starred{{/if}} {{colorClass}} {{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}", draggable="true")
if isInvited
.board-list-item
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.details
span.board-list-item-name= title
span.js-star-board(
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
title="{{_ 'star-board-title'}}")
| {{#if isStarred}}⭐{{else}}☆{{/if}}
p.board-list-item-desc {{_ 'just-invited'}}
button.js-accept-invite.primary {{_ 'accept'}}
button.js-decline-invite {{_ 'decline'}}
else
if $eq type "template-container"
.template-container.board-list-item
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.board-handle(title="{{_ 'drag-board'}}") ↕️
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
span.details
span.board-list-item-name(title="{{_ 'template-container'}}")
+viewer
= title
p.board-list-item-desc
+viewer
= description
if hasSpentTimeCards
span.js-has-spenttime-cards(
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
| ⏱️
span.js-star-board(
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
title="{{_ 'star-board-title'}}")
| {{#if isStarred}}⭐{{else}}☆{{/if}}
else
.board-list-item
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.board-handle(title="{{_ 'drag-board'}}") ↕️
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
span.details
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
+viewer
= title
unless currentSetting.hideBoardMemberList
if allowsBoardMemberList
.minicard-members
each member in boardMembers _id
a.name
+userAvatar(userId=member noRemove=true)
unless currentSetting.hideCardCounterList
if allowsCardCounterList
.minicard-lists.flex.flex-wrap
each list in boardLists _id
.item
| {{ list }}
p.board-list-item-desc
+viewer
= description
if hasSpentTimeCards
span.js-has-spenttime-cards(
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
| ⏱️
a.js-star-board(
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
title="{{_ 'star-board-title'}}")
| {{#if isStarred}}⭐{{else}}☆{{/if}}
template(name="boardListHeaderBar")
h1 {{_ title }}
@ -154,3 +166,25 @@ template(name="boardListHeaderBar")
// a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
// i.fa.fa-clone
// span {{_ 'templates'}}
// Recursive template for workspaces tree
template(name="workspaceTree")
if nodes
ul.workspace-tree.js-workspace-tree
each nodes
li.workspace-node(class="{{#if $eq id selectedWorkspaceId}}active{{/if}}" data-workspace-id="{{id}}" draggable="true")
.workspace-node-content
span.workspace-drag-handle ↕️
a.js-select-workspace(data-id="{{id}}")
span.workspace-icon
if icon
+viewer
= icon
else
| 📁
span.workspace-name= name
a.js-edit-workspace(data-id="{{id}}" title="{{_ 'allboards.edit-workspace'}}") ✏️
span.workspace-count {{workspaceCount id}}
a.js-add-subworkspace(data-id="{{id}}" title="{{_ 'allboards.add-subworkspace'}}") +
if children
+workspaceTree(nodes=children selectedWorkspaceId=selectedWorkspaceId)

View file

@ -14,6 +14,9 @@ Template.boardList.helpers({
return Utils.isMiniScreen() && Session.get('currentBoard'); */
return true;
},
BoardMultiSelection() {
return BoardMultiSelection;
},
})
Template.boardListHeaderBar.events({
@ -45,6 +48,9 @@ BlazeComponent.extendComponent({
onCreated() {
Meteor.subscribe('setting');
Meteor.subscribe('tableVisibilityModeSettings');
this.selectedMenu = new ReactiveVar('starred');
this.selectedWorkspaceIdVar = new ReactiveVar(null);
this.workspacesTreeVar = new ReactiveVar([]);
let currUser = ReactiveCache.getCurrentUser();
let userLanguage;
if (currUser && currUser.profile) {
@ -53,9 +59,72 @@ BlazeComponent.extendComponent({
if (userLanguage) {
TAPi18n.setLanguage(userLanguage);
}
// Load workspaces tree reactively
this.autorun(() => {
const u = ReactiveCache.getCurrentUser();
const tree = (u && u.profile && u.profile.boardWorkspacesTree) || [];
this.workspacesTreeVar.set(tree);
});
},
reorderWorkspaces(draggedSpaceId, targetSpaceId) {
const tree = this.workspacesTreeVar.get();
// Helper to remove a space from tree
const removeSpace = (nodes, id) => {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].id === id) {
const removed = nodes.splice(i, 1)[0];
return { tree: nodes, removed };
}
if (nodes[i].children) {
const result = removeSpace(nodes[i].children, id);
if (result.removed) {
return { tree: nodes, removed: result.removed };
}
}
}
return { tree: nodes, removed: null };
};
// Helper to insert a space after target
const insertAfter = (nodes, targetId, spaceToInsert) => {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].id === targetId) {
nodes.splice(i + 1, 0, spaceToInsert);
return true;
}
if (nodes[i].children) {
if (insertAfter(nodes[i].children, targetId, spaceToInsert)) {
return true;
}
}
}
return false;
};
// Clone the tree
const newTree = EJSON.clone(tree);
// Remove the dragged space
const { tree: treeAfterRemoval, removed } = removeSpace(newTree, draggedSpaceId);
if (removed) {
// Insert after target
insertAfter(treeAfterRemoval, targetSpaceId, removed);
// Save the new tree
Meteor.call('setWorkspacesTree', treeAfterRemoval, (err) => {
if (err) console.error(err);
});
}
},
onRendered() {
// jQuery sortable is disabled in favor of HTML5 drag-and-drop for space management
// The old sortable code has been removed to prevent conflicts
/* OLD SORTABLE CODE - DISABLED
const itemsSelector = '.js-board:not(.placeholder)';
const $boards = this.$('.js-boards');
@ -73,27 +142,20 @@ BlazeComponent.extendComponent({
EscapeActions.executeUpTo('popup-close');
},
stop(evt, ui) {
// To attribute the new index number, we need to get the DOM element
// of the previous and the following card -- if any.
const prevBoardDom = ui.item.prev('.js-board').get(0);
const nextBoardBom = ui.item.next('.js-board').get(0);
const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardBom, 1);
const nextBoardDom = ui.item.next('.js-board').get(0);
const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardDom, 1);
const boardDomElement = ui.item.get(0);
const board = Blaze.getData(boardDomElement);
// Normally the jquery-ui sortable library moves the dragged DOM element
// to its new position, which disrupts Blaze reactive updates mechanism
// (especially when we move the last card of a list, or when multiple
// users move some cards at the same time). To prevent these UX glitches
// we ask sortable to gracefully cancel the move, and to put back the
// DOM in its initial state. The card move is then handled reactively by
// Blaze with the below query.
$boards.sortable('cancel');
board.move(sortIndex.base);
const currentUser = ReactiveCache.getCurrentUser();
if (currentUser && typeof currentUser.setBoardSortIndex === 'function') {
currentUser.setBoardSortIndex(board._id, sortIndex.base);
}
},
});
// Disable drag-dropping if the current user is not a board member or is comment only
this.autorun(() => {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$boards.sortable({
@ -101,6 +163,7 @@ BlazeComponent.extendComponent({
});
}
});
*/
},
userHasTeams() {
if (ReactiveCache.getCurrentUser()?.teams?.length > 0)
@ -132,6 +195,41 @@ BlazeComponent.extendComponent({
const ret = this.userHasOrgs() || this.userHasTeams();
return ret;
},
currentMenuPath() {
const sel = this.selectedMenu.get();
const currentUser = ReactiveCache.getCurrentUser();
// Helper to find space by id in tree
const findSpaceById = (nodes, targetId, path = []) => {
for (const node of nodes) {
if (node.id === targetId) {
return [...path, node];
}
if (node.children && node.children.length > 0) {
const result = findSpaceById(node.children, targetId, [...path, node]);
if (result) return result;
}
}
return null;
};
if (sel === 'starred') {
return { icon: '⭐', text: TAPi18n.__('allboards.starred') };
} else if (sel === 'templates') {
return { icon: '📋', text: TAPi18n.__('allboards.templates') };
} else if (sel === 'remaining') {
return { icon: '📂', text: TAPi18n.__('allboards.remaining') };
} else {
// sel is a workspaceId, build path
const tree = this.workspacesTreeVar.get();
const spacePath = findSpaceById(tree, sel);
if (spacePath && spacePath.length > 0) {
const pathText = spacePath.map(s => s.name).join(' / ');
return { icon: '🗂️', text: `${TAPi18n.__('allboards.workspaces')} / ${pathText}` };
}
return { icon: '🗂️', text: TAPi18n.__('allboards.workspaces') };
}
},
boards() {
let query = {
// { type: 'board' },
@ -184,10 +282,33 @@ BlazeComponent.extendComponent({
};
}
const ret = ReactiveCache.getBoards(query, {
sort: { sort: 1 /* boards default sorting */ },
});
return ret;
const boards = ReactiveCache.getBoards(query, {});
const currentUser = ReactiveCache.getCurrentUser();
let list = boards;
// Apply left menu filtering
const sel = this.selectedMenu.get();
const assignments = (currentUser && currentUser.profile && currentUser.profile.boardWorkspaceAssignments) || {};
if (sel === 'starred') {
list = list.filter(b => currentUser && currentUser.hasStarred(b._id));
} else if (sel === 'templates') {
list = list.filter(b => b.type === 'template-container');
} else if (sel === 'remaining') {
// Show boards not in any workspace AND not templates
// Keep starred boards visible in Remaining too
list = list.filter(b =>
!assignments[b._id] &&
b.type !== 'template-container'
);
} else {
// assume sel is a workspaceId
// Keep starred boards visible in their workspace too
list = list.filter(b => assignments[b._id] === sel);
}
if (currentUser && typeof currentUser.sortBoardsForUser === 'function') {
return currentUser.sortBoardsForUser(list);
}
return list.slice().sort((a, b) => (a.title || '').localeCompare(b.title || ''));
},
boardLists(boardId) {
/* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
@ -235,11 +356,65 @@ BlazeComponent.extendComponent({
events() {
return [
{
'click .js-add-board': Popup.open('createBoard'),
'click .js-star-board'(evt) {
const boardId = this.currentData()._id;
ReactiveCache.getCurrentUser().toggleBoardStar(boardId);
'click .js-select-menu'(evt) {
const type = evt.currentTarget.getAttribute('data-type');
this.selectedWorkspaceIdVar.set(null);
this.selectedMenu.set(type);
},
'click .js-select-workspace'(evt) {
const id = evt.currentTarget.getAttribute('data-id');
this.selectedWorkspaceIdVar.set(id);
this.selectedMenu.set(id);
},
'click .js-add-workspace'(evt) {
evt.preventDefault();
const name = prompt(TAPi18n.__('allboards.add-workspace-prompt') || 'New Space name');
if (name && name.trim()) {
Meteor.call('createWorkspace', { parentId: null, name: name.trim() }, (err, res) => {
if (err) console.error(err);
});
}
},
'click .js-add-board'(evt) {
// Store the currently selected workspace/menu for board creation
const selectedWorkspaceId = this.selectedWorkspaceIdVar.get();
const selectedMenu = this.selectedMenu.get();
if (selectedWorkspaceId) {
Session.set('createBoardInWorkspace', selectedWorkspaceId);
} else {
Session.set('createBoardInWorkspace', null);
}
// Open different popup based on context
if (selectedMenu === 'templates') {
Popup.open('createTemplateContainer')(evt);
} else {
Popup.open('createBoard')(evt);
}
},
'click .js-star-board'(evt) {
evt.preventDefault();
evt.stopPropagation();
const boardId = this.currentData()._id;
if (boardId) {
Meteor.call('toggleBoardStar', boardId);
}
},
// HTML5 DnD from boards to spaces
'dragstart .js-board'(evt) {
const boardId = this.currentData()._id;
// Support multi-drag
if (BoardMultiSelection.isActive() && BoardMultiSelection.isSelected(boardId)) {
const selectedIds = BoardMultiSelection.getSelectedBoardIds();
try {
evt.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(selectedIds));
evt.originalEvent.dataTransfer.setData('application/x-board-multi', 'true');
} catch (e) {}
} else {
try { evt.originalEvent.dataTransfer.setData('text/plain', boardId); } catch (e) {}
}
},
'click .js-clone-board'(evt) {
if (confirm(TAPi18n.__('duplicate-board-confirm'))) {
@ -290,6 +465,58 @@ BlazeComponent.extendComponent({
}
});
},
'click .js-multiselection-activate'(evt) {
evt.preventDefault();
if (BoardMultiSelection.isActive()) {
BoardMultiSelection.disable();
} else {
BoardMultiSelection.activate();
}
},
'click .js-multiselection-reset'(evt) {
evt.preventDefault();
BoardMultiSelection.disable();
},
'click .js-toggle-board-multi-selection'(evt) {
evt.preventDefault();
evt.stopPropagation();
const boardId = this.currentData()._id;
BoardMultiSelection.toogle(boardId);
},
'click .js-archive-selected-boards'(evt) {
evt.preventDefault();
const selectedBoards = BoardMultiSelection.getSelectedBoardIds();
if (selectedBoards.length > 0 && confirm(TAPi18n.__('archive-board-confirm'))) {
selectedBoards.forEach(boardId => {
Meteor.call('archiveBoard', boardId);
});
BoardMultiSelection.reset();
}
},
'click .js-duplicate-selected-boards'(evt) {
evt.preventDefault();
const selectedBoards = BoardMultiSelection.getSelectedBoardIds();
if (selectedBoards.length > 0 && confirm(TAPi18n.__('duplicate-board-confirm'))) {
selectedBoards.forEach(boardId => {
const board = ReactiveCache.getBoard(boardId);
if (board) {
Meteor.call(
'copyBoard',
boardId,
{
sort: ReactiveCache.getBoards({ archived: false }).length,
type: 'board',
title: board.title,
},
(err, res) => {
if (err) console.error(err);
}
);
}
});
BoardMultiSelection.reset();
}
},
'click #resetBtn'(event) {
let allBoards = document.getElementsByClassName("js-board");
let currBoard;
@ -356,7 +583,260 @@ BlazeComponent.extendComponent({
}
}
},
'click .js-edit-workspace'(evt) {
evt.preventDefault();
evt.stopPropagation();
const workspaceId = evt.currentTarget.getAttribute('data-id');
// Find the space in the tree
const findSpace = (nodes, id) => {
for (const node of nodes) {
if (node.id === id) return node;
if (node.children) {
const found = findSpace(node.children, id);
if (found) return found;
}
}
return null;
};
const tree = this.workspacesTreeVar.get();
const space = findSpace(tree, workspaceId);
if (space) {
const newName = prompt(TAPi18n.__('allboards.edit-workspace-name') || 'Space name:', space.name);
const newIcon = prompt(TAPi18n.__('allboards.edit-workspace-icon') || 'Space icon (markdown):', space.icon || '📁');
if (newName !== null && newName.trim()) {
// Update space in tree
const updateSpaceInTree = (nodes, id, updates) => {
return nodes.map(node => {
if (node.id === id) {
return { ...node, ...updates };
}
if (node.children) {
return { ...node, children: updateSpaceInTree(node.children, id, updates) };
}
return node;
});
};
const updatedTree = updateSpaceInTree(tree, workspaceId, {
name: newName.trim(),
icon: newIcon || '📁'
});
Meteor.call('setWorkspacesTree', updatedTree, (err) => {
if (err) console.error(err);
});
}
}
},
'click .js-add-subworkspace'(evt) {
evt.preventDefault();
evt.stopPropagation();
const parentId = evt.currentTarget.getAttribute('data-id');
const name = prompt(TAPi18n.__('allboards.add-subworkspace-prompt') || 'Subspace name:');
if (name && name.trim()) {
Meteor.call('createWorkspace', { parentId, name: name.trim() }, (err) => {
if (err) console.error(err);
});
}
},
'dragstart .workspace-node'(evt) {
const workspaceId = evt.currentTarget.getAttribute('data-workspace-id');
evt.originalEvent.dataTransfer.effectAllowed = 'move';
evt.originalEvent.dataTransfer.setData('application/x-workspace-id', workspaceId);
// Create a better drag image
const dragImage = evt.currentTarget.cloneNode(true);
dragImage.style.position = 'absolute';
dragImage.style.top = '-9999px';
dragImage.style.opacity = '0.8';
document.body.appendChild(dragImage);
evt.originalEvent.dataTransfer.setDragImage(dragImage, 0, 0);
setTimeout(() => document.body.removeChild(dragImage), 0);
evt.currentTarget.classList.add('dragging');
},
'dragend .workspace-node'(evt) {
evt.currentTarget.classList.remove('dragging');
document.querySelectorAll('.workspace-node').forEach(el => {
el.classList.remove('drag-over');
});
},
'dragover .workspace-node'(evt) {
evt.preventDefault();
evt.stopPropagation();
const draggingEl = document.querySelector('.workspace-node.dragging');
const targetEl = evt.currentTarget;
// Allow dropping boards on any space
// Or allow dropping spaces on other spaces (but not on itself or descendants)
if (!draggingEl || (targetEl !== draggingEl && !draggingEl.contains(targetEl))) {
evt.originalEvent.dataTransfer.dropEffect = 'move';
targetEl.classList.add('drag-over');
}
},
'dragleave .workspace-node'(evt) {
evt.currentTarget.classList.remove('drag-over');
},
'drop .workspace-node'(evt) {
evt.preventDefault();
evt.stopPropagation();
const targetEl = evt.currentTarget;
targetEl.classList.remove('drag-over');
// Check what's being dropped - board or workspace
const draggedWorkspaceId = evt.originalEvent.dataTransfer.getData('application/x-workspace-id');
const isMultiBoard = evt.originalEvent.dataTransfer.getData('application/x-board-multi');
const boardData = evt.originalEvent.dataTransfer.getData('text/plain');
if (draggedWorkspaceId && !boardData) {
// This is a workspace reorder operation
const targetWorkspaceId = targetEl.getAttribute('data-workspace-id');
if (draggedWorkspaceId !== targetWorkspaceId) {
this.reorderWorkspaces(draggedWorkspaceId, targetWorkspaceId);
}
} else if (boardData) {
// This is a board assignment operation
// Get the workspace ID directly from the dropped workspace-node's data-workspace-id attribute
const workspaceId = targetEl.getAttribute('data-workspace-id');
if (workspaceId) {
if (isMultiBoard) {
// Multi-board drag
try {
const boardIds = JSON.parse(boardData);
boardIds.forEach(boardId => {
Meteor.call('assignBoardToWorkspace', boardId, workspaceId);
});
} catch (e) {
// Error parsing multi-board data
}
} else {
// Single board drag
Meteor.call('assignBoardToWorkspace', boardData, workspaceId);
}
}
}
},
'dragover .js-select-menu'(evt) {
evt.preventDefault();
evt.stopPropagation();
const menuType = evt.currentTarget.getAttribute('data-type');
// Only allow drop on "remaining" menu to unassign boards from spaces
if (menuType === 'remaining') {
evt.originalEvent.dataTransfer.dropEffect = 'move';
evt.currentTarget.classList.add('drag-over');
}
},
'dragleave .js-select-menu'(evt) {
evt.currentTarget.classList.remove('drag-over');
},
'drop .js-select-menu'(evt) {
evt.preventDefault();
evt.stopPropagation();
const menuType = evt.currentTarget.getAttribute('data-type');
evt.currentTarget.classList.remove('drag-over');
// Only handle drops on "remaining" menu
if (menuType !== 'remaining') return;
const isMultiBoard = evt.originalEvent.dataTransfer.getData('application/x-board-multi');
const boardData = evt.originalEvent.dataTransfer.getData('text/plain');
if (boardData) {
if (isMultiBoard) {
// Multi-board drag - unassign all from workspaces
try {
const boardIds = JSON.parse(boardData);
boardIds.forEach(boardId => {
Meteor.call('unassignBoardFromWorkspace', boardId);
});
} catch (e) {
// Error parsing multi-board data
}
} else {
// Single board drag - unassign from workspace
Meteor.call('unassignBoardFromWorkspace', boardData);
}
}
},
},
];
},
// Helpers for templates
workspacesTree() {
return this.workspacesTreeVar.get();
},
selectedWorkspaceId() {
return this.selectedWorkspaceIdVar.get();
},
isSelectedMenu(type) {
return this.selectedMenu.get() === type;
},
isSpaceSelected(id) {
return this.selectedWorkspaceIdVar.get() === id;
},
menuItemCount(type) {
const currentUser = ReactiveCache.getCurrentUser();
const assignments = (currentUser && currentUser.profile && currentUser.profile.boardWorkspaceAssignments) || {};
// Get all boards for counting
let query = {
$and: [
{ archived: false },
{ type: { $in: ['board', 'template-container'] } },
{ $or: [{ 'members.userId': Meteor.userId() }] },
{ title: { $not: { $regex: /^\^.*\^$/ } } }
]
};
const allBoards = ReactiveCache.getBoards(query, {});
if (type === 'starred') {
return allBoards.filter(b => currentUser && currentUser.hasStarred(b._id)).length;
} else if (type === 'templates') {
return allBoards.filter(b => b.type === 'template-container').length;
} else if (type === 'remaining') {
// Count boards not in any workspace AND not templates
// Include starred boards (they appear in both Starred and Remaining)
return allBoards.filter(b =>
!assignments[b._id] &&
b.type !== 'template-container'
).length;
}
return 0;
},
workspaceCount(workspaceId) {
const currentUser = ReactiveCache.getCurrentUser();
const assignments = (currentUser && currentUser.profile && currentUser.profile.boardWorkspaceAssignments) || {};
// Get all boards for counting
let query = {
$and: [
{ archived: false },
{ type: { $in: ['board', 'template-container'] } },
{ $or: [{ 'members.userId': Meteor.userId() }] },
{ title: { $not: { $regex: /^\^.*\^$/ } } }
]
};
const allBoards = ReactiveCache.getBoards(query, {});
// Count boards directly assigned to this space (not including children)
return allBoards.filter(b => assignments[b._id] === workspaceId).length;
},
canModifyBoards() {
const currentUser = ReactiveCache.getCurrentUser();
return currentUser && !currentUser.isCommentOnly();
},
hasBoardsSelected() {
return BoardMultiSelection.count() > 0;
},
}).register('boardList');

View file

@ -86,17 +86,17 @@ template(name="attachmentGallery")
= name
span.file-size ({{fileSize size}})
.attachment-actions
a.js-download(href="{{link}}?download=true", download="{{name}}")
| ⬇️(title="{{_ 'download'}}")
a.js-download(href="{{link}}?download=true", download="{{name}}", title="{{_ 'download'}}")
| ⬇️
if currentUser.isBoardMember
unless currentUser.isCommentOnly
unless currentUser.isWorker
a.js-rename
| ✏️(title="{{_ 'rename'}}")
a.js-confirm-delete
| 🗑️(title="{{_ 'delete'}}")
a.js-open-attachment-menu
| ☰(data-attachment-link="{{link}}" title="{{_ 'attachmentActionsPopup-title'}}")
a.js-rename(title="{{_ 'rename'}}")
| ✏️
a.js-confirm-delete(title="{{_ 'delete'}}")
| 🗑️
a.js-open-attachment-menu(data-attachment-link="{{link}}", title="{{_ 'attachmentActionsPopup-title'}}")
| ☰
// Migration spinner overlay
if isAttachmentMigrating _id

View file

@ -343,7 +343,7 @@ export function handleFileUpload(card, files) {
}
// Check if user can modify the card
if (!card.canModifyCard()) {
if (!Utils.canModifyCard()) {
if (process.env.DEBUG === 'true') {
console.warn('User does not have permission to modify this card');
}

View file

@ -1,8 +1,10 @@
import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker';
import { ReactiveCache } from '/imports/reactiveCache';
import {
formatDateTime,
formatDate,
formatDateByUserPreference,
formatTime,
getISOWeek,
isValidDate,
@ -168,11 +170,18 @@ CardCustomField.register('cardCustomField');
}
showWeekOfYear() {
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
const user = ReactiveCache.getCurrentUser();
if (!user) {
// For non-logged-in users, week of year is not shown
return false;
}
return user.isShowWeekOfYear();
}
showDate() {
return calendar(this.date.get());
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
showISODate() {

View file

@ -8,76 +8,112 @@
.card-date.is-active {
background-color: #b3b3b3;
}
.card-date.current,
.card-date.almost-due,
.card-date.due,
.card-date.long-overdue {
/* Date status colors - red = overdue, amber = due soon, no shade = not due */
.card-date.overdue {
background-color: #ff4444; /* Red for overdue */
color: #fff;
}
.card-date.overdue:hover,
.card-date.overdue.is-active {
background-color: #cc3333;
}
.card-date.due-soon {
background-color: #ffaa00; /* Amber for due soon */
color: #000;
}
.card-date.due-soon:hover,
.card-date.due-soon.is-active {
background-color: #e69900;
}
.card-date.not-due {
/* No special background - uses default date type colors */
}
.card-date.current {
background-color: #5ba639;
background-color: #5ba639; /* Green for current/active */
color: #fff;
}
.card-date.current:hover,
.card-date.current.is-active {
background-color: #46802c;
}
.card-date.almost-due {
background-color: #edc909;
.card-date.completed {
background-color: #90ee90; /* Light green for completed */
color: #000;
}
.card-date.almost-due:hover,
.card-date.almost-due.is-active {
background-color: #bc9f07;
.card-date.completed:hover,
.card-date.completed.is-active {
background-color: #7dd87d;
}
.card-date.due {
background-color: #fa3f00;
.card-date.completed-early {
background-color: #4caf50; /* Green for completed early */
color: #fff;
}
.card-date.due:hover,
.card-date.due.is-active {
background-color: #c73200;
.card-date.completed-early:hover,
.card-date.completed-early.is-active {
background-color: #45a049;
}
.card-date.long-overdue {
background-color: #fd5d47;
.card-date.completed-late {
background-color: #ff9800; /* Orange for completed late */
color: #fff;
}
.card-date.long-overdue:hover,
.card-date.long-overdue.is-active {
background-color: #fd3e24;
.card-date.completed-late:hover,
.card-date.completed-late.is-active {
background-color: #f57c00;
}
.card-date.completed-on-time {
background-color: #2196f3; /* Blue for completed on time */
color: #fff;
}
.card-date.completed-on-time:hover,
.card-date.completed-on-time.is-active {
background-color: #1976d2;
}
/* Date type specific colors */
.card-date.received-date {
background-color: #0079bf; /* Blue for received */
background-color: #dbdbdb; /* Light grey for received */
}
.card-date.received-date:hover,
.card-date.received-date.is-active {
background-color: #005a8b;
background-color: #b3b3b3;
}
.card-date.start-date {
background-color: #3cb500; /* Green for start */
background-color: #90ee90; /* Light green for start */
color: #000; /* Black text for start */
}
.card-date.start-date:hover,
.card-date.start-date.is-active {
background-color: #2d8f00;
background-color: #7dd87d;
}
.card-date.due-date {
background-color: #ff9f19; /* Orange for due */
background-color: #ffd700; /* Yellow for due */
color: #000; /* Black text for due */
}
.card-date.due-date:hover,
.card-date.due-date.is-active {
background-color: #e68a00;
background-color: #e6c200;
}
.card-date.end-date {
background-color: #a632db; /* Purple for end */
background-color: #ffb3b3; /* Light red for end */
color: #000; /* Black text for end */
}
.card-date.end-date:hover,
.card-date.end-date.is-active {
background-color: #8a2bb8;
background-color: #ff9999;
}
.card-date.end-date time::before {
content: "🏁"; /* Finish flag - represents end/completion */
@ -94,7 +130,7 @@
/* Generic date badge and custom field date */
.card-date:not(.received-date):not(.start-date):not(.due-date):not(.end-date) time::before {
content: "📅"; /* Calendar - represents generic date */
/*content: "📅"; // Calendar - represents generic date */
}
.card-date time::before {
font-size: inherit;

View file

@ -24,14 +24,14 @@ template(name="dateCustomField")
template(name="minicardReceivedDate")
if canModifyCard
a.js-edit-date.card-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
a.js-edit-date.card-date.received-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
a.card-date.received-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
@ -40,14 +40,14 @@ template(name="minicardReceivedDate")
template(name="minicardStartDate")
if canModifyCard
a.js-edit-date.card-date(title="{{_ 'card-start'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
a.js-edit-date.card-date.start-date(title="{{_ 'card-start'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{_ 'card-start'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
a.card-date.start-date(title="{{_ 'card-start'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
@ -56,14 +56,14 @@ template(name="minicardStartDate")
template(name="minicardDueDate")
if canModifyCard
a.js-edit-date.card-date(title="{{_ 'card-due'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
a.js-edit-date.card-date.due-date(title="{{_ 'card-due'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{_ 'card-due'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
a.card-date.due-date(title="{{_ 'card-due'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
@ -72,14 +72,14 @@ template(name="minicardDueDate")
template(name="minicardEndDate")
if canModifyCard
a.js-edit-date.card-date(title="{{_ 'card-end'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
a.js-edit-date.card-date.end-date(title="{{_ 'card-end'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{_ 'card-end'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
a.card-date.end-date(title="{{_ 'card-end'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
@ -93,3 +93,60 @@ template(name="minicardCustomFieldDate")
if showWeekOfYear
b
| {{showWeek}}
template(name="editCardReceivedDatePopup")
form.edit-card-received-date
.datepicker
// Date input field (existing)
// Insert calendar selector right after date input
.calendar-selector
label(for="calendar-received") 🗓️
input#calendar-received.js-calendar-date(type="date")
// Time input field (if present)
.clear-date
a.js-clear-date {{_ 'clear'}}
.datepicker-actions
button.primary.wide.left(type="submit") {{_ 'save'}}
button.js-delete-date.negate.wide.right {{_ 'delete'}}
template(name="editCardStartDatePopup")
form.edit-card-start-date
.datepicker
// Date input field (existing)
.calendar-selector
label(for="calendar-start") 🗓️
input#calendar-start.js-calendar-date(type="date")
// Time input field (if present)
.clear-date
a.js-clear-date {{_ 'clear'}}
.datepicker-actions
button.primary.wide.left(type="submit") {{_ 'save'}}
button.js-delete-date.negate.wide.right {{_ 'delete'}}
template(name="editCardDueDatePopup")
form.edit-card-due-date
.datepicker
// Date input field (existing)
.calendar-selector
label(for="calendar-due") 🗓️
input#calendar-due.js-calendar-date(type="date")
// Time input field (if present)
.clear-date
a.js-clear-date {{_ 'clear'}}
.datepicker-actions
button.primary.wide.left(type="submit") {{_ 'save'}}
button.js-delete-date.negate.wide.right {{_ 'delete'}}
template(name="editCardEndDatePopup")
form.edit-card-end-date
.datepicker
// Date input field (existing)
.calendar-selector
label(for="calendar-end") 🗓️
input#calendar-end.js-calendar-date(type="date")
// Time input field (if present)
.clear-date
a.js-clear-date {{_ 'clear'}}
.datepicker-actions
button.primary.wide.left(type="submit") {{_ 'save'}}
button.js-delete-date.negate.wide.right {{_ 'delete'}}

View file

@ -3,6 +3,7 @@ import { DatePicker } from '/client/lib/datepicker';
import {
formatDateTime,
formatDate,
formatDateByUserPreference,
formatTime,
getISOWeek,
isValidDate,
@ -18,7 +19,8 @@ import {
now,
createDate,
fromNow,
calendar
calendar,
diff
} from '/imports/lib/dateUtils';
// editCardReceivedDatePopup
@ -47,12 +49,18 @@ import {
onRendered() {
super.onRendered();
if (moment.isDate(this.card.getReceived())) {
this.$('.js-datepicker').datepicker(
'setStartDate',
this.card.getReceived(),
);
}
// DatePicker base class handles initialization with native HTML inputs
const self = this;
this.$('.js-calendar-date').on('change', function(evt) {
const currentUser = ReactiveCache.getCurrentUser && ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const value = evt.target.value;
if (value) {
// Format date according to user preference
const formatted = formatDateByUserPreference(new Date(value), dateFormat, true);
self._storeDate(new Date(value));
}
});
}
_storeDate(date) {
@ -73,9 +81,7 @@ import {
onRendered() {
super.onRendered();
if (moment.isDate(this.card.getStart())) {
this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
}
// DatePicker base class handles initialization with native HTML inputs
}
_storeDate(date) {
@ -96,9 +102,7 @@ import {
onRendered() {
super.onRendered();
if (moment.isDate(this.card.getStart())) {
this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
}
// DatePicker base class handles initialization with native HTML inputs
}
_storeDate(date) {
@ -130,11 +134,18 @@ const CardDate = BlazeComponent.extendComponent({
},
showWeekOfYear() {
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
const user = ReactiveCache.getCurrentUser();
if (!user) {
// For non-logged-in users, week of year is not shown
return false;
}
return user.isShowWeekOfYear();
},
showDate() {
return calendar(this.date.get());
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
},
showISODate() {
@ -157,21 +168,26 @@ class CardReceivedDate extends CardDate {
const endAt = this.data().getEnd();
const startAt = this.data().getStart();
const theDate = this.date.get();
// if dueAt, endAt and startAt exist & are > receivedAt, receivedAt doesn't need to be flagged
const now = this.now.get();
// Received date logic: if received date is after start, due, or end dates, it's overdue
if (
(startAt && theDate.isAfter(startAt)) ||
(endAt && theDate.isAfter(endAt)) ||
(dueAt && theDate.isAfter(dueAt))
)
classes += 'long-overdue';
else classes += 'current';
(startAt && isAfter(theDate, startAt)) ||
(endAt && isAfter(theDate, endAt)) ||
(dueAt && isAfter(theDate, dueAt))
) {
classes += 'overdue';
} else {
classes += 'not-due';
}
return classes;
}
showTitle() {
return `${TAPi18n.__('card-received-on')} ${this.date
.get()
.format('LLLL')}`;
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
}
events() {
@ -192,21 +208,30 @@ class CardStartDate extends CardDate {
}
classes() {
let classes = 'start-date' + ' ';
let classes = 'start-date ';
const dueAt = this.data().getDue();
const endAt = this.data().getEnd();
const theDate = this.date.get();
const now = this.now.get();
// if dueAt or endAt exist & are > startAt, startAt doesn't need to be flagged
if ((endAt && theDate.isAfter(endAt)) || (dueAt && theDate.isAfter(dueAt)))
classes += 'long-overdue';
else if (theDate.isAfter(now)) classes += '';
else classes += 'current';
// Start date logic: if start date is after due or end dates, it's overdue
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
classes += 'overdue';
} else if (isAfter(theDate, now)) {
// Start date is in the future - not due yet
classes += 'not-due';
} else {
// Start date is today or in the past - current/active
classes += 'current';
}
return classes;
}
showTitle() {
return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
}
events() {
@ -227,22 +252,43 @@ class CardDueDate extends CardDate {
}
classes() {
let classes = 'due-date' + ' ';
let classes = 'due-date ';
const endAt = this.data().getEnd();
const theDate = this.date.get();
const now = this.now.get();
// if the due date is after the end date, green - done early
if (endAt && theDate.isAfter(endAt)) classes += 'current';
// if there is an end date, don't need to flag the due date
else if (endAt) classes += '';
else if (now.diff(theDate, 'days') >= 2) classes += 'long-overdue';
else if (now.diff(theDate, 'minute') >= 0) classes += 'due';
else if (now.diff(theDate, 'days') >= -1) classes += 'almost-due';
// If there's an end date and it's before the due date, task is completed early
if (endAt && isBefore(endAt, theDate)) {
classes += 'completed-early';
}
// If there's an end date, don't show due date status since task is completed
else if (endAt) {
classes += 'completed';
}
// Due date logic based on current time
else {
const daysDiff = diff(theDate, now, 'days');
if (daysDiff < 0) {
// Due date is in the past - overdue
classes += 'overdue';
} else if (daysDiff <= 1) {
// Due today or tomorrow - due soon
classes += 'due-soon';
} else {
// Due date is more than 1 day away - not due yet
classes += 'not-due';
}
}
return classes;
}
showTitle() {
return `${TAPi18n.__('card-due-on')} ${this.date.get().format('LLLL')}`;
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${TAPi18n.__('card-due-on')} ${formattedDate}`;
}
events() {
@ -263,17 +309,28 @@ class CardEndDate extends CardDate {
}
classes() {
let classes = 'end-date' + ' ';
let classes = 'end-date ';
const dueAt = this.data().getDue();
const theDate = this.date.get();
if (!dueAt) classes += '';
else if (theDate.isBefore(dueAt)) classes += 'current';
else if (theDate.isAfter(dueAt)) classes += 'due';
if (!dueAt) {
// No due date set - just show as completed
classes += 'completed';
} else if (isBefore(theDate, dueAt)) {
// End date is before due date - completed early
classes += 'completed-early';
} else if (isAfter(theDate, dueAt)) {
// End date is after due date - completed late
classes += 'completed-late';
} else {
// End date equals due date - completed on time
classes += 'completed-on-time';
}
return classes;
}
showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`;
}
events() {
@ -302,7 +359,12 @@ class CardCustomFieldDate extends CardDate {
}
showWeekOfYear() {
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
const user = ReactiveCache.getCurrentUser();
if (!user) {
// For non-logged-in users, week of year is not shown
return false;
}
return user.isShowWeekOfYear();
}
showDate() {
@ -315,7 +377,10 @@ class CardCustomFieldDate extends CardDate {
}
showTitle() {
return `${this.date.get().format('LLLL')}`;
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${formattedDate}`;
}
classes() {
@ -334,7 +399,9 @@ CardCustomFieldDate.register('cardCustomFieldDate');
}
showDate() {
return format(this.date.get(), 'L');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardReceivedDate'));
@ -344,7 +411,9 @@ CardCustomFieldDate.register('cardCustomFieldDate');
}
showDate() {
return format(this.date.get(), 'YYYY-MM-DD HH:mm');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardStartDate'));
@ -354,7 +423,9 @@ CardCustomFieldDate.register('cardCustomFieldDate');
}
showDate() {
return format(this.date.get(), 'YYYY-MM-DD HH:mm');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardDueDate'));
@ -364,7 +435,9 @@ CardCustomFieldDate.register('cardCustomFieldDate');
}
showDate() {
return format(this.date.get(), 'YYYY-MM-DD HH:mm');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardEndDate'));
@ -374,7 +447,9 @@ CardCustomFieldDate.register('cardCustomFieldDate');
}
showDate() {
return format(this.date.get(), 'L');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardCustomFieldDate'));
@ -391,7 +466,9 @@ class VoteEndDate extends CardDate {
return classes;
}
showDate() {
return format(this.date.get(), 'L') + ' ' + format(this.date.get(), 'HH:mm');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().toLocaleString()}`;
@ -418,10 +495,12 @@ class PokerEndDate extends CardDate {
return classes;
}
showDate() {
return this.date.get().format('l LT');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`;
}
events() {

View file

@ -1,3 +1,31 @@
/* Date Format Selector */
.card-details-item-date-format {
margin-bottom: 10px;
}
.card-details-item-date-format .card-details-item-title {
font-size: 14px;
font-weight: bold;
margin-bottom: 5px;
color: #333;
}
.card-details-item-date-format .js-date-format-selector {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
font-size: 14px;
cursor: pointer;
}
.card-details-item-date-format .js-date-format-selector:focus {
outline: none;
border-color: #007cba;
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2);
}
.assignee {
border-radius: 3px;
display: block;

View file

@ -113,6 +113,16 @@ template(name="cardDetails")
if currentBoard.hasAnyAllowsDate
hr
.card-details-item.card-details-item-date-format
h3.card-details-item-title
| 📅
| {{_ 'date-format'}}
.card-details-item-content
select.js-date-format-selector
option(value="YYYY-MM-DD" selected="{{#if isDateFormat 'YYYY-MM-DD'}}selected{{/if}}") {{_ 'date-format-yyyy-mm-dd'}}
option(value="DD-MM-YYYY" selected="{{#if isDateFormat 'DD-MM-YYYY'}}selected{{/if}}") {{_ 'date-format-dd-mm-yyyy'}}
option(value="MM-DD-YYYY" selected="{{#if isDateFormat 'MM-DD-YYYY'}}selected{{/if}}") {{_ 'date-format-mm-dd-yyyy'}}
if currentBoard.allowsReceivedDate
.card-details-item.card-details-item-received
h3.card-details-item-title
@ -181,7 +191,7 @@ template(name="cardDetails")
if currentBoard.allowsMembers
.card-details-item.card-details-item-members
h3.card-details-item-title
| 👤s
| &#x1F465;
| {{_ 'members'}}
each userId in getMembers
+userAvatar(userId=userId cardId=_id)
@ -232,7 +242,7 @@ template(name="cardDetails")
if currentBoard.allowsAssignedBy
.card-details-item.card-details-item-name
h3.card-details-item-title
| 👤-plus
| ✍️
| {{_ 'assigned-by'}}
if canModifyCard
unless currentUser.isWorker

View file

@ -306,6 +306,10 @@ BlazeComponent.extendComponent({
const $tooltip = this.$('.card-details-header .copied-tooltip');
Utils.showCopied(promise, $tooltip);
},
'change .js-date-format-selector'(event) {
const dateFormat = event.target.value;
Meteor.call('changeDateFormat', dateFormat);
},
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
'submit .js-card-description'(event) {
event.preventDefault();
@ -426,56 +430,57 @@ BlazeComponent.extendComponent({
) {
newState = forIt;
}
this.data().setVote(Meteor.userId(), newState);
// Use secure server method; direct client updates to vote are blocked
Meteor.call('cards.vote', this.data()._id, newState);
},
'click .js-poker'(e) {
let newState = null;
if ($(e.target).hasClass('js-poker-vote-one')) {
newState = 'one';
this.data().setPoker(Meteor.userId(), newState);
Meteor.call('cards.pokerVote', this.data()._id, newState);
}
if ($(e.target).hasClass('js-poker-vote-two')) {
newState = 'two';
this.data().setPoker(Meteor.userId(), newState);
Meteor.call('cards.pokerVote', this.data()._id, newState);
}
if ($(e.target).hasClass('js-poker-vote-three')) {
newState = 'three';
this.data().setPoker(Meteor.userId(), newState);
Meteor.call('cards.pokerVote', this.data()._id, newState);
}
if ($(e.target).hasClass('js-poker-vote-five')) {
newState = 'five';
this.data().setPoker(Meteor.userId(), newState);
Meteor.call('cards.pokerVote', this.data()._id, newState);
}
if ($(e.target).hasClass('js-poker-vote-eight')) {
newState = 'eight';
this.data().setPoker(Meteor.userId(), newState);
Meteor.call('cards.pokerVote', this.data()._id, newState);
}
if ($(e.target).hasClass('js-poker-vote-thirteen')) {
newState = 'thirteen';
this.data().setPoker(Meteor.userId(), newState);
Meteor.call('cards.pokerVote', this.data()._id, newState);
}
if ($(e.target).hasClass('js-poker-vote-twenty')) {
newState = 'twenty';
this.data().setPoker(Meteor.userId(), newState);
Meteor.call('cards.pokerVote', this.data()._id, newState);
}
if ($(e.target).hasClass('js-poker-vote-forty')) {
newState = 'forty';
this.data().setPoker(Meteor.userId(), newState);
Meteor.call('cards.pokerVote', this.data()._id, newState);
}
if ($(e.target).hasClass('js-poker-vote-one-hundred')) {
newState = 'oneHundred';
this.data().setPoker(Meteor.userId(), newState);
Meteor.call('cards.pokerVote', this.data()._id, newState);
}
if ($(e.target).hasClass('js-poker-vote-unsure')) {
newState = 'unsure';
this.data().setPoker(Meteor.userId(), newState);
Meteor.call('cards.pokerVote', this.data()._id, newState);
}
},
'click .js-poker-finish'(e) {
if ($(e.target).hasClass('js-poker-finish')) {
e.preventDefault();
const now = formatDateTime(new Date());
this.data().setPokerEnd(now);
const now = new Date();
Meteor.call('cards.setPokerEnd', this.data()._id, now);
}
},
@ -483,9 +488,9 @@ BlazeComponent.extendComponent({
if ($(e.target).hasClass('js-poker-replay')) {
e.preventDefault();
this.currentCard = this.currentData();
this.currentCard.replayPoker();
this.data().unsetPokerEnd();
this.data().unsetPokerEstimation();
Meteor.call('cards.replayPoker', this.currentCard._id);
Meteor.call('cards.unsetPokerEnd', this.currentCard._id);
Meteor.call('cards.unsetPokerEstimation', this.currentCard._id);
}
},
'click .js-poker-estimation'(event) {
@ -496,63 +501,66 @@ BlazeComponent.extendComponent({
this.find('#pokerEstimation').value = '';
if (ruleTitle) {
this.data().setPokerEstimation(parseInt(ruleTitle, 10));
Meteor.call('cards.setPokerEstimation', this.data()._id, parseInt(ruleTitle, 10));
} else {
this.data().setPokerEstimation('');
Meteor.call('cards.unsetPokerEstimation', this.data()._id);
}
}
},
// Drag and drop file upload handlers
'dragover .js-card-details'(event) {
event.preventDefault();
event.stopPropagation();
// Only prevent default for file drags to avoid interfering with other drag operations
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
}
},
'dragenter .js-card-details'(event) {
event.preventDefault();
event.stopPropagation();
const card = this.data();
const board = card.board();
// Only allow drag-and-drop if user can modify card and board allows attachments
if (card.canModifyCard() && board && board.allowsAttachments) {
// Check if the drag contains files
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
const card = this.data();
const board = card.board();
// Only allow drag-and-drop if user can modify card and board allows attachments
if (Utils.canModifyCard() && board && board.allowsAttachments) {
$(event.currentTarget).addClass('is-dragging-over');
}
}
},
'dragleave .js-card-details'(event) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
}
},
'drop .js-card-details'(event) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
const card = this.data();
const board = card.board();
// Check permissions
if (!card.canModifyCard() || !board || !board.allowsAttachments) {
return;
}
// Check if this is a file drop (not a checklist item reorder)
const dataTransfer = event.originalEvent.dataTransfer;
if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) {
return;
}
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
// Check if the drop contains files (not just text/HTML)
if (!dataTransfer.types.includes('Files')) {
return;
}
const card = this.data();
const board = card.board();
const files = dataTransfer.files;
if (files && files.length > 0) {
handleFileUpload(card, files);
// Check permissions
if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
return;
}
// Check if this is a file drop (not a checklist item reorder)
if (!dataTransfer.files || dataTransfer.files.length === 0) {
return;
}
const files = dataTransfer.files;
if (files && files.length > 0) {
handleFileUpload(card, files);
}
}
},
},
@ -565,6 +573,11 @@ Template.cardDetails.helpers({
let ret = !!Utils.getPopupCardId();
return ret;
},
isDateFormat(format) {
const currentUser = ReactiveCache.getCurrentUser();
if (!currentUser) return format === 'YYYY-MM-DD';
return currentUser.getDateFormat() === format;
},
// Upload progress helpers
hasActiveUploads() {
return uploadProgressManager.hasActiveUploads(this._id);
@ -1093,20 +1106,15 @@ BlazeComponent.extendComponent({
'is-checked',
);
const endString = this.currentCard.getVoteEnd();
this.currentCard.setVoteQuestion(
voteQuestion,
publicVote,
allowNonBoardMembers,
);
Meteor.call('cards.setVoteQuestion', this.currentCard._id, voteQuestion, publicVote, allowNonBoardMembers);
if (endString) {
this.currentCard.setVoteEnd(endString);
Meteor.call('cards.setVoteEnd', this.currentCard._id, endString);
}
Popup.back();
},
'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => {
event.preventDefault();
this.currentCard.unsetVote();
Meteor.call('cards.unsetVote', this.currentCard._id);
Popup.back();
}),
'click a.js-toggle-vote-public'(event) {
@ -1305,10 +1313,10 @@ BlazeComponent.extendComponent({
];
}
_storeDate(newDate) {
this.card.setVoteEnd(newDate);
Meteor.call('cards.setVoteEnd', this.card._id, newDate);
}
_deleteDate() {
this.card.unsetVoteEnd();
Meteor.call('cards.unsetVoteEnd', this.card._id);
}
}.register('editVoteEndDatePopup'));
@ -1330,17 +1338,14 @@ BlazeComponent.extendComponent({
);
const endString = this.currentCard.getPokerEnd();
this.currentCard.setPokerQuestion(
pokerQuestion,
allowNonBoardMembers,
);
Meteor.call('cards.setPokerQuestion', this.currentCard._id, pokerQuestion, allowNonBoardMembers);
if (endString) {
this.currentCard.setPokerEnd(endString);
Meteor.call('cards.setPokerEnd', this.currentCard._id, new Date(endString));
}
Popup.back();
},
'click .js-remove-poker': Popup.afterConfirm('deletePoker', (event) => {
this.currentCard.unsetPoker();
Meteor.call('cards.unsetPoker', this.currentCard._id);
Popup.back();
}),
'click a.js-toggle-poker-allow-non-members'(event) {
@ -1561,10 +1566,10 @@ BlazeComponent.extendComponent({
];
}
_storeDate(newDate) {
this.card.setPokerEnd(newDate);
Meteor.call('cards.setPokerEnd', this.card._id, newDate);
}
_deleteDate() {
this.card.unsetPokerEnd();
Meteor.call('cards.unsetPokerEnd', this.card._id);
}
}.register('editPokerEndDatePopup'));

View file

@ -72,6 +72,10 @@ textarea.js-edit-checklist-item {
padding-top: 3px;
float: left;
}
.checklist-title span.fa.checklist-handle.fa-arrows::before {
content: "↕️" !important;
font-family: inherit !important;
}
#card-details-overlay {
top: 0;
bottom: -600px;
@ -148,6 +152,10 @@ textarea.js-edit-checklist-item {
padding-top: 2px;
padding-right: 10px;
}
.checklist-item span.fa.checklistitem-handle.fa-arrows::before {
content: "↕️" !important;
font-family: inherit !important;
}
.js-delete-checklist-item,
.js-convert-checklist-item-to-card {
margin: 0 0 0.5em 1.33em;

View file

@ -69,6 +69,7 @@ template(name="addChecklistItemForm")
.edit-controls.clearfix
button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
a.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
| ❌
if showNewlineBecomesNewChecklistItem
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItem'}}")
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItem")
@ -91,6 +92,7 @@ template(name="editChecklistItemForm")
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
a.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
| ❌
span(title=createdAt) {{ moment createdAt }}
if canModifyCard
a.js-delete-checklist-item {{_ "delete"}}...
@ -125,8 +127,7 @@ template(name='checklistItemDetail')
if canModifyCard
.check-box-container
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
if isTouchScreenOrShowDesktopDragHandles
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer
= item.title

View file

@ -125,8 +125,19 @@ Template.createLabelPopup.events({
.$('#labelName')
.val()
.trim();
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
board.addLabel(name, color);
// Find the selected color by looking for the palette color that contains the checkmark
let selectedColor = null;
templateInstance.$('.js-palette-color').each(function() {
if ($(this).text().includes('✅')) {
selectedColor = Blaze.getData(this).color;
return false; // break out of loop
}
});
if (selectedColor) {
board.addLabel(name, selectedColor);
}
Popup.back();
},
});
@ -144,8 +155,19 @@ Template.editLabelPopup.events({
.$('#labelName')
.val()
.trim();
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
board.editLabel(this._id, name, color);
// Find the selected color by looking for the palette color that contains the checkmark
let selectedColor = null;
templateInstance.$('.js-palette-color').each(function() {
if ($(this).text().includes('✅')) {
selectedColor = Blaze.getData(this).color;
return false; // break out of loop
}
});
if (selectedColor) {
board.editLabel(this._id, name, selectedColor);
}
Popup.back();
},
});

View file

@ -99,8 +99,8 @@
float: none;
}
.minicard .minicard-labels .minicard-label {
width: 1.5vw;
height: 1.5vw;
width: clamp(12px, 1.5vw, 16px);
height: clamp(12px, 1.5vw, 16px);
border-radius: 0.3vw;
margin-right: 0.4vw;
margin-bottom: 0.4vh;
@ -130,8 +130,8 @@
margin-right: 0.5vw;
}
.minicard .handle {
width: 2.5vw;
height: 2.5vw;
width: clamp(20px, 2.5vw, 28px);
height: clamp(20px, 2.5vw, 28px);
position: absolute;
right: 0.7vw;
top: 0.7vh;
@ -169,6 +169,134 @@
margin-right: 0.4vw;
}
/* Unicode icons for minicard dates - matching cardDate.css */
.minicard .card-date.end-date time::before {
content: "🏁"; /* Finish flag - represents end/completion */
}
.minicard .card-date.due-date time::before {
content: "⏰"; /* Alarm clock - represents due/deadline */
}
.minicard .card-date.start-date time::before {
content: "🚀"; /* Rocket - represents start/launch */
}
.minicard .card-date.received-date time::before {
content: "📥"; /* Inbox tray - represents received/incoming */
}
.minicard .card-date time::before {
font-size: inherit;
margin-right: 0.3em;
display: inline-block;
}
/* Date type specific colors for minicards - matching cardDate.css */
.minicard .card-date.received-date {
background-color: #dbdbdb; /* Grey for received - same as base card-date */
}
.minicard .card-date.received-date:hover,
.minicard .card-date.received-date.is-active {
background-color: #b3b3b3;
}
.minicard .card-date.start-date {
background-color: #90ee90; /* Light green for start */
color: #000; /* Black text for start */
}
.minicard .card-date.start-date:hover,
.minicard .card-date.start-date.is-active {
background-color: #7dd87d;
}
.minicard .card-date.due-date {
background-color: #ffd700; /* Yellow for due */
color: #000; /* Black text for due */
}
.minicard .card-date.due-date:hover,
.minicard .card-date.due-date.is-active {
background-color: #e6c200;
}
.minicard .card-date.end-date {
background-color: #ffb3b3; /* Light red for end */
color: #000; /* Black text for end */
}
.minicard .card-date.end-date:hover,
.minicard .card-date.end-date.is-active {
background-color: #ff9999;
}
/* Date status colors for minicards - matching cardDate.css */
.minicard .card-date.overdue {
background-color: #ff4444 !important; /* Red for overdue */
color: #fff !important;
}
.minicard .card-date.overdue:hover,
.minicard .card-date.overdue.is-active {
background-color: #cc3333 !important;
}
.minicard .card-date.due-soon {
background-color: #ffaa00 !important; /* Amber for due soon */
color: #000 !important;
}
.minicard .card-date.due-soon:hover,
.minicard .card-date.due-soon.is-active {
background-color: #e69900 !important;
}
.minicard .card-date.not-due {
/* No special background - uses default date type colors */
}
.minicard .card-date.current {
background-color: #5ba639 !important; /* Green for current/active */
color: #fff !important;
}
.minicard .card-date.current:hover,
.minicard .card-date.current.is-active {
background-color: #46802c !important;
}
.minicard .card-date.completed {
background-color: #90ee90 !important; /* Light green for completed */
color: #000 !important;
}
.minicard .card-date.completed:hover,
.minicard .card-date.completed.is-active {
background-color: #7dd87d !important;
}
.minicard .card-date.completed-early {
background-color: #4caf50 !important; /* Green for completed early */
color: #fff !important;
}
.minicard .card-date.completed-early:hover,
.minicard .card-date.completed-early.is-active {
background-color: #45a049 !important;
}
.minicard .card-date.completed-late {
background-color: #ff9800 !important; /* Orange for completed late */
color: #fff !important;
}
.minicard .card-date.completed-late:hover,
.minicard .card-date.completed-late.is-active {
background-color: #f57c00 !important;
}
.minicard .card-date.completed-on-time {
background-color: #2196f3 !important; /* Blue for completed on time */
color: #fff !important;
}
.minicard .card-date.completed-on-time:hover,
.minicard .card-date.completed-on-time.is-active {
background-color: #1976d2 !important;
}
/* Font Awesome icons in minicard dates */
.minicard .card-date i.fa {
margin-right: 0.3vw;
@ -234,8 +362,8 @@
.minicard .minicard-creator .member {
float: right;
border-radius: 50%;
height: 3.5vw;
width: 3.5vw;
height: clamp(24px, 3.5vw, 32px);
width: clamp(24px, 3.5vw, 32px);
margin-bottom: 0.5vh;
}
.minicard .minicard-members .assignee,
@ -243,8 +371,8 @@
.minicard .minicard-creator .assignee {
float: right;
border-radius: 50%;
height: 3.5vw;
width: 3.5vw;
height: clamp(24px, 3.5vw, 32px);
width: clamp(24px, 3.5vw, 32px);
}
.minicard .minicard-members + .badges,
.minicard .minicard-assignees + .badges,

View file

@ -4,19 +4,14 @@ template(name="minicard")
class="{{#if isLinkedBoard}}linked-board{{/if}}"
class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}")
if canModifyCard
if isTouchScreenOrShowDesktopDragHandles
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") | ☰
.handle
| ↔️
else
a.minicard-details-menu.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") | ☰
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") ☰
if canMoveCard
.handle
| ↕️
.dates
if getReceived
unless getStart
unless getDue
unless getEnd
.date
+minicardReceivedDate
.date
+minicardReceivedDate
if getStart
.date
+minicardStartDate
@ -147,7 +142,7 @@ template(name="minicard")
if canModifyCard
if comments.length
.badge(title="{{_ 'card-comments-title' comments.length }}")
span.badge-icon.badge-comment.badge-text | 💬
span.badge-icon.badge-comment.badge-text 💬
= ' '
= comments.length
//span.badge-comment.badge-text
@ -155,36 +150,36 @@ template(name="minicard")
if getDescription
unless currentBoard.allowsDescriptionTextOnMinicard
.badge.badge-state-image-only(title=getDescription)
span.badge-icon | 📝
span.badge-icon 📝
if getVoteQuestion
.badge.badge-state-image-only(title=getVoteQuestion)
span.badge-icon(class="{{#if voteState}}text-green{{/if}}") | 👍
span.badge-icon(class="{{#if voteState}}text-green{{/if}}") 👍
span.badge-text {{ voteCountPositive }}
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}") | 👎
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}") 👎
span.badge-text {{ voteCountNegative }}
if getPokerQuestion
.badge.badge-state-image-only(title=getPokerQuestion)
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}") |
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}") ✅
if expiredPoker
span.badge-text {{ getPokerEstimation }}
if attachments.length
if currentBoard.allowsBadgeAttachmentOnMinicard
.badge
span.badge-icon | 📎
span.badge-icon 📎
span.badge-text= attachments.length
if checklists.length
.badge(class="{{#if checklistFinished}}is-finished{{/if}}")
span.badge-icon | ☑️
span.badge-icon ☑️
span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
if allSubtasks.count
.badge
span.badge-icon | 🌐
span.badge-icon 🌐
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
if currentBoard.allowsCardSortingByNumber
if currentBoard.allowsCardSortingByNumberOnMinicard
.badge
span.badge-icon | 🔢
span.badge-icon 🔢
span.badge-text.check-list-sort {{ sort }}
if currentBoard.allowsDescriptionTextOnMinicard
if getDescription

View file

@ -111,55 +111,58 @@ BlazeComponent.extendComponent({
'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
// Drag and drop file upload handlers
'dragover .minicard'(event) {
event.preventDefault();
event.stopPropagation();
// Only prevent default for file drags to avoid interfering with sortable
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
}
},
'dragenter .minicard'(event) {
event.preventDefault();
event.stopPropagation();
const card = this.data();
const board = card.board();
// Only allow drag-and-drop if user can modify card and board allows attachments
if (card.canModifyCard() && board && board.allowsAttachments) {
// Check if the drag contains files
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
const card = this.data();
const board = card.board();
// Only allow drag-and-drop if user can modify card and board allows attachments
if (Utils.canModifyCard() && board && board.allowsAttachments) {
$(event.currentTarget).addClass('is-dragging-over');
}
}
},
'dragleave .minicard'(event) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
}
},
'drop .minicard'(event) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
const card = this.data();
const board = card.board();
// Check permissions
if (!card.canModifyCard() || !board || !board.allowsAttachments) {
return;
}
// Check if this is a file drop (not a card reorder)
const dataTransfer = event.originalEvent.dataTransfer;
if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) {
return;
}
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
// Check if the drop contains files (not just text/HTML)
if (!dataTransfer.types.includes('Files')) {
return;
}
const card = this.data();
const board = card.board();
const files = dataTransfer.files;
if (files && files.length > 0) {
handleFileUpload(card, files);
// Check permissions
if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
return;
}
// Check if this is a file drop (not a card reorder)
if (!dataTransfer.files || dataTransfer.files.length === 0) {
return;
}
const files = dataTransfer.files;
if (files && files.length > 0) {
handleFileUpload(card, files);
}
}
},
}

View file

@ -56,17 +56,17 @@ template(name="importMapMembersAddPopup")
p
| {{_ 'import-user-select'}}
.js-map-member
+EasySearch.Input(index=searchIndex)
input.js-search-member-input(type="text" placeholder="{{_ 'search-users'}}")
ul.pop-over-list
+EasySearch.Each(index=searchIndex)
each searchResults
li.item.js-member-item
a.name.js-select-import(title="{{profile.fullname}} ({{username}})" data-id="{{__originalId}}")
+userAvatar(userId=__originalId)
a.name.js-select-import(title="{{profile.fullname}} ({{username}})" data-id="{{_id}}")
+userAvatar(userId=_id)
span.full-name
= profile.fullname
| (<span class="username">{{username}}</span>)
+EasySearch.IfSearching(index=searchIndex)
if searching.get
+spinner
+EasySearch.IfNoResults(index=searchIndex)
if noResults.get
.manage-member-section
p.quiet {{_ 'no-results'}}

View file

@ -311,6 +311,73 @@ BlazeComponent.extendComponent({
},
}).register('importMapMembersAddPopup');
// Global reactive variables for import member popup
const importMemberPopupState = {
searching: new ReactiveVar(false),
searchResults: new ReactiveVar([]),
noResults: new ReactiveVar(false),
searchTimeout: null
};
BlazeComponent.extendComponent({
onCreated() {
// Use global state
this.searching = importMemberPopupState.searching;
this.searchResults = importMemberPopupState.searchResults;
this.noResults = importMemberPopupState.noResults;
this.searchTimeout = importMemberPopupState.searchTimeout;
},
onRendered() {
this.find('.js-search-member-input').focus();
},
performSearch(query) {
if (!query || query.length < 2) {
this.searchResults.set([]);
this.noResults.set(false);
return;
}
this.searching.set(true);
this.noResults.set(false);
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
this.searchResults.set(results);
this.searching.set(false);
if (results.length === 0) {
this.noResults.set(true);
}
},
events() {
return [
{
'keyup .js-search-member-input'(event) {
const query = event.target.value.trim();
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
this.searchTimeout = setTimeout(() => {
this.performSearch(query);
}, 300);
},
},
];
},
}).register('importMapMembersAddPopupSearch');
Template.importMapMembersAddPopup.helpers({
searchIndex: () => UserSearchIndex,
searchResults() {
return importMemberPopupState.searchResults.get();
},
searching() {
return importMemberPopupState.searching;
},
noResults() {
return importMemberPopupState.noResults;
}
})

View file

@ -191,6 +191,11 @@ body.list-resizing-active * {
margin-right: 0 !important;
/* Ensure proper display */
display: inline-block !important;
/* Ensure it's clickable and shows proper cursor */
cursor: move !important;
pointer-events: auto !important;
/* Add some padding for better clickability */
padding: 4px !important;
}
/* Ensure buttons maintain original positioning */
@ -373,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;
@ -639,17 +641,22 @@ body.list-resizing-active * {
.mini-list.mobile-view {
flex: 0 0 60px;
height: auto;
width: 100%;
min-width: 100%;
border-left: 0px;
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%;
border-left: 0px;
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;
@ -657,9 +664,11 @@ body.list-resizing-active * {
.list.mobile-view.ui-sortable-helper {
flex: 0 0 60px;
height: 60px;
width: 100%;
border-left: 0px;
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;
@ -667,14 +676,17 @@ body.list-resizing-active * {
.list.mobile-view.placeholder {
flex: 0 0 60px;
height: 60px;
width: 100%;
border-left: 0px;
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*/
@ -683,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;
@ -760,21 +773,27 @@ body.list-resizing-active * {
align-items: initial;
}
@media screen and (max-width: 800px) {
@media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
.mini-list {
flex: 0 0 60px;
height: auto;
width: 100%;
min-width: 100%;
border-left: 0px;
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%;
border-left: 0px;
width: 100vw;
max-width: 100vw;
min-width: 100vw;
border-left: 0px !important;
margin: 0 !important;
padding: 0 !important;
}
.list:first-child {
margin-left: 0px;
@ -782,9 +801,11 @@ body.list-resizing-active * {
.list.ui-sortable-helper {
flex: 0 0 60px;
height: 60px;
width: 100%;
border-left: 0px;
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;
@ -792,14 +813,17 @@ body.list-resizing-active * {
.list.placeholder {
flex: 0 0 60px;
height: 60px;
width: 100%;
border-left: 0px;
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*/
@ -808,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;

View file

@ -150,17 +150,13 @@ BlazeComponent.extendComponent({
});
this.autorun(() => {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$cards.sortable({
handle: '.handle',
});
} else {
$cards.sortable({
handle: '.minicard',
});
}
if ($cards.data('uiSortable') || $cards.data('sortable')) {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$cards.sortable('option', 'handle', '.handle');
} else {
$cards.sortable('option', 'handle', '.minicard');
}
$cards.sortable(
'option',
'disabled',
@ -201,20 +197,60 @@ BlazeComponent.extendComponent({
listWidth() {
const user = ReactiveCache.getCurrentUser();
const list = Template.currentData();
if (!user || !list) return 270; // Return default width if user or list is not available
return user.getListWidthFromStorage(list.boardId, list._id);
if (!list) return 270; // Return default width if list is not available
if (user) {
// For logged-in users, get from user profile
return user.getListWidthFromStorage(list.boardId, list._id);
} else {
// For non-logged-in users, get from localStorage
try {
const stored = localStorage.getItem('wekan-list-widths');
if (stored) {
const widths = JSON.parse(stored);
if (widths[list.boardId] && widths[list.boardId][list._id]) {
return widths[list.boardId][list._id];
}
}
} catch (e) {
console.warn('Error reading list width from localStorage:', e);
}
return 270; // Return default width if not found
}
},
listConstraint() {
const user = ReactiveCache.getCurrentUser();
const list = Template.currentData();
if (!user || !list) return 550; // Return default constraint if user or list is not available
return user.getListConstraintFromStorage(list.boardId, list._id);
if (!list) return 550; // Return default constraint if list is not available
if (user) {
// For logged-in users, get from user profile
return user.getListConstraintFromStorage(list.boardId, list._id);
} else {
// For non-logged-in users, get from localStorage
try {
const stored = localStorage.getItem('wekan-list-constraints');
if (stored) {
const constraints = JSON.parse(stored);
if (constraints[list.boardId] && constraints[list.boardId][list._id]) {
return constraints[list.boardId][list._id];
}
}
} catch (e) {
console.warn('Error reading list constraint from localStorage:', e);
}
return 550; // Return default constraint if not found
}
},
autoWidth() {
const user = ReactiveCache.getCurrentUser();
const list = Template.currentData();
if (!user) {
// For non-logged-in users, auto-width is disabled
return false;
}
return user.isAutoWidth(list.boardId);
},
@ -333,14 +369,49 @@ BlazeComponent.extendComponent({
// Use the new storage method that handles both logged-in and non-logged-in users
if (process.env.DEBUG === 'true') {
}
Meteor.call('applyListWidthToStorage', boardId, listId, finalWidth, listConstraint, (error, result) => {
if (error) {
console.error('Error saving list width:', error);
} else {
const currentUser = ReactiveCache.getCurrentUser();
if (currentUser) {
// For logged-in users, use server method
Meteor.call('applyListWidthToStorage', boardId, listId, finalWidth, listConstraint, (error, result) => {
if (error) {
console.error('Error saving list width:', error);
} else {
if (process.env.DEBUG === 'true') {
}
}
});
} else {
// For non-logged-in users, save to localStorage directly
try {
// Save list width
const storedWidths = localStorage.getItem('wekan-list-widths');
let widths = storedWidths ? JSON.parse(storedWidths) : {};
if (!widths[boardId]) {
widths[boardId] = {};
}
widths[boardId][listId] = finalWidth;
localStorage.setItem('wekan-list-widths', JSON.stringify(widths));
// Save list constraint
const storedConstraints = localStorage.getItem('wekan-list-constraints');
let constraints = storedConstraints ? JSON.parse(storedConstraints) : {};
if (!constraints[boardId]) {
constraints[boardId] = {};
}
constraints[boardId][listId] = listConstraint;
localStorage.setItem('wekan-list-constraints', JSON.stringify(constraints));
if (process.env.DEBUG === 'true') {
}
} catch (e) {
console.warn('Error saving list width/constraint to localStorage:', e);
}
});
}
e.preventDefault();
};

View file

@ -51,11 +51,12 @@ template(name="listHeader")
div.list-header-menu
unless currentUser.isCommentOnly
if canSeeAddCard
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") |
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") |
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
else
a.list-header-menu-icon.js-select-list | ▶️
a.list-header-handle.handle.js-list-handle | ↔️
a.list-header-menu-icon.js-select-list ▶️
unless currentUser.isWorker
a.list-header-handle.handle.js-list-handle ↕️
else if currentUser.isBoardMember
if isWatching
i.list-header-watch-icon | 👁️
@ -65,14 +66,15 @@ template(name="listHeader")
//if isBoardAdmin
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
if canSeeAddCard
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") |
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
a.js-collapse(title="{{_ 'collapse'}}")
| ⬅️
| ➡️
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") | ☰
if currentUser.isBoardAdmin
if isTouchScreenOrShowDesktopDragHandles
a.list-header-handle.handle.js-list-handle | ↔️
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
if currentUser.isBoardMember
unless currentUser.isCommentOnly
unless currentUser.isWorker
a.list-header-handle.handle.js-list-handle ↕️
template(name="editListTitleForm")
.list-composer

View file

@ -32,7 +32,16 @@ template(name="dueCards")
span.global-search-error-messages
= msg
else
+resultsPaged(this)
.due-cards-results-header
h1
= resultsText
each card in dueCardsList
+resultCard(card)
else
.global-search-results-list-wrapper
.no-results
h3 {{_ 'dueCards-noResults-title'}}
p {{_ 'dueCards-noResults-description'}}
template(name="dueCardsViewChangePopup")
if currentUser

View file

@ -1,13 +1,6 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { CardSearchPagedComponent } from '../../lib/cardSearch';
import {
OPERATOR_HAS,
OPERATOR_SORT,
OPERATOR_USER,
ORDER_ASCENDING,
PREDICATE_DUE_AT,
} from '../../../config/search-const';
import { QueryParams } from '../../../config/query-classes';
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
import { TAPi18n } from '/imports/i18n';
// const subManager = new SubsManager();
@ -15,7 +8,7 @@ BlazeComponent.extendComponent({
dueCardsView() {
// eslint-disable-next-line no-console
// console.log('sort:', Utils.dueCardsView());
return Utils.dueCardsView();
return Utils && Utils.dueCardsView ? Utils.dueCardsView() : 'me';
},
events() {
@ -31,6 +24,47 @@ Template.dueCards.helpers({
userId() {
return Meteor.userId();
},
dueCardsList() {
const component = BlazeComponent.getComponentForElement(this.firstNode);
if (component && component.dueCardsList) {
return component.dueCardsList();
}
return [];
},
hasResults() {
const component = BlazeComponent.getComponentForElement(this.firstNode);
if (component && component.hasResults) {
return component.hasResults.get();
}
return false;
},
searching() {
const component = BlazeComponent.getComponentForElement(this.firstNode);
if (component && component.isLoading) {
return component.isLoading.get();
}
return true; // Show loading by default
},
hasQueryErrors() {
return false; // No longer using search, so always false
},
errorMessages() {
return []; // No longer using search, so always empty
},
cardsCount() {
const component = BlazeComponent.getComponentForElement(this.firstNode);
if (component && component.cardsCount) {
return component.cardsCount();
}
return 0;
},
resultsText() {
const component = BlazeComponent.getComponentForElement(this.firstNode);
if (component && component.resultsText) {
return component.resultsText();
}
return '';
},
});
BlazeComponent.extendComponent({
@ -38,12 +72,16 @@ BlazeComponent.extendComponent({
return [
{
'click .js-due-cards-view-me'() {
Utils.setDueCardsView('me');
if (Utils && Utils.setDueCardsView) {
Utils.setDueCardsView('me');
}
Popup.back();
},
'click .js-due-cards-view-all'() {
Utils.setDueCardsView('all');
if (Utils && Utils.setDueCardsView) {
Utils.setDueCardsView('all');
}
Popup.back();
},
},
@ -51,61 +89,162 @@ BlazeComponent.extendComponent({
},
}).register('dueCardsViewChangePopup');
class DueCardsComponent extends CardSearchPagedComponent {
class DueCardsComponent extends BlazeComponent {
onCreated() {
super.onCreated();
const queryParams = new QueryParams();
queryParams.addPredicate(OPERATOR_HAS, {
field: PREDICATE_DUE_AT,
exists: true,
});
// queryParams[OPERATOR_LIMIT] = 5;
queryParams.addPredicate(OPERATOR_SORT, {
name: PREDICATE_DUE_AT,
order: ORDER_ASCENDING,
});
this._cachedCards = null;
this._cachedTimestamp = null;
this.subscriptionHandle = null;
this.isLoading = new ReactiveVar(true);
this.hasResults = new ReactiveVar(false);
this.searching = new ReactiveVar(false);
if (Utils.dueCardsView() !== 'all') {
queryParams.addPredicate(OPERATOR_USER, ReactiveCache.getCurrentUser().username);
// Subscribe to the optimized due cards publication
this.autorun(() => {
const allUsers = this.dueCardsView() === 'all';
if (this.subscriptionHandle) {
this.subscriptionHandle.stop();
}
this.subscriptionHandle = Meteor.subscribe('dueCards', allUsers);
// Update loading state based on subscription
this.autorun(() => {
if (this.subscriptionHandle && this.subscriptionHandle.ready()) {
if (process.env.DEBUG === 'true') {
console.log('dueCards: subscription ready, loading data...');
}
this.isLoading.set(false);
const cards = this.dueCardsList();
this.hasResults.set(cards && cards.length > 0);
} else {
if (process.env.DEBUG === 'true') {
console.log('dueCards: subscription not ready, showing loading...');
}
this.isLoading.set(true);
this.hasResults.set(false);
}
});
});
}
onDestroyed() {
super.onDestroyed();
if (this.subscriptionHandle) {
this.subscriptionHandle.stop();
}
this.runGlobalSearch(queryParams);
}
dueCardsView() {
// eslint-disable-next-line no-console
//console.log('sort:', Utils.dueCardsView());
return Utils.dueCardsView();
return Utils && Utils.dueCardsView ? Utils.dueCardsView() : 'me';
}
sortByBoard() {
return this.dueCardsView() === 'board';
}
hasResults() {
return this.hasResults.get();
}
cardsCount() {
const cards = this.dueCardsList();
return cards ? cards.length : 0;
}
resultsText() {
const count = this.cardsCount();
if (count === 1) {
return TAPi18n.__('one-card-found');
} else {
// Get the translated text and manually replace %s with the count
const baseText = TAPi18n.__('n-cards-found');
const result = baseText.replace('%s', count);
if (process.env.DEBUG === 'true') {
console.log('dueCards: base text:', baseText, 'count:', count, 'result:', result);
}
return result;
}
}
dueCardsList() {
const results = this.getResults();
console.log('results:', results);
const cards = [];
if (results) {
results.forEach(card => {
cards.push(card);
// Check if subscription is ready
if (!this.subscriptionHandle || !this.subscriptionHandle.ready()) {
if (process.env.DEBUG === 'true') {
console.log('dueCards client: subscription not ready');
}
return [];
}
// Use cached results if available to avoid expensive re-sorting
if (this._cachedCards && this._cachedTimestamp && (Date.now() - this._cachedTimestamp < 5000)) {
if (process.env.DEBUG === 'true') {
console.log('dueCards client: using cached results,', this._cachedCards.length, 'cards');
}
return this._cachedCards;
}
// Get cards directly from the subscription (already sorted by the publication)
const cards = ReactiveCache.getCards({
type: 'cardType-card',
archived: false,
dueAt: { $exists: true, $nin: [null, ''] }
});
if (process.env.DEBUG === 'true') {
console.log('dueCards client: found', cards.length, 'cards with due dates');
console.log('dueCards client: cards details:', cards.map(c => ({
id: c._id,
title: c.title,
dueAt: c.dueAt,
boardId: c.boardId,
members: c.members,
assignees: c.assignees,
userId: c.userId
})));
}
// Filter cards based on user view preference
const allUsers = this.dueCardsView() === 'all';
const currentUser = ReactiveCache.getCurrentUser();
let filteredCards = cards;
if (process.env.DEBUG === 'true') {
console.log('dueCards client: current user:', currentUser ? currentUser._id : 'none');
console.log('dueCards client: showing all users:', allUsers);
}
if (!allUsers && currentUser) {
filteredCards = cards.filter(card => {
const isMember = card.members && card.members.includes(currentUser._id);
const isAssignee = card.assignees && card.assignees.includes(currentUser._id);
const isAuthor = card.userId === currentUser._id;
const matches = isMember || isAssignee || isAuthor;
if (process.env.DEBUG === 'true' && matches) {
console.log('dueCards client: card matches user:', card.title, { isMember, isAssignee, isAuthor });
}
return matches;
});
}
cards.sort((a, b) => {
const x = a.dueAt === null ? new Date('2100-12-31') : a.dueAt;
const y = b.dueAt === null ? new Date('2100-12-31') : b.dueAt;
if (process.env.DEBUG === 'true') {
console.log('dueCards client: filtered to', filteredCards.length, 'cards');
}
if (x > y) return 1;
else if (x < y) return -1;
// Cache the results for 5 seconds to avoid re-filtering on every render
this._cachedCards = filteredCards;
this._cachedTimestamp = Date.now();
return 0;
});
// Update reactive variables
this.hasResults.set(filteredCards && filteredCards.length > 0);
this.isLoading.set(false);
// eslint-disable-next-line no-console
console.log('cards:', cards);
return cards;
return filteredCards;
}
}

View file

@ -100,8 +100,9 @@
z-index: 1000;
padding: 10px 0px;
align-items: center;
flex-wrap: wrap; /* Allow wrapping on mobile */
min-height: 28px; /* Allow height to grow */
flex-wrap: nowrap; /* Prevent wrapping to keep single row */
min-height: 28px;
overflow: hidden; /* Prevent content from overflowing */
}
#header-quick-access .home-icon {
display: flex;
@ -167,13 +168,39 @@
white-space: nowrap;
padding: 10px;
margin: -10px;
flex: 1; /* Take up available space */
min-width: 0; /* Allow shrinking below content size */
display: flex; /* Use flexbox for better control */
align-items: center;
scrollbar-width: thin; /* Firefox */
scrollbar-color: rgba(255, 255, 255, 0.3) transparent; /* Firefox */
}
/* Webkit scrollbar styling for better UX */
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar {
height: 4px;
}
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar-track {
background: transparent;
}
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
#header-quick-access ul.header-quick-access-list li {
display: inline;
display: inline-block; /* Keep inline-block for proper spacing */
width: auto;
color: #d9d9d9;
padding: 12px 0px;
margin: -10px 0px;
flex-shrink: 0; /* Prevent items from shrinking */
white-space: nowrap; /* Prevent text wrapping within items */
}
#header-quick-access ul.header-quick-access-list li a {
padding: 12px 10px;
@ -220,6 +247,7 @@
margin: 0;
margin-top: 1px;
}
#header-quick-access #header-user-bar .header-user-bar-name,
#header-quick-access #header-help {
margin: 4px 8px 0 0;
@ -314,7 +342,8 @@
}
/* Make zoom input wider on all mobile screens */
@media screen and (max-width: 800px) {
@media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
#header-quick-access .zoom-controls .zoom-input {
min-width: 50px !important; /* Wider on mobile */
width: 50px !important; /* Fixed width to show all numbers */
@ -424,7 +453,8 @@
margin: 6px 5px 0;
width: 12px;
}
@media screen and (max-width: 800px) {
@media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
#header #header-main-bar {
height: 40px;
}
@ -446,6 +476,8 @@
transition: background-color 0.4s;
width: 100%;
z-index: 30;
flex-wrap: nowrap !important; /* Force single row on mobile */
overflow: hidden; /* Prevent content overflow */
}
/* Mobile home icon styling */
@ -489,11 +521,12 @@
screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) {
#header-quick-access {
height: auto !important; /* Allow height to grow */
height: 48px !important; /* Fixed height for mobile */
min-height: 48px !important; /* Minimum height for mobile */
flex-wrap: wrap !important; /* Force wrapping */
align-items: flex-start !important; /* Align to top when wrapping */
flex-wrap: nowrap !important; /* Force single row */
align-items: center !important; /* Center align items */
padding: 8px 0px !important; /* Adjust padding for mobile */
overflow: hidden !important; /* Prevent content overflow */
}
#header-quick-access {
font-size: 2em !important; /* 2x bigger base font size */

View file

@ -83,10 +83,6 @@ template(name="header")
i.mobile-icon(class="{{#if mobileMode}}active{{/if}}") 📱
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}") 🖥️
// Bookmarks button - desktop opens popup, mobile routes to page
a.board-header-btn.js-open-bookmarks(title="{{_ 'bookmarks'}}")
| 🔖
// Notifications
+notifications

View file

@ -470,8 +470,10 @@ a:not(.disabled).is-active i.fa {
MOBILE & TABLET RESPONSIVE IMPROVEMENTS
======================================== */
/* Mobile devices (up to 800px) */
@media screen and (max-width: 800px) {
/* Mobile devices (up to 800px) and all iPhone models */
@media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) and (orientation: landscape),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) and (orientation: portrait) {
#content {
margin: 1px 0px 0px 0px;
height: calc(100% - 0px);

View file

@ -92,6 +92,18 @@ Template.userFormsLayout.onRendered(() => {
if (loginInput && loginInput.name && (loginInput.name.toLowerCase().includes('user') || loginInput.name.toLowerCase().includes('email'))) {
loginInput.setAttribute('autocomplete', 'username email');
}
// Add autocomplete attributes to password fields for WCAG compliance
const passwordInputs = document.querySelectorAll('input[type="password"]');
passwordInputs.forEach(input => {
if (input.name && input.name.includes('password')) {
if (input.name.includes('password_again') || input.name.includes('new_password')) {
input.setAttribute('autocomplete', 'new-password');
} else {
input.setAttribute('autocomplete', 'current-password');
}
}
});
});
});

View file

@ -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;
@ -270,6 +293,8 @@
overflow-y: auto !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date button,
.pop-over[data-popup="editCardStartDatePopup"] .edit-date button,
.pop-over[data-popup="editCardDueDatePopup"] .edit-date button,
@ -364,9 +389,6 @@
margin: 0;
visibility: hidden;
}
.pop-over .quiet {
/* padding: 6px 6px 4px;*/
}
.pop-over.search-over {
background: #f0f0f0;
min-height: 14vh;

View file

@ -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-header p {
margin: 0;
.migration-progress-close {
cursor: pointer;
font-size: 16px;
opacity: 0.9;
opacity: 0.8;
transition: opacity 0.2s ease;
}
.migration-content {
padding: 24px 32px;
max-height: 60vh;
overflow-y: auto;
.migration-progress-close:hover {
opacity: 1;
}
.migration-overview {
margin-bottom: 32px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #667eea;
.migration-progress-content {
padding: 30px;
}
.overall-progress {
margin-bottom: 20px;
.migration-progress-overall {
margin-bottom: 25px;
}
.progress-bar {
width: 100%;
height: 12px;
background-color: #e0e0e0;
border-radius: 6px;
overflow: hidden;
.migration-progress-overall-label {
font-weight: 600;
color: #333;
margin-bottom: 8px;
position: relative;
font-size: 14px;
}
.progress-fill {
.migration-progress-overall-bar {
background: #e9ecef;
border-radius: 10px;
height: 12px;
overflow: hidden;
margin-bottom: 5px;
}
.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;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
animation: migrationProgressShimmer 2s infinite;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
@keyframes migrationProgressShimmer {
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;
}
.current-step i {
margin-right: 8px;
color: #667eea;
}
.estimated-time {
text-align: center;
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;
}
@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 {
font-weight: 600;
color: #333;
font-size: 14px;
margin-bottom: 2px;
}
.step-description {
color: #666;
font-size: 12px;
line-height: 1.3;
}
.step-progress {
.migration-progress-overall-percentage {
text-align: right;
min-width: 40px;
}
.step-progress .progress-text {
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-current-step {
margin-bottom: 25px;
}
.step-progress-bar .progress-fill {
.migration-progress-step-label {
font-weight: 600;
color: #333;
margin-bottom: 8px;
font-size: 14px;
}
.migration-progress-step-bar {
background: #e9ecef;
border-radius: 8px;
height: 8px;
overflow: hidden;
margin-bottom: 5px;
}
.migration-progress-step-fill {
background: linear-gradient(90deg, #007bff, #0056b3);
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 2px;
border-radius: 8px;
transition: width 0.3s ease;
}
.migration-status {
text-align: center;
color: #333;
font-size: 16px;
background-color: #e3f2fd;
padding: 12px 16px;
.migration-progress-step-percentage {
text-align: right;
font-size: 12px;
color: #666;
font-weight: 600;
}
.migration-progress-status {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 6px;
border: 1px solid #bbdefb;
margin-bottom: 16px;
border-left: 4px solid #007bff;
}
.migration-status i {
margin-right: 8px;
color: #2196f3;
.migration-progress-status-label {
font-weight: 600;
color: #333;
margin-bottom: 5px;
font-size: 13px;
}
.migration-footer {
padding: 16px 32px 24px;
border-top: 1px solid #e0e0e0;
background-color: #f8f9fa;
.migration-progress-status-text {
color: #555;
font-size: 14px;
line-height: 1.4;
}
.migration-info {
.migration-progress-details {
margin-bottom: 20px;
padding: 12px;
background: #e3f2fd;
border-radius: 6px;
border-left: 4px solid #2196f3;
}
.migration-progress-details-label {
font-weight: 600;
color: #1976d2;
margin-bottom: 5px;
font-size: 13px;
}
.migration-progress-details-text {
color: #1565c0;
font-size: 13px;
line-height: 1.4;
}
.migration-progress-footer {
padding: 20px 30px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
}
.migration-progress-note {
text-align: center;
color: #666;
font-size: 13px;
line-height: 1.4;
margin-bottom: 8px;
}
.migration-info i {
margin-right: 6px;
color: #667eea;
}
.migration-warning {
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;
}
.migration-warning i {
margin-right: 6px;
color: #f39c12;
font-style: italic;
}
/* Responsive design */
@media (max-width: 768px) {
.migration-modal {
width: 98%;
margin: 10px;
@media (max-width: 600px) {
.migration-progress-modal {
width: 95%;
margin: 20px;
}
.migration-header,
.migration-content,
.migration-footer {
padding-left: 16px;
padding-right: 16px;
.migration-progress-content {
padding: 20px;
}
.migration-header h3 {
font-size: 20px;
.migration-progress-header {
padding: 15px;
}
.step-header {
flex-direction: column;
align-items: flex-start;
}
.step-progress {
text-align: left;
margin-top: 8px;
}
.steps-list {
max-height: 200px;
.migration-progress-title {
font-size: 16px;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.migration-progress-modal {
background: #2d3748;
color: #e2e8f0;
}
.migration-progress-overall-label,
.migration-progress-step-label,
.migration-progress-status-label {
color: #e2e8f0;
}
.migration-progress-status {
background: #4a5568;
border-left-color: #63b3ed;
}
.migration-progress-status-text {
color: #cbd5e0;
}
.migration-progress-details {
background: #2b6cb0;
border-left-color: #4299e1;
}
.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;
}
}

View file

@ -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'}}
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-content
.migration-overview
.overall-progress
.progress-bar
.progress-fill(style="width: {{migrationProgress}}%")
.progress-text {{migrationProgress}}%
.progress-label {{_ 'overall-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-progress-status
.migration-progress-status-label
| Status:
.migration-progress-status-text
| {{stepStatus}}
.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}}%")
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...

View file

@ -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();
},
migrationSteps() {
const steps = migrationSteps.get();
const currentStep = migrationCurrentStep.get();
stepName() {
return migrationStepName.get();
},
return steps.map(step => ({
...step,
isCurrentStep: step.name === currentStep
}));
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}%`;
},
stepNameFormatted() {
const stepName = migrationStepName.get();
if (!stepName) return '';
// 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();
}
});

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -48,6 +48,59 @@
display: flex;
flex-direction: column;
}
/* Use checklist-style green checkboxes for all sidebar checkboxes */
.sidebar .materialCheckBox.is-checked,
.boardCardSettingsPopup .materialCheckBox.is-checked,
.boardSubtaskSettingsPopup .materialCheckBox.is-checked {
top: -4px !important;
left: -3px !important;
width: 7px !important;
height: 15px !important;
margin-right: 6px !important;
border-top: 2px solid transparent !important;
border-left: 2px solid transparent !important;
border-bottom: 2px solid #3cb500 !important;
border-right: 2px solid #3cb500 !important;
transform: rotate(40deg) !important;
-webkit-backface-visibility: hidden !important;
backface-visibility: hidden !important;
transform-origin: 100% 100% !important;
}
/* Card Settings 3-column grid layout */
.card-settings-grid {
display: grid;
grid-template-columns: 1fr 1fr 2fr;
gap: 10px;
margin-bottom: 10px;
}
.card-settings-row {
display: grid;
grid-template-columns: 1fr 1fr 2fr;
gap: 10px;
align-items: center;
padding: 5px 0;
border-bottom: 1px solid #eee;
}
.card-settings-column {
display: flex;
align-items: center;
justify-content: center;
}
.card-settings-column:last-child {
justify-content: flex-start;
}
.card-settings-column h4 {
margin: 0;
font-size: 12px;
font-weight: bold;
text-align: center;
}
.sidebar .sidebar-content ul.sidebar-list li > a {
display: flex;
height: 30px;

View file

@ -28,38 +28,32 @@ template(name="sidebar")
+Template.dynamic(template=getViewTemplate)
template(name='homeSidebar')
hr
+membersWidget
hr
+labelsWidget
hr
ul#cards.label-text-hidden
a.flex.js-toggle-minicard-label-text(title="{{_ 'hide-minicard-label-text'}}")
span {{#if hiddenMinicardLabelText}}✅{{else}}⬜{{/if}}
span {{_ 'hide-minicard-label-text'}}
b &nbsp;
.materialCheckBox(class="{{#if hiddenMinicardLabelText}}is-checked{{/if}}")
ul#cards.vertical-scrollbars-toggle
a.flex.js-vertical-scrollbars-toggle(title="{{_ 'enable-vertical-scrollbars'}}")
span {{_ 'enable-vertical-scrollbars'}}
b &nbsp;
.materialCheckBox(class="{{#if isVerticalScrollbars}}is-checked{{/if}}")
if currentUser
ul#cards.vertical-scrollbars-toggle
a.flex.js-vertical-scrollbars-toggle(title="{{_ 'enable-vertical-scrollbars'}}")
span {{#if isVerticalScrollbars}}✅{{else}}⬜{{/if}}
span {{_ 'enable-vertical-scrollbars'}}
ul#cards.show-week-of-year-toggle
a.flex.js-show-week-of-year-toggle(title="{{_ 'show-week-of-year'}}")
span {{#if isShowWeekOfYear}}✅{{else}}⬜{{/if}}
span {{_ 'show-week-of-year'}}
b &nbsp;
.materialCheckBox(class="{{#if isShowWeekOfYear}}is-checked{{/if}}")
hr
unless currentUser.isNoComments
h3.activity-title
| 💬
| {{_ 'activities'}}
.material-toggle-switch(title="{{_ 'show-activities'}}")
if showActivities
input.toggle-switch(type="checkbox" id="toggleShowActivitiesBoard" checked="checked")
else
input.toggle-switch(type="checkbox" id="toggleShowActivitiesBoard")
label.toggle-label(for="toggleShowActivitiesBoard")
a.flex.js-toggle-show-activities(title="{{_ 'show-activities'}}")
span {{#if showActivities}}✅{{else}}⬜{{/if}}
span {{_ 'show-activities'}}
+activities(mode="board")
template(name="membersWidget")
@ -185,165 +179,282 @@ template(name="boardInfoOnMyBoardsPopup")
unless currentSetting.hideCardCounterList
div.check-div
a.flex.js-field-has-cardcounterlist(class="{{#if allowsCardCounterList}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCardCounterList}}is-checked{{/if}}")
span {{#if allowsCardCounterList}}✅{{else}}⬜{{/if}}
span
| 🚪
| {{_ 'show-card-counter-per-list'}}
unless currentSetting.hideBoardMemberList
div.check-div
a.flex.js-field-has-boardmemberlist(class="{{#if allowsBoardMemberList}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsBoardMemberList}}is-checked{{/if}}")
span {{#if allowsBoardMemberList}}✅{{else}}⬜{{/if}}
span
| ⏳
| {{_ 'show-board_members-avatar'}}
template(name="boardCardSettingsPopup")
form.board-card-settings
h3 {{_ 'show-on-card'}}, {{_ 'show-on-minicard'}}
div.check-div
a.flex.js-field-has-receiveddate(class="{{#if allowsReceivedDate}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsReceivedDate}}is-checked{{/if}}")
.card-settings-grid
.card-settings-column
h4 {{_ 'show-on-card'}}
.card-settings-column
h4 {{_ 'show-on-minicard'}}
.card-settings-column
h4 {{_ 'description'}}
.card-settings-row
.card-settings-column
a.flex.js-field-has-receiveddate(title="{{_ 'card-received'}}" class="{{#if allowsReceivedDate}}is-checked{{/if}}")
span {{#if allowsReceivedDate}}✅{{else}}⬜{{/if}}
.card-settings-column
a.flex.js-field-has-receiveddate(title="{{_ 'card-received'}}" class="{{#if allowsReceivedDate}}is-checked{{/if}}")
span {{#if allowsReceivedDate}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 🚪
| {{_ 'card-received'}}
div.check-div
a.flex.js-field-has-startdate(class="{{#if allowsStartDate}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsStartDate}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-startdate(title="{{_ 'card-start'}}" class="{{#if allowsStartDate}}is-checked{{/if}}")
span {{#if allowsStartDate}}✅{{else}}⬜{{/if}}
.card-settings-column
a.flex.js-field-has-startdate(title="{{_ 'card-start'}}" class="{{#if allowsStartDate}}is-checked{{/if}}")
span {{#if allowsStartDate}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| ⏳
| {{_ 'card-start'}}
div.check-div
a.flex.js-field-has-duedate(class="{{#if allowsDueDate}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsDueDate}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-duedate(title="{{_ 'card-due'}}" class="{{#if allowsDueDate}}is-checked{{/if}}")
span {{#if allowsDueDate}}✅{{else}}⬜{{/if}}
.card-settings-column
a.flex.js-field-has-duedate(title="{{_ 'card-due'}}" class="{{#if allowsDueDate}}is-checked{{/if}}")
span {{#if allowsDueDate}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 🚪
| {{_ 'card-due'}}
div.check-div
a.flex.js-field-has-enddate(class="{{#if allowsEndDate}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsEndDate}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-enddate(title="{{_ 'card-end'}}" class="{{#if allowsEndDate}}is-checked{{/if}}")
span {{#if allowsEndDate}}✅{{else}}⬜{{/if}}
.card-settings-column
a.flex.js-field-has-enddate(title="{{_ 'card-end'}}" class="{{#if allowsEndDate}}is-checked{{/if}}")
span {{#if allowsEndDate}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| ⏰
| {{_ 'card-end'}}
div.check-div
a.flex.js-field-has-members(class="{{#if allowsMembers}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsMembers}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-members(title="{{_ 'members'}}" class="{{#if allowsMembers}}is-checked{{/if}}")
span {{#if allowsMembers}}✅{{else}}⬜{{/if}}
.card-settings-column
a.flex.js-field-has-members(title="{{_ 'members'}}" class="{{#if allowsMembers}}is-checked{{/if}}")
span {{#if allowsMembers}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 👥
| {{_ 'members'}}
div.check-div
a.flex.js-field-has-creator(class="{{#if allowsCreator}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCreator}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-creator(title="{{_ 'creator'}}" class="{{#if allowsCreator}}is-checked{{/if}}")
span {{#if allowsCreator}}✅{{else}}⬜{{/if}}
.card-settings-column
span
.card-settings-column
span
| 👤
| {{_ 'creator'}}
div.check-div
a.flex.js-field-has-creator-on-minicard(class="{{#if allowsCreatorOnMinicard}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCreatorOnMinicard}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
span
.card-settings-column
a.flex.js-field-has-creator-on-minicard(title="{{_ 'creator-on-minicard'}}" class="{{#if allowsCreatorOnMinicard}}is-checked{{/if}}")
span {{#if allowsCreatorOnMinicard}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 👤
| {{_ 'creator-on-minicard'}}
div.check-div
a.flex.js-field-has-assignee(class="{{#if allowsAssignee}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsAssignee}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-assignee(title="{{_ 'assignee'}}" class="{{#if allowsAssignee}}is-checked{{/if}}")
span {{#if allowsAssignee}}✅{{else}}⬜{{/if}}
.card-settings-column
a.flex.js-field-has-assignee(title="{{_ 'assignee'}}" class="{{#if allowsAssignee}}is-checked{{/if}}")
span {{#if allowsAssignee}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 👤
| {{_ 'assignee'}}
div.check-div
a.flex.js-field-has-assigned-by(class="{{#if allowsAssignedBy}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsAssignedBy}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-assigned-by(title="{{_ 'assigned-by'}}" class="{{#if allowsAssignedBy}}is-checked{{/if}}")
span {{#if allowsAssignedBy}}✅{{else}}⬜{{/if}}
.card-settings-column
a.flex.js-field-has-assigned-by(title="{{_ 'assigned-by'}}" class="{{#if allowsAssignedBy}}is-checked{{/if}}")
span {{#if allowsAssignedBy}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 🛒
| {{_ 'assigned-by'}}
div.check-div
a.flex.js-field-has-requested-by(class="{{#if allowsRequestedBy}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsRequestedBy}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-requested-by(title="{{_ 'requested-by'}}" class="{{#if allowsRequestedBy}}is-checked{{/if}}")
span {{#if allowsRequestedBy}}✅{{else}}⬜{{/if}}
.card-settings-column
a.flex.js-field-has-requested-by(title="{{_ 'requested-by'}}" class="{{#if allowsRequestedBy}}is-checked{{/if}}")
span {{#if allowsRequestedBy}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 👤➕
| {{_ 'requested-by'}}
div.check-div
a.flex.js-field-has-card-sorting-by-number(class="{{#if allowsCardSortingByNumber}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCardSortingByNumber}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-card-sorting-by-number(title="{{_ 'card-sorting-by-number'}}" class="{{#if allowsCardSortingByNumber}}is-checked{{/if}}")
span {{#if allowsCardSortingByNumber}}✅{{else}}⬜{{/if}}
.card-settings-column
span
.card-settings-column
span
| 🔢
| {{_ 'card-sorting-by-number'}}
div.check-div
a.flex.js-field-has-card-sorting-by-number-on-minicard(class="{{#if allowsCardSortingByNumberOnMinicard}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCardSortingByNumberOnMinicard}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
span
.card-settings-column
a.flex.js-field-has-card-sorting-by-number-on-minicard(title="{{_ 'card-sorting-by-number-on-minicard'}}" class="{{#if allowsCardSortingByNumberOnMinicard}}is-checked{{/if}}")
span {{#if allowsCardSortingByNumberOnMinicard}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 🔢
| {{_ 'card-sorting-by-number-on-minicard'}}
div.check-div
a.flex.js-field-has-card-show-lists(class="{{#if allowsShowLists}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsShowLists}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-card-show-lists(title="{{_ 'card-show-lists'}}" class="{{#if allowsShowLists}}is-checked{{/if}}")
span {{#if allowsShowLists}}✅{{else}}⬜{{/if}}
.card-settings-column
span
.card-settings-column
span
| 📋
| {{_ 'card-show-lists'}}
div.check-div
a.flex.js-field-has-labels(class="{{#if allowsLabels}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsLabels}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-labels(title="{{_ 'labels'}}" class="{{#if allowsLabels}}is-checked{{/if}}")
span {{#if allowsLabels}}✅{{else}}⬜{{/if}}
.card-settings-column
a.flex.js-field-has-labels(title="{{_ 'labels'}}" class="{{#if allowsLabels}}is-checked{{/if}}")
span {{#if allowsLabels}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 🏷️
| {{_ 'labels'}}
div.check-div
a.flex.js-field-has-card-show-lists-on-minicard(class="{{#if allowsShowListsOnMinicard}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsShowListsOnMinicard}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
span
.card-settings-column
a.flex.js-field-has-card-show-lists-on-minicard(title="{{_ 'card-show-lists-on-minicard'}}" class="{{#if allowsShowListsOnMinicard}}is-checked{{/if}}")
span {{#if allowsShowListsOnMinicard}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 📋
| {{_ 'card-show-lists-on-minicard'}}
div.check-div
a.flex.js-field-has-card-number(class="{{#if allowsCardNumber}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCardNumber}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-card-number(title="{{_ 'card'}} {{_ 'number'}}" class="{{#if allowsCardNumber}}is-checked{{/if}}")
span {{#if allowsCardNumber}}✅{{else}}⬜{{/if}}
.card-settings-column
a.flex.js-field-has-card-number(title="{{_ 'card'}} {{_ 'number'}}" class="{{#if allowsCardNumber}}is-checked{{/if}}")
span {{#if allowsCardNumber}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| #️⃣
| {{_ 'card'}}
| {{_ 'number'}}
div.check-div
a.flex.js-field-has-description-title(class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-description-title(title="{{_ 'description'}} {{_ 'title'}}" class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
span {{#if allowsDescriptionTitle}}✅{{else}}⬜{{/if}}
.card-settings-column
a.flex.js-field-has-description-title(title="{{_ 'description'}} {{_ 'title'}}" class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
span {{#if allowsDescriptionTitle}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 📝
| {{_ 'description'}}
| {{_ 'title'}}
div.check-div
a.flex.js-field-has-description-text(class="{{#if allowsDescriptionText}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsDescriptionText}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-description-text(title="{{_ 'description'}} {{_ 'custom-field-text'}}" class="{{#if allowsDescriptionText}}is-checked{{/if}}")
span {{#if allowsDescriptionText}}✅{{else}}⬜{{/if}}
.card-settings-column
a.flex.js-field-has-description-text(title="{{_ 'description'}} {{_ 'custom-field-text'}}" class="{{#if allowsDescriptionText}}is-checked{{/if}}")
span {{#if allowsDescriptionText}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 📝
| {{_ 'description'}}
| {{_ 'custom-field-text'}}
div.check-div
a.flex.js-field-has-description-text-on-minicard(class="{{#if allowsDescriptionTextOnMinicard}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsDescriptionTextOnMinicard}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
span
.card-settings-column
a.flex.js-field-has-description-text-on-minicard(title="{{_ 'description-on-minicard'}}" class="{{#if allowsDescriptionTextOnMinicard}}is-checked{{/if}}")
span {{#if allowsDescriptionTextOnMinicard}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 📝
| {{_ 'description-on-minicard'}}
div.check-div
a.flex.js-field-has-checklists(class="{{#if allowsChecklists}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsChecklists}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-checklists(title="{{_ 'checklists'}}" class="{{#if allowsChecklists}}is-checked{{/if}}")
span {{#if allowsChecklists}}✅{{else}}⬜{{/if}}
.card-settings-column
a.flex.js-field-has-checklists(title="{{_ 'checklists'}}" class="{{#if allowsChecklists}}is-checked{{/if}}")
span {{#if allowsChecklists}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| ✅
| {{_ 'checklists'}}
div.check-div
a.flex.js-field-has-subtasks(class="{{#if allowsSubtasks}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsSubtasks}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-subtasks(title="{{_ 'subtasks'}}" class="{{#if allowsSubtasks}}is-checked{{/if}}")
span {{#if allowsSubtasks}}✅{{else}}⬜{{/if}}
.card-settings-column
a.flex.js-field-has-subtasks(title="{{_ 'subtasks'}}" class="{{#if allowsSubtasks}}is-checked{{/if}}")
span {{#if allowsSubtasks}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 🌐
| {{_ 'subtasks'}}
div.check-div
a.flex.js-field-has-attachments(class="{{#if allowsAttachments}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsAttachments}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
a.flex.js-field-has-attachments(title="{{_ 'attachments'}}" class="{{#if allowsAttachments}}is-checked{{/if}}")
span {{#if allowsAttachments}}✅{{else}}⬜{{/if}}
.card-settings-column
a.flex.js-field-has-attachments(title="{{_ 'attachments'}}" class="{{#if allowsAttachments}}is-checked{{/if}}")
span {{#if allowsAttachments}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 📎
| {{_ 'attachments'}}
div.check-div
a.flex.js-field-has-badge-attachment-on-minicard(class="{{#if allowsBadgeAttachmentOnMinicard}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsBadgeAttachmentOnMinicard}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
span
.card-settings-column
a.flex.js-field-has-badge-attachment-on-minicard(title="{{_ 'badge-attachment-on-minicard'}}" class="{{#if allowsBadgeAttachmentOnMinicard}}is-checked{{/if}}")
span {{#if allowsBadgeAttachmentOnMinicard}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 📎
| {{_ 'badge-attachment-on-minicard'}}
div.check-div
a.flex.js-field-has-cover-attachment-on-minicard(class="{{#if allowsCoverAttachmentOnMinicard}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCoverAttachmentOnMinicard}}is-checked{{/if}}")
.card-settings-row
.card-settings-column
span
.card-settings-column
a.flex.js-field-has-cover-attachment-on-minicard(title="{{_ 'cover-attachment-on-minicard'}}" class="{{#if allowsCoverAttachmentOnMinicard}}is-checked{{/if}}")
span {{#if allowsCoverAttachmentOnMinicard}}✅{{else}}⬜{{/if}}
.card-settings-column
span
| 📖
| 🖼️
@ -364,27 +475,27 @@ template(name="boardCardSettingsPopup")
template(name="boardSubtaskSettingsPopup")
form.board-subtask-settings
h3 {{_ 'show-parent-in-minicard'}}
a#prefix-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
.materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
a#prefix-with-full-path.flex.js-field-show-parent-in-minicard(title="{{_ 'prefix-with-full-path'}}" class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
span {{#if $eq presentParentTask 'prefix-with-full-path'}}✅{{else}}⬜{{/if}}
span {{_ 'prefix-with-full-path'}}
a#prefix-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
.materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
a#prefix-with-parent.flex.js-field-show-parent-in-minicard(title="{{_ 'prefix-with-parent'}}" class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
span {{#if $eq presentParentTask 'prefix-with-parent'}}✅{{else}}⬜{{/if}}
span {{_ 'prefix-with-parent'}}
a#subtext-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
.materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
a#subtext-with-full-path.flex.js-field-show-parent-in-minicard(title="{{_ 'subtext-with-full-path'}}" class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
span {{#if $eq presentParentTask 'subtext-with-full-path'}}✅{{else}}⬜{{/if}}
span {{_ 'subtext-with-full-path'}}
a#subtext-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
.materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
a#subtext-with-parent.flex.js-field-show-parent-in-minicard(title="{{_ 'subtext-with-parent'}}" class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
span {{#if $eq presentParentTask 'subtext-with-parent'}}✅{{else}}⬜{{/if}}
span {{_ 'subtext-with-parent'}}
a#no-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
.materialCheckBox(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
a#no-parent.flex.js-field-show-parent-in-minicard(title="{{_ 'no-parent'}}" class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
span {{#if $eq presentParentTask 'no-parent'}}✅{{else}}⬜{{/if}}
span {{_ 'no-parent'}}
div
hr
div.check-div
a.flex.js-field-has-subtasks(class="{{#if allowsSubtasks}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsSubtasks}}is-checked{{/if}}")
a.flex.js-field-has-subtasks(title="{{_ 'show-subtasks-field'}}" class="{{#if allowsSubtasks}}is-checked{{/if}}")
span {{#if allowsSubtasks}}✅{{else}}⬜{{/if}}
span {{_ 'show-subtasks-field'}}
label
@ -426,13 +537,18 @@ 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
a.flex
span {{#unless enabled}}✅{{else}}⬜{{/unless}}
span {{_ 'disable-webhook'}}
b &nbsp;
.materialCheckBox(class="{{#unless enabled}}is-checked{{/unless}}")
input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" value=title)
input.js-outgoing-webhooks-url(type="text" name="url" value=url)
input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" value=token name="token")
@ -471,6 +587,10 @@ template(name="boardMenuPopup")
| 📦
| {{_ 'archived-items'}}
if currentUser.isBoardAdmin
li
a.js-open-migrations
| 🔧
| {{_ 'migrations'}}
li
a.js-change-board-color
| 🎨
@ -511,6 +631,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
| ➡️📦
@ -631,7 +755,7 @@ template(name="removeBoardTeamPopup")
template(name="addMemberPopup")
.js-search-member
+EasySearch.Input(index=searchIndex)
input.js-search-member-input(type="text" placeholder="{{_ 'email-address'}}")
if loading.get
+spinner
@ -639,25 +763,38 @@ template(name="addMemberPopup")
.warning {{_ error.get}}
else
ul.pop-over-list
+EasySearch.Each(index=searchIndex)
each searchResults
li.item.js-member-item(class="{{#if isBoardMember}}disabled{{/if}}")
a.name.js-select-member(title="{{profile.fullname}} ({{username}})")
+userAvatar(userId=__originalId)
+userAvatar(userId=_id)
span.full-name
= profile.fullname
| (<span class="username">{{username}}</span>)
if isBoardMember
.quiet ({{_ 'joined'}})
+EasySearch.IfSearching(index=searchIndex)
if searching.get
+spinner
+EasySearch.IfNoResults(index=searchIndex)
if noResults.get
.manage-member-section
p.quiet {{_ 'no-results'}}
button.js-email-invite.primary.full {{_ 'email-invite'}}
template(name="addMemberPopupTest")
.js-search-member
input.js-search-member-input(type="text" placeholder="{{_ 'email-address'}}")
ul.pop-over-list
each searchResults
li.item.js-member-item
a.name.js-select-member(title="{{profile.fullname}} ({{username}})")
+userAvatar(userId=_id)
span.full-name
= profile.fullname
| (<span class="username">{{username}}</span>)
button.js-email-invite.primary.full {{_ 'email-invite'}}
template(name="changePermissionsPopup")
ul.pop-over-list
li

View file

@ -13,6 +13,7 @@ const viewTitles = {
multiselection: 'multi-selection',
customFields: 'custom-fields',
archives: 'archives',
migrations: 'migrations',
};
BlazeComponent.extendComponent({
@ -195,7 +196,7 @@ BlazeComponent.extendComponent({
events() {
return [
{
'click #toggleShowActivitiesBoard'() {
'click .js-toggle-show-activities'() {
Utils.getCurrentBoard().toggleShowActivities();
},
},
@ -203,6 +204,8 @@ BlazeComponent.extendComponent({
},
}).register('homeSidebar');
Template.boardInfoOnMyBoardsPopup.helpers({
hideCardCounterList() {
return Utils.isMiniScreen() && Session.get('currentBoard');
@ -269,10 +272,53 @@ Template.boardMenuPopup.events({
Sidebar.setView('archives');
Popup.back();
},
'click .js-open-migrations'() {
Sidebar.setView('migrations');
Popup.back();
},
'click .js-change-board-color': Popup.open('boardChangeColor'),
'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();
@ -820,7 +866,11 @@ BlazeComponent.extendComponent({
},
allowsSubtasks() {
return this.currentBoard.allowsSubtasks;
// Get the current board reactively using board ID from Session
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
const result = currentBoard ? currentBoard.allowsSubtasks : false;
return result;
},
allowsReceivedDate() {
@ -872,7 +922,11 @@ BlazeComponent.extendComponent({
},
presentParentTask() {
let result = this.currentBoard.presentParentTask;
// Get the current board reactively using board ID from Session
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
let result = currentBoard ? currentBoard.presentParentTask : null;
if (result === null || result === undefined) {
result = 'no-parent';
}
@ -884,19 +938,11 @@ BlazeComponent.extendComponent({
{
'click .js-field-has-subtasks'(evt) {
evt.preventDefault();
this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks;
this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks);
$(`.js-field-has-subtasks ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsSubtasks,
);
$('.js-field-has-subtasks').toggleClass(
CKCLS,
this.currentBoard.allowsSubtasks,
);
const newValue = !this.currentBoard.allowsSubtasks;
Boards.update(this.currentBoard._id, { $set: { allowsSubtasks: newValue } });
$('.js-field-deposit-board').prop(
'disabled',
!this.currentBoard.allowsSubtasks,
!newValue,
);
},
'change .js-field-deposit-board'(evt) {
@ -912,28 +958,13 @@ BlazeComponent.extendComponent({
evt.preventDefault();
},
'click .js-field-show-parent-in-minicard'(evt) {
const value =
evt.target.id ||
$(evt.target).parent()[0].id ||
$(evt.target)
.parent()[0]
.parent()[0].id;
const options = [
'prefix-with-full-path',
'prefix-with-parent',
'subtext-with-full-path',
'subtext-with-parent',
'no-parent',
];
options.forEach(function(element) {
if (element !== value) {
$(`#${element} ${MCB}`).toggleClass(CKCLS, false);
$(`#${element}`).toggleClass(CKCLS, false);
}
});
$(`#${value} ${MCB}`).toggleClass(CKCLS, true);
$(`#${value}`).toggleClass(CKCLS, true);
this.currentBoard.setPresentParentTask(value);
// Get the ID from the anchor element, not the span
const anchorElement = $(evt.target).closest('.js-field-show-parent-in-minicard')[0];
const value = anchorElement ? anchorElement.id : null;
if (value) {
Boards.update(this.currentBoard._id, { $set: { presentParentTask: value } });
}
evt.preventDefault();
},
},
@ -947,115 +978,168 @@ BlazeComponent.extendComponent({
},
allowsReceivedDate() {
return this.currentBoard.allowsReceivedDate;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsReceivedDate : false;
},
allowsStartDate() {
return this.currentBoard.allowsStartDate;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsStartDate : false;
},
allowsDueDate() {
return this.currentBoard.allowsDueDate;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsDueDate : false;
},
allowsEndDate() {
return this.currentBoard.allowsEndDate;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsEndDate : false;
},
allowsSubtasks() {
return this.currentBoard.allowsSubtasks;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsSubtasks : false;
},
allowsCreator() {
return this.currentBoard.allowsCreator ?? false;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? (currentBoard.allowsCreator ?? false) : false;
},
allowsCreatorOnMinicard() {
return this.currentBoard.allowsCreatorOnMinicard ?? false;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? (currentBoard.allowsCreatorOnMinicard ?? false) : false;
},
allowsMembers() {
return this.currentBoard.allowsMembers;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsMembers : false;
},
allowsAssignee() {
return this.currentBoard.allowsAssignee;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsAssignee : false;
},
allowsAssignedBy() {
return this.currentBoard.allowsAssignedBy;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsAssignedBy : false;
},
allowsRequestedBy() {
return this.currentBoard.allowsRequestedBy;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsRequestedBy : false;
},
allowsCardSortingByNumber() {
return this.currentBoard.allowsCardSortingByNumber;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsCardSortingByNumber : false;
},
allowsShowLists() {
return this.currentBoard.allowsShowLists;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsShowLists : false;
},
allowsLabels() {
return this.currentBoard.allowsLabels;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsLabels : false;
},
allowsShowListsOnMinicard() {
return this.currentBoard.allowsShowListsOnMinicard;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsShowListsOnMinicard : false;
},
allowsChecklists() {
return this.currentBoard.allowsChecklists;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsChecklists : false;
},
allowsAttachments() {
return this.currentBoard.allowsAttachments;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsAttachments : false;
},
allowsComments() {
return this.currentBoard.allowsComments;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsComments : false;
},
allowsCardNumber() {
return this.currentBoard.allowsCardNumber;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsCardNumber : false;
},
allowsDescriptionTitle() {
return this.currentBoard.allowsDescriptionTitle;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsDescriptionTitle : false;
},
allowsDescriptionText() {
return this.currentBoard.allowsDescriptionText;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsDescriptionText : false;
},
isBoardSelected() {
return this.currentBoard.dateSettingsDefaultBoardID;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.dateSettingsDefaultBoardID : false;
},
isNullBoardSelected() {
return (
this.currentBoard.dateSettingsDefaultBoardId === null ||
this.currentBoard.dateSettingsDefaultBoardId === undefined
);
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? (
currentBoard.dateSettingsDefaultBoardId === null ||
currentBoard.dateSettingsDefaultBoardId === undefined
) : true;
},
allowsDescriptionTextOnMinicard() {
return this.currentBoard.allowsDescriptionTextOnMinicard;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsDescriptionTextOnMinicard : false;
},
allowsCoverAttachmentOnMinicard() {
return this.currentBoard.allowsCoverAttachmentOnMinicard;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsCoverAttachmentOnMinicard : false;
},
allowsBadgeAttachmentOnMinicard() {
return this.currentBoard.allowsBadgeAttachmentOnMinicard;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsBadgeAttachmentOnMinicard : false;
},
allowsCardSortingByNumberOnMinicard() {
return this.currentBoard.allowsCardSortingByNumberOnMinicard;
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsCardSortingByNumberOnMinicard : false;
},
boards() {
@ -1098,203 +1182,73 @@ BlazeComponent.extendComponent({
{
'click .js-field-has-receiveddate'(evt) {
evt.preventDefault();
this.currentBoard.allowsReceivedDate = !this.currentBoard
.allowsReceivedDate;
this.currentBoard.setAllowsReceivedDate(
this.currentBoard.allowsReceivedDate,
);
$(`.js-field-has-receiveddate ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsReceivedDate,
);
$('.js-field-has-receiveddate').toggleClass(
CKCLS,
this.currentBoard.allowsReceivedDate,
);
const newValue = !this.currentBoard.allowsReceivedDate;
Boards.update(this.currentBoard._id, { $set: { allowsReceivedDate: newValue } });
},
'click .js-field-has-startdate'(evt) {
evt.preventDefault();
this.currentBoard.allowsStartDate = !this.currentBoard
.allowsStartDate;
this.currentBoard.setAllowsStartDate(
this.currentBoard.allowsStartDate,
);
$(`.js-field-has-startdate ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsStartDate,
);
$('.js-field-has-startdate').toggleClass(
CKCLS,
this.currentBoard.allowsStartDate,
);
const newValue = !this.currentBoard.allowsStartDate;
Boards.update(this.currentBoard._id, { $set: { allowsStartDate: newValue } });
},
'click .js-field-has-enddate'(evt) {
evt.preventDefault();
this.currentBoard.allowsEndDate = !this.currentBoard.allowsEndDate;
this.currentBoard.setAllowsEndDate(this.currentBoard.allowsEndDate);
$(`.js-field-has-enddate ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsEndDate,
);
$('.js-field-has-enddate').toggleClass(
CKCLS,
this.currentBoard.allowsEndDate,
);
const newValue = !this.currentBoard.allowsEndDate;
Boards.update(this.currentBoard._id, { $set: { allowsEndDate: newValue } });
},
'click .js-field-has-duedate'(evt) {
evt.preventDefault();
this.currentBoard.allowsDueDate = !this.currentBoard.allowsDueDate;
this.currentBoard.setAllowsDueDate(this.currentBoard.allowsDueDate);
$(`.js-field-has-duedate ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsDueDate,
);
$('.js-field-has-duedate').toggleClass(
CKCLS,
this.currentBoard.allowsDueDate,
);
const newValue = !this.currentBoard.allowsDueDate;
Boards.update(this.currentBoard._id, { $set: { allowsDueDate: newValue } });
},
'click .js-field-has-subtasks'(evt) {
evt.preventDefault();
this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks;
this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks);
$(`.js-field-has-subtasks ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsSubtasks,
);
$('.js-field-has-subtasks').toggleClass(
CKCLS,
this.currentBoard.allowsSubtasks,
);
const newValue = !this.currentBoard.allowsSubtasks;
Boards.update(this.currentBoard._id, { $set: { allowsSubtasks: newValue } });
},
'click .js-field-has-creator'(evt) {
evt.preventDefault();
this.currentBoard.allowsCreator = !this.currentBoard.allowsCreator;
this.currentBoard.setAllowsCreator(this.currentBoard.allowsCreator);
$(`.js-field-has-creator ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsCreator,
);
$('.js-field-has-creator').toggleClass(
CKCLS,
this.currentBoard.allowsCreator,
);
const newValue = !this.currentBoard.allowsCreator;
Boards.update(this.currentBoard._id, { $set: { allowsCreator: newValue } });
},
'click .js-field-has-creator-on-minicard'(evt) {
evt.preventDefault();
this.currentBoard.allowsCreatorOnMinicard = !this.currentBoard.allowsCreatorOnMinicard;
this.currentBoard.setAllowsCreatorOnMinicard(this.currentBoard.allowsCreatorOnMinicard);
$(`.js-field-has-creator-on-minicard ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsCreatorOnMinicard,
);
$('.js-field-has-creator-on-minicard').toggleClass(
CKCLS,
this.currentBoard.allowsCreatorOnMinicard,
);
const newValue = !this.currentBoard.allowsCreatorOnMinicard;
Boards.update(this.currentBoard._id, { $set: { allowsCreatorOnMinicard: newValue } });
},
'click .js-field-has-members'(evt) {
evt.preventDefault();
this.currentBoard.allowsMembers = !this.currentBoard.allowsMembers;
this.currentBoard.setAllowsMembers(this.currentBoard.allowsMembers);
$(`.js-field-has-members ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsMembers,
);
$('.js-field-has-members').toggleClass(
CKCLS,
this.currentBoard.allowsMembers,
);
const newValue = !this.currentBoard.allowsMembers;
Boards.update(this.currentBoard._id, { $set: { allowsMembers: newValue } });
},
'click .js-field-has-assignee'(evt) {
evt.preventDefault();
this.currentBoard.allowsAssignee = !this.currentBoard.allowsAssignee;
this.currentBoard.setAllowsAssignee(this.currentBoard.allowsAssignee);
$(`.js-field-has-assignee ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsAssignee,
);
$('.js-field-has-assignee').toggleClass(
CKCLS,
this.currentBoard.allowsAssignee,
);
const newValue = !this.currentBoard.allowsAssignee;
Boards.update(this.currentBoard._id, { $set: { allowsAssignee: newValue } });
},
'click .js-field-has-assigned-by'(evt) {
evt.preventDefault();
this.currentBoard.allowsAssignedBy = !this.currentBoard
.allowsAssignedBy;
this.currentBoard.setAllowsAssignedBy(
this.currentBoard.allowsAssignedBy,
);
$(`.js-field-has-assigned-by ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsAssignedBy,
);
$('.js-field-has-assigned-by').toggleClass(
CKCLS,
this.currentBoard.allowsAssignedBy,
);
const newValue = !this.currentBoard.allowsAssignedBy;
Boards.update(this.currentBoard._id, { $set: { allowsAssignedBy: newValue } });
},
'click .js-field-has-requested-by'(evt) {
evt.preventDefault();
this.currentBoard.allowsRequestedBy = !this.currentBoard
.allowsRequestedBy;
this.currentBoard.setAllowsRequestedBy(
this.currentBoard.allowsRequestedBy,
);
$(`.js-field-has-requested-by ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsRequestedBy,
);
$('.js-field-has-requested-by').toggleClass(
CKCLS,
this.currentBoard.allowsRequestedBy,
);
const newValue = !this.currentBoard.allowsRequestedBy;
Boards.update(this.currentBoard._id, { $set: { allowsRequestedBy: newValue } });
},
'click .js-field-has-card-sorting-by-number'(evt) {
evt.preventDefault();
this.currentBoard.allowsCardSortingByNumber = !this.currentBoard
.allowsCardSortingByNumber;
this.currentBoard.setAllowsCardSortingByNumber(
this.currentBoard.allowsCardSortingByNumber,
);
$(`.js-field-has-card-sorting-by-number ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsCardSortingByNumber,
);
$('.js-field-has-card-sorting-by-number').toggleClass(
CKCLS,
this.currentBoard.allowsCardSortingByNumber,
);
const newValue = !this.currentBoard.allowsCardSortingByNumber;
Boards.update(this.currentBoard._id, { $set: { allowsCardSortingByNumber: newValue } });
},
'click .js-field-has-card-show-lists'(evt) {
evt.preventDefault();
this.currentBoard.allowsShowLists = !this.currentBoard
.allowsShowLists;
this.currentBoard.setAllowsShowLists(
this.currentBoard.allowsShowLists,
);
$(`.js-field-has-card-show-lists ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsShowLists,
);
$('.js-field-has-card-show-lists').toggleClass(
CKCLS,
this.currentBoard.allowsShowLists,
);
const newValue = !this.currentBoard.allowsShowLists;
Boards.update(this.currentBoard._id, { $set: { allowsShowLists: newValue } });
},
'click .js-field-has-labels'(evt) {
evt.preventDefault();
this.currentBoard.allowsLabels = !this.currentBoard.allowsLabels;
this.currentBoard.setAllowsLabels(this.currentBoard.allowsLabels);
$(`.js-field-has-labels ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsLabels,
);
$('.js-field-has-labels').toggleClass(
CKCLS,
this.currentBoard.allowsLabels,
);
const newValue = !this.currentBoard.allowsLabels;
Boards.update(this.currentBoard._id, { $set: { allowsLabels: newValue } });
},
'click .js-field-has-card-show-lists-on-minicard'(evt) {
evt.preventDefault();
@ -1490,19 +1444,27 @@ BlazeComponent.extendComponent({
},
}).register('boardCardSettingsPopup');
// Use Session variables instead of global ReactiveVars
Session.setDefault('addMemberPopup.searchResults', []);
Session.setDefault('addMemberPopup.searching', false);
Session.setDefault('addMemberPopup.noResults', false);
Session.setDefault('addMemberPopup.loading', false);
Session.setDefault('addMemberPopup.error', '');
BlazeComponent.extendComponent({
onCreated() {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
// Use Session variables
this.searchTimeout = null;
},
onRendered() {
this.find('.js-search-member input').focus();
this.find('.js-search-member-input').focus();
this.setLoading(false);
},
isBoardMember() {
const userId = this.currentData().__originalId;
const userId = this.currentData()._id;
const user = ReactiveCache.getUser(userId);
return user && user.isBoardMember();
},
@ -1512,15 +1474,35 @@ BlazeComponent.extendComponent({
},
setError(error) {
this.error.set(error);
Session.set('addMemberPopup.error', error);
},
setLoading(w) {
this.loading.set(w);
Session.set('addMemberPopup.loading', w);
},
isLoading() {
return this.loading.get();
return Session.get('addMemberPopup.loading');
},
performSearch(query) {
if (!query || query.length < 2) {
Session.set('addMemberPopup.searchResults', []);
Session.set('addMemberPopup.noResults', false);
return;
}
Session.set('addMemberPopup.searching', true);
Session.set('addMemberPopup.noResults', false);
// Use the fallback search
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
Session.set('addMemberPopup.searchResults', results);
Session.set('addMemberPopup.searching', false);
if (results.length === 0) {
Session.set('addMemberPopup.noResults', true);
}
},
inviteUser(idNameEmail) {
@ -1538,18 +1520,30 @@ BlazeComponent.extendComponent({
events() {
return [
{
'keyup input'() {
'keyup .js-search-member-input'(event) {
this.setError('');
const query = event.target.value.trim();
this.searchQuery.set(query);
// Clear previous timeout
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
// Debounce search
this.searchTimeout = setTimeout(() => {
this.performSearch(query);
}, 300);
},
'click .js-select-member'() {
const userId = this.currentData().__originalId;
const userId = this.currentData()._id;
const currentBoard = Utils.getCurrentBoard();
if (!currentBoard.hasMember(userId)) {
this.inviteUser(userId);
}
},
'click .js-email-invite'() {
const idNameEmail = $('.js-search-member input').val();
const idNameEmail = $('.js-search-member-input').val();
if (idNameEmail.indexOf('@') < 0 || this.isValidEmail(idNameEmail)) {
this.inviteUser(idNameEmail);
} else this.setError('email-invalid');
@ -1560,7 +1554,35 @@ BlazeComponent.extendComponent({
}).register('addMemberPopup');
Template.addMemberPopup.helpers({
searchIndex: () => UserSearchIndex,
searchResults() {
const results = Session.get('addMemberPopup.searchResults');
console.log('searchResults helper called, returning:', results);
return results;
},
searching() {
return Session.get('addMemberPopup.searching');
},
noResults() {
return Session.get('addMemberPopup.noResults');
},
loading() {
return Session.get('addMemberPopup.loading');
},
error() {
return Session.get('addMemberPopup.error');
},
isBoardMember() {
const userId = this._id;
const user = ReactiveCache.getUser(userId);
return user && user.isBoardMember();
}
})
Template.addMemberPopupTest.helpers({
searchResults() {
console.log('addMemberPopupTest searchResults helper called');
return Session.get('addMemberPopup.searchResults') || [];
}
})
BlazeComponent.extendComponent({

View file

@ -95,3 +95,7 @@ template(name="createCustomFieldPopup")
template(name="deleteCustomFieldPopup")
p {{_ "custom-field-delete-pop"}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
// Reuse the create form for editing to satisfy popup template lookup
template(name="editCustomFieldPopup")
+Template.dynamic(template="createCustomFieldPopup")

View file

@ -0,0 +1,109 @@
template(name='migrationsSidebar')
if currentUser.isBoardAdmin
.sidebar-migrations
h3
| 🔧
| {{_ 'migrations'}}
p.quiet {{_ 'migrations-description'}}
.migrations-list
h4 {{_ 'board-migrations'}}
.migration-item
a.js-run-migration(data-migration="comprehensive")
.migration-name
| {{_ 'comprehensive-board-migration'}}
.migration-status
if comprehensiveMigrationNeeded
span.badge.badge-warning {{_ 'migration-needed'}}
else
span.badge.badge-success {{_ 'migration-complete'}}
.migration-item
a.js-run-migration(data-migration="fixMissingLists")
.migration-name
| {{_ 'fix-missing-lists-migration'}}
.migration-status
if fixMissingListsNeeded
span.badge.badge-warning {{_ 'migration-needed'}}
else
span.badge.badge-success {{_ 'migration-complete'}}
.migration-item
a.js-run-migration(data-migration="deleteDuplicateEmptyLists")
.migration-name
| {{_ 'delete-duplicate-empty-lists-migration'}}
.migration-status
if deleteDuplicateEmptyListsNeeded
span.badge.badge-warning {{_ 'migration-needed'}}
else
span.badge.badge-success {{_ 'migration-complete'}}
.migration-item
a.js-run-migration(data-migration="restoreLostCards")
.migration-name
| {{_ 'restore-lost-cards-migration'}}
.migration-status
if restoreLostCardsNeeded
span.badge.badge-warning {{_ 'migration-needed'}}
else
span.badge.badge-success {{_ 'migration-complete'}}
.migration-item
a.js-run-migration(data-migration="restoreAllArchived")
.migration-name
| {{_ 'restore-all-archived-migration'}}
.migration-status
if restoreAllArchivedNeeded
span.badge.badge-warning {{_ 'migration-needed'}}
else
span.badge.badge-success {{_ 'migration-complete'}}
.migration-item
a.js-run-migration(data-migration="fixAvatarUrls")
.migration-name
| {{_ 'fix-avatar-urls-migration'}}
.migration-status
if fixAvatarUrlsNeeded
span.badge.badge-warning {{_ 'migration-needed'}}
else
span.badge.badge-success {{_ 'migration-complete'}}
.migration-item
a.js-run-migration(data-migration="fixAllFileUrls")
.migration-name
| {{_ 'fix-all-file-urls-migration'}}
.migration-status
if fixAllFileUrlsNeeded
span.badge.badge-warning {{_ 'migration-needed'}}
else
span.badge.badge-success {{_ 'migration-complete'}}
else
p.quiet {{_ 'migrations-admin-only'}}
template(name='runComprehensiveMigrationPopup')
p {{_ 'run-comprehensive-migration-confirm'}}
button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}}
template(name='runFixMissingListsMigrationPopup')
p {{_ 'run-fix-missing-lists-migration-confirm'}}
button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}}
template(name='runDeleteDuplicateEmptyListsMigrationPopup')
p {{_ 'run-delete-duplicate-empty-lists-migration-confirm'}}
button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}}
template(name='runRestoreLostCardsMigrationPopup')
p {{_ 'run-restore-lost-cards-migration-confirm'}}
button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}}
template(name='runRestoreAllArchivedMigrationPopup')
p {{_ 'run-restore-all-archived-migration-confirm'}}
button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}}
template(name='runFixAvatarUrlsMigrationPopup')
p {{_ 'run-fix-avatar-urls-migration-confirm'}}
button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}}
template(name='runFixAllFileUrlsMigrationPopup')
p {{_ 'run-fix-all-file-urls-migration-confirm'}}
button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}}

View file

@ -0,0 +1,341 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { migrationProgressManager } from '/client/components/migrationProgress';
BlazeComponent.extendComponent({
onCreated() {
this.migrationStatuses = new ReactiveVar({});
this.loadMigrationStatuses();
},
loadMigrationStatuses() {
const boardId = Session.get('currentBoard');
if (!boardId) return;
// Check comprehensive migration
Meteor.call('comprehensiveBoardMigration.needsMigration', boardId, (err, res) => {
if (!err) {
const statuses = this.migrationStatuses.get();
statuses.comprehensive = res;
this.migrationStatuses.set(statuses);
}
});
// Check fix missing lists migration
Meteor.call('fixMissingListsMigration.needsMigration', boardId, (err, res) => {
if (!err) {
const statuses = this.migrationStatuses.get();
statuses.fixMissingLists = res;
this.migrationStatuses.set(statuses);
}
});
// Check delete duplicate empty lists migration
Meteor.call('deleteDuplicateEmptyLists.needsMigration', boardId, (err, res) => {
if (!err) {
const statuses = this.migrationStatuses.get();
statuses.deleteDuplicateEmptyLists = res;
this.migrationStatuses.set(statuses);
}
});
// Check restore lost cards migration
Meteor.call('restoreLostCards.needsMigration', boardId, (err, res) => {
if (!err) {
const statuses = this.migrationStatuses.get();
statuses.restoreLostCards = res;
this.migrationStatuses.set(statuses);
}
});
// Check restore all archived migration
Meteor.call('restoreAllArchived.needsMigration', boardId, (err, res) => {
if (!err) {
const statuses = this.migrationStatuses.get();
statuses.restoreAllArchived = res;
this.migrationStatuses.set(statuses);
}
});
// Check fix avatar URLs migration (board-specific)
Meteor.call('fixAvatarUrls.needsMigration', boardId, (err, res) => {
if (!err) {
const statuses = this.migrationStatuses.get();
statuses.fixAvatarUrls = res;
this.migrationStatuses.set(statuses);
}
});
// Check fix all file URLs migration (board-specific)
Meteor.call('fixAllFileUrls.needsMigration', boardId, (err, res) => {
if (!err) {
const statuses = this.migrationStatuses.get();
statuses.fixAllFileUrls = res;
this.migrationStatuses.set(statuses);
}
});
},
comprehensiveMigrationNeeded() {
return this.migrationStatuses.get().comprehensive === true;
},
fixMissingListsNeeded() {
return this.migrationStatuses.get().fixMissingLists === true;
},
deleteDuplicateEmptyListsNeeded() {
return this.migrationStatuses.get().deleteDuplicateEmptyLists === true;
},
restoreLostCardsNeeded() {
return this.migrationStatuses.get().restoreLostCards === true;
},
restoreAllArchivedNeeded() {
return this.migrationStatuses.get().restoreAllArchived === true;
},
fixAvatarUrlsNeeded() {
return this.migrationStatuses.get().fixAvatarUrls === true;
},
fixAllFileUrlsNeeded() {
return this.migrationStatuses.get().fixAllFileUrls === true;
},
// Simulate migration progress updates using the global progress popup
async simulateMigrationProgress(progressSteps) {
const totalSteps = progressSteps.length;
for (let i = 0; i < progressSteps.length; i++) {
const step = progressSteps[i];
const overall = Math.round(((i + 1) / totalSteps) * 100);
// Start step
migrationProgressManager.updateProgress({
overallProgress: overall,
currentStep: i + 1,
totalSteps,
stepName: step.step,
stepProgress: 0,
stepStatus: `Starting ${step.name}...`,
stepDetails: null,
boardId: Session.get('currentBoard'),
});
const stepDuration = step.duration;
const updateInterval = 100;
const totalUpdates = Math.max(1, Math.floor(stepDuration / updateInterval));
for (let j = 0; j < totalUpdates; j++) {
const per = Math.round(((j + 1) / totalUpdates) * 100);
migrationProgressManager.updateProgress({
overallProgress: overall,
currentStep: i + 1,
totalSteps,
stepName: step.step,
stepProgress: per,
stepStatus: `Processing ${step.name}...`,
stepDetails: { progress: `${per}%` },
boardId: Session.get('currentBoard'),
});
// eslint-disable-next-line no-await-in-loop
await new Promise((r) => setTimeout(r, updateInterval));
}
// Complete step
migrationProgressManager.updateProgress({
overallProgress: overall,
currentStep: i + 1,
totalSteps,
stepName: step.step,
stepProgress: 100,
stepStatus: `${step.name} completed`,
stepDetails: { status: 'completed' },
boardId: Session.get('currentBoard'),
});
}
},
runMigration(migrationType) {
const boardId = Session.get('currentBoard');
let methodName;
let methodArgs = [];
switch (migrationType) {
case 'comprehensive':
methodName = 'comprehensiveBoardMigration.execute';
methodArgs = [boardId];
break;
case 'fixMissingLists':
methodName = 'fixMissingListsMigration.execute';
methodArgs = [boardId];
break;
case 'deleteDuplicateEmptyLists':
methodName = 'deleteDuplicateEmptyLists.execute';
methodArgs = [boardId];
break;
case 'restoreLostCards':
methodName = 'restoreLostCards.execute';
methodArgs = [boardId];
break;
case 'restoreAllArchived':
methodName = 'restoreAllArchived.execute';
methodArgs = [boardId];
break;
case 'fixAvatarUrls':
methodName = 'fixAvatarUrls.execute';
methodArgs = [boardId];
break;
case 'fixAllFileUrls':
methodName = 'fixAllFileUrls.execute';
methodArgs = [boardId];
break;
}
if (methodName) {
// Define simulated steps per migration type
const stepsByType = {
comprehensive: [
{ step: 'analyze_board_structure', name: 'Analyze Board Structure', duration: 800 },
{ step: 'fix_orphaned_cards', name: 'Fix Orphaned Cards', duration: 1200 },
{ step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 1000 },
{ step: 'ensure_per_swimlane_lists', name: 'Ensure Per-Swimlane Lists', duration: 800 },
{ step: 'validate_migration', name: 'Validate Migration', duration: 800 },
{ step: 'fix_avatar_urls', name: 'Fix Avatar URLs', duration: 600 },
{ step: 'fix_attachment_urls', name: 'Fix Attachment URLs', duration: 600 },
],
fixMissingLists: [
{ step: 'analyze_lists', name: 'Analyze Lists', duration: 600 },
{ step: 'create_missing_lists', name: 'Create Missing Lists', duration: 900 },
{ step: 'update_cards', name: 'Update Cards', duration: 900 },
{ step: 'finalize', name: 'Finalize', duration: 400 },
],
deleteDuplicateEmptyLists: [
{ step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 700 },
{ step: 'delete_duplicate_empty_lists', name: 'Delete Duplicate Empty Lists', duration: 800 },
],
restoreLostCards: [
{ step: 'ensure_lost_cards_swimlane', name: 'Ensure Lost Cards Swimlane', duration: 600 },
{ step: 'restore_lists', name: 'Restore Lists', duration: 800 },
{ step: 'restore_cards', name: 'Restore Cards', duration: 1000 },
],
restoreAllArchived: [
{ step: 'restore_swimlanes', name: 'Restore Swimlanes', duration: 800 },
{ step: 'restore_lists', name: 'Restore Lists', duration: 900 },
{ step: 'restore_cards', name: 'Restore Cards', duration: 1000 },
{ step: 'fix_missing_ids', name: 'Fix Missing IDs', duration: 600 },
],
fixAvatarUrls: [
{ step: 'scan_users', name: 'Checking board member avatars', duration: 500 },
{ step: 'fix_urls', name: 'Fixing avatar URLs', duration: 900 },
],
fixAllFileUrls: [
{ step: 'scan_files', name: 'Checking board file attachments', duration: 600 },
{ step: 'fix_urls', name: 'Fixing file URLs', duration: 1000 },
],
};
const steps = stepsByType[migrationType] || [
{ step: 'running', name: 'Running Migration', duration: 1000 },
];
// Kick off popup and simulated progress
migrationProgressManager.startMigration();
const progressPromise = this.simulateMigrationProgress(steps);
// Start migration call
const callPromise = new Promise((resolve, reject) => {
Meteor.call(methodName, ...methodArgs, (err, result) => {
if (err) return reject(err);
return resolve(result);
});
});
Promise.allSettled([callPromise, progressPromise]).then(([callRes]) => {
if (callRes.status === 'rejected') {
migrationProgressManager.failMigration(callRes.reason);
} else {
const result = callRes.value;
// Summarize result details in the popup
let summary = {};
if (result && result.results) {
// Comprehensive returns {success, results}
const r = result.results;
summary = {
totalCardsProcessed: r.totalCardsProcessed,
totalListsProcessed: r.totalListsProcessed,
totalListsCreated: r.totalListsCreated,
};
} else if (result && result.changes) {
// Many migrations return a changes string array
summary = { changes: result.changes.join(' | ') };
} else if (result && typeof result === 'object') {
summary = result;
}
migrationProgressManager.updateProgress({
overallProgress: 100,
currentStep: steps.length,
totalSteps: steps.length,
stepName: 'completed',
stepProgress: 100,
stepStatus: 'Migration completed',
stepDetails: summary,
boardId: Session.get('currentBoard'),
});
migrationProgressManager.completeMigration();
// Refresh status badges slightly after
Meteor.setTimeout(() => {
this.loadMigrationStatuses();
}, 1000);
}
});
}
},
events() {
const self = this; // Capture component reference
return [
{
'click .js-run-migration[data-migration="comprehensive"]': Popup.afterConfirm('runComprehensiveMigration', function() {
self.runMigration('comprehensive');
Popup.back();
}),
'click .js-run-migration[data-migration="fixMissingLists"]': Popup.afterConfirm('runFixMissingListsMigration', function() {
self.runMigration('fixMissingLists');
Popup.back();
}),
'click .js-run-migration[data-migration="deleteDuplicateEmptyLists"]': Popup.afterConfirm('runDeleteDuplicateEmptyListsMigration', function() {
self.runMigration('deleteDuplicateEmptyLists');
Popup.back();
}),
'click .js-run-migration[data-migration="restoreLostCards"]': Popup.afterConfirm('runRestoreLostCardsMigration', function() {
self.runMigration('restoreLostCards');
Popup.back();
}),
'click .js-run-migration[data-migration="restoreAllArchived"]': Popup.afterConfirm('runRestoreAllArchivedMigration', function() {
self.runMigration('restoreAllArchived');
Popup.back();
}),
'click .js-run-migration[data-migration="fixAvatarUrls"]': Popup.afterConfirm('runFixAvatarUrlsMigration', function() {
self.runMigration('fixAvatarUrls');
Popup.back();
}),
'click .js-run-migration[data-migration="fixAllFileUrls"]': Popup.afterConfirm('runFixAllFileUrlsMigration', function() {
self.runMigration('fixAllFileUrls');
Popup.back();
}),
},
];
},
}).register('migrationsSidebar');

View file

@ -23,27 +23,28 @@ template(name="swimlaneFixedHeader")
+viewer
| {{isTitleDefault title}}
.swimlane-header-menu
unless currentUser.isCommentOnly
a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
|
a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
| ☰
//// TODO: Collapse Swimlane: make button working, etc.
//unless collapsed
// a.js-collapse-swimlane(title="{{_ 'collapse'}}")
// i.fa.fa-arrow-down.swimlane-header-collapse-down
// ⬆️.swimlane-header-collapse-up
//if collapsed
// a.js-collapse-swimlane(title="{{_ 'uncollapse'}}")
// ⬆️.swimlane-header-collapse-up
// i.fa.fa-arrow-down.swimlane-header-collapse-down
unless isTouchScreen
if isShowDesktopDragHandles
a.swimlane-header-handle.handle.js-swimlane-header-handle
| ↕️
if isTouchScreen
a.swimlane-header-miniscreen-handle.handle.js-swimlane-header-handle
| ↕️
if currentUser
unless currentUser.isCommentOnly
unless currentUser.isWorker
a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
|
a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
| ☰
//// TODO: Collapse Swimlane: make button working, etc.
//unless collapsed
// a.js-collapse-swimlane(title="{{_ 'collapse'}}")
// i.fa.fa-arrow-down.swimlane-header-collapse-down
// ⬆️.swimlane-header-collapse-up
//if collapsed
// a.js-collapse-swimlane(title="{{_ 'uncollapse'}}")
// ⬆️.swimlane-header-collapse-up
// i.fa.fa-arrow-down.swimlane-header-collapse-down
unless isTouchScreen
a.swimlane-header-handle.handle.js-swimlane-header-handle
| ↕️
if isTouchScreen
a.swimlane-header-miniscreen-handle.handle.js-swimlane-header-handle
| ↕️
template(name="editSwimlaneTitleForm")
.list-composer
@ -54,44 +55,46 @@ template(name="editSwimlaneTitleForm")
| ❌
template(name="swimlaneActionPopup")
unless currentUser.isCommentOnly
ul.pop-over-list
if currentUser.isBoardAdmin
li: a.js-set-swimlane-color
| 🎨
| {{_ 'select-color'}}
li: a.js-set-swimlane-height
| ↕️
| {{_ 'set-swimlane-height'}}
if currentUser.isBoardAdmin
unless this.isTemplateContainer
hr
ul.pop-over-list
li: a.js-close-swimlane
| ▶️
| 📦
| {{_ 'archive-swimlane'}}
ul.pop-over-list
li: a.js-copy-swimlane
| 📋
| {{_ 'copy-swimlane'}}
ul.pop-over-list
li: a.js-move-swimlane
| ⬆️
| {{_ 'move-swimlane'}}
if currentUser
unless currentUser.isCommentOnly
ul.pop-over-list
if currentUser.isBoardAdmin
li: a.js-set-swimlane-color
| 🎨
| {{_ 'select-color'}}
li: a.js-set-swimlane-height
| ↕️
| {{_ 'set-swimlane-height'}}
if currentUser.isBoardAdmin
unless this.isTemplateContainer
hr
ul.pop-over-list
li: a.js-close-swimlane
| ▶️
| 📦
| {{_ 'archive-swimlane'}}
ul.pop-over-list
li: a.js-copy-swimlane
| 📋
| {{_ 'copy-swimlane'}}
ul.pop-over-list
li: a.js-move-swimlane
| ⬆️
| {{_ 'move-swimlane'}}
template(name="swimlaneAddPopup")
unless currentUser.isCommentOnly
form
input.swimlane-name-input.full-line(type="text" placeholder="{{_ 'add-swimlane'}}"
autocomplete="off" autofocus)
.edit-controls.clearfix
button.primary.confirm(type="submit") {{_ 'add'}}
unless currentBoard.isTemplatesBoard
unless currentBoard.isTemplateBoard
span.quiet
| {{_ 'or'}}
a.js-swimlane-template {{_ 'template'}}
if currentUser
unless currentUser.isCommentOnly
form
input.swimlane-name-input.full-line(type="text" placeholder="{{_ 'add-swimlane'}}"
autocomplete="off" autofocus)
.edit-controls.clearfix
button.primary.confirm(type="submit") {{_ 'add'}}
unless currentBoard.isTemplatesBoard
unless currentBoard.isTemplateBoard
span.quiet
| {{_ 'or'}}
a.js-swimlane-template {{_ 'template'}}
template(name="setSwimlaneColorPopup")
form.edit-label.swimlane-color-popup

View file

@ -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);
},

View file

@ -112,7 +112,7 @@
padding: 7px;
top: 50%;
transform: translateY(-50%);
left: 87vw;
right: 10px;
font-size: 24px;
cursor: move;
z-index: 15;

View file

@ -72,21 +72,23 @@ template(name="addListForm")
|
template(name="moveSwimlanePopup")
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each toBoard in toBoards
option(value="{{toBoard._id}}") {{toBoard.title}}
if currentUser
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each toBoard in toBoards
option(value="{{toBoard._id}}") {{toBoard.title}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
template(name="copySwimlanePopup")
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each toBoard in toBoards
option(value="{{toBoard._id}}" selected="{{#if $eq toBoard.title board.title}}1{{/if}}") {{toBoard.title}}
if currentUser
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each toBoard in toBoards
option(value="{{toBoard._id}}" selected="{{#if $eq toBoard.title board.title}}1{{/if}}") {{toBoard.title}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}

View file

@ -1,6 +1,9 @@
import { ReactiveCache } from '/imports/reactiveCache';
import dragscroll from '@wekanteam/dragscroll';
const { calculateIndex } = Utils;
function currentListIsInThisSwimlane(swimlaneId) {
const currentList = Utils.getCurrentList();
return (
@ -43,6 +46,18 @@ function currentCardIsInThisList(listId, swimlaneId) {
}
function initSortable(boardComponent, $listsDom) {
// Safety check: ensure we have valid DOM elements
if (!$listsDom || $listsDom.length === 0) {
console.error('initSortable: No valid DOM elements provided');
return;
}
// Check if sortable is already initialized
if ($listsDom.data('uiSortable') || $listsDom.data('sortable')) {
$listsDom.sortable('destroy');
}
// We want to animate the card details window closing. We rely on CSS
// transition for the actual animation.
$listsDom._uihooks = {
@ -62,18 +77,68 @@ function initSortable(boardComponent, $listsDom) {
},
};
$listsDom.sortable({
connectWith: '.js-swimlane, .js-lists',
tolerance: 'pointer',
helper: 'clone',
items: '.js-list:not(.js-list-composer)',
placeholder: 'js-list placeholder',
distance: 7,
// Add click debugging for drag handles
$listsDom.on('mousedown', '.js-list-handle', function(e) {
e.stopPropagation();
});
$listsDom.on('mousedown', '.js-list-header', function(e) {
});
// Add debugging for any mousedown on lists
$listsDom.on('mousedown', '.js-list', function(e) {
});
// Add debugging for sortable events
$listsDom.on('sortstart', function(e, ui) {
});
$listsDom.on('sortbeforestop', function(e, ui) {
});
$listsDom.on('sortstop', function(e, ui) {
});
try {
$listsDom.sortable({
connectWith: '.js-swimlane, .js-lists',
tolerance: 'pointer',
appendTo: '.board-canvas',
helper(evt, item) {
const helper = item.clone();
helper.css('z-index', 1000);
return helper;
},
items: '.js-list:not(.js-list-composer)',
placeholder: 'list placeholder',
distance: 3,
forcePlaceholderSize: true,
cursor: 'move',
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
ui.placeholder.width(ui.helper.width());
EscapeActions.executeUpTo('popup-close');
boardComponent.setIsDragging(true);
// Add visual feedback for list being dragged
ui.item.addClass('ui-sortable-helper');
// Disable dragscroll during list dragging to prevent interference
try {
dragscroll.reset();
} catch (e) {
}
// Also disable dragscroll on all swimlanes during list dragging
$('.js-swimlane').each(function() {
$(this).removeClass('dragscroll');
});
},
beforeStop(evt, ui) {
// Clean up visual feedback
ui.item.removeClass('ui-sortable-helper');
},
stop(evt, ui) {
// To attribute the new index number, we need to get the DOM element
@ -83,15 +148,37 @@ function initSortable(boardComponent, $listsDom) {
const sortIndex = calculateIndex(prevListDom, nextListDom, 1);
const listDomElement = ui.item.get(0);
const list = Blaze.getData(listDomElement);
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
targetSwimlaneId = targetSwimlaneDom.attr('id').replace('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
@ -127,9 +214,6 @@ function initSortable(boardComponent, $listsDom) {
// If the list was dropped in a different swimlane, update the swimlaneId
if (isDifferentSwimlane) {
updateData.swimlaneId = targetSwimlaneId;
if (process.env.DEBUG === 'true') {
console.log(`Moving list "${list.title}" from swimlane ${originalSwimlaneId} to swimlane ${targetSwimlaneId}`);
}
// Move all cards in the list to the new swimlane
const cardsInList = ReactiveCache.getCards({
@ -141,47 +225,48 @@ function initSortable(boardComponent, $listsDom) {
card.move(list.boardId, targetSwimlaneId, list._id);
});
if (process.env.DEBUG === 'true') {
console.log(`Moved ${cardsInList.length} cards to swimlane ${targetSwimlaneId}`);
}
// 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, {
$set: updateData,
});
} catch (error) {
console.error('Error updating list:', error);
return;
}
Lists.update(list._id, {
$set: updateData,
});
boardComponent.setIsDragging(false);
// Re-enable dragscroll after list dragging is complete
try {
dragscroll.reset();
} catch (e) {
}
// Re-enable dragscroll on all swimlanes
$('.js-swimlane').each(function() {
$(this).addClass('dragscroll');
});
},
});
} catch (error) {
console.error('Error initializing list sortable:', error);
return;
}
boardComponent.autorun(() => {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$listsDom.sortable({
handle: '.js-list-handle',
connectWith: '.js-swimlane, .js-lists',
});
} else {
$listsDom.sortable({
handle: '.js-list-header',
connectWith: '.js-swimlane, .js-lists',
});
}
const $listDom = $listsDom;
if ($listDom.data('uiSortable') || $listDom.data('sortable')) {
$listsDom.sortable(
'option',
'disabled',
!ReactiveCache.getCurrentUser()?.isBoardAdmin(),
);
}
});
// Check if drag handles exist
const dragHandles = $listsDom.find('.js-list-handle');
// Check if lists exist
const lists = $listsDom.find('.js-list');
// Skip the complex autorun and options for now
}
BlazeComponent.extendComponent({
@ -189,11 +274,54 @@ BlazeComponent.extendComponent({
const boardComponent = this.parentComponent();
const $listsDom = this.$('.js-lists');
if (!Utils.getCurrentCardId()) {
boardComponent.scrollLeft();
}
initSortable(boardComponent, $listsDom);
// Try a simpler approach - initialize sortable directly like cards do
// Wait for DOM to be ready
setTimeout(() => {
const $lists = this.$('.js-list');
const $parent = $lists.parent();
if ($lists.length > 0) {
// Check for drag handles
const $handles = $parent.find('.js-list-handle');
// Test if drag handles are clickable
$handles.on('click', function(e) {
e.preventDefault();
e.stopPropagation();
});
$parent.sortable({
connectWith: '.js-swimlane, .js-lists',
tolerance: 'pointer',
appendTo: '.board-canvas',
helper: 'clone',
items: '.js-list:not(.js-list-composer)',
placeholder: 'list placeholder',
distance: 7,
handle: '.js-list-handle',
disabled: !Utils.canModifyBoard(),
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
ui.placeholder.width(ui.helper.width());
EscapeActions.executeUpTo('popup-close');
boardComponent.setIsDragging(true);
},
stop(evt, ui) {
boardComponent.setIsDragging(false);
}
});
} else {
}
}, 100);
},
onCreated() {
this.draggingActive = new ReactiveVar(false);
@ -256,7 +384,6 @@ BlazeComponent.extendComponent({
const isInNoDragArea = $(evt.target).closest(noDragInside.join(',')).length > 0;
if (isResizeHandle) {
console.log('Board drag prevented - resize handle clicked');
return;
}
@ -294,7 +421,31 @@ BlazeComponent.extendComponent({
swimlaneHeight() {
const user = ReactiveCache.getCurrentUser();
const swimlane = Template.currentData();
const height = user.getSwimlaneHeightFromStorage(swimlane.boardId, swimlane._id);
let height;
if (user) {
// For logged-in users, get from user profile
height = user.getSwimlaneHeightFromStorage(swimlane.boardId, swimlane._id);
} else {
// For non-logged-in users, get from localStorage
try {
const stored = localStorage.getItem('wekan-swimlane-heights');
if (stored) {
const heights = JSON.parse(stored);
if (heights[swimlane.boardId] && heights[swimlane.boardId][swimlane._id]) {
height = heights[swimlane.boardId][swimlane._id];
} else {
height = -1;
}
} else {
height = -1;
}
} catch (e) {
console.warn('Error reading swimlane height from localStorage:', e);
height = -1;
}
}
return height == -1 ? "auto" : (height + 5 + "px");
},
@ -408,15 +559,36 @@ BlazeComponent.extendComponent({
if (process.env.DEBUG === 'true') {
}
// Use the new storage method that handles both logged-in and non-logged-in users
Meteor.call('applySwimlaneHeightToStorage', boardId, swimlaneId, finalHeight, (error, result) => {
if (error) {
console.error('Error saving swimlane height:', error);
} else {
const currentUser = ReactiveCache.getCurrentUser();
if (currentUser) {
// For logged-in users, use server method
Meteor.call('applySwimlaneHeightToStorage', boardId, swimlaneId, finalHeight, (error, result) => {
if (error) {
console.error('Error saving swimlane height:', error);
} else {
if (process.env.DEBUG === 'true') {
}
}
});
} else {
// For non-logged-in users, save to localStorage directly
try {
const stored = localStorage.getItem('wekan-swimlane-heights');
let heights = stored ? JSON.parse(stored) : {};
if (!heights[boardId]) {
heights[boardId] = {};
}
heights[boardId][swimlaneId] = finalHeight;
localStorage.setItem('wekan-swimlane-heights', JSON.stringify(heights));
if (process.env.DEBUG === 'true') {
}
} catch (e) {
console.warn('Error saving swimlane height to localStorage:', e);
}
});
}
e.preventDefault();
};
@ -440,6 +612,7 @@ BlazeComponent.extendComponent({
},
}).register('swimlane');
BlazeComponent.extendComponent({
onCreated() {
this.currentBoard = Utils.getCurrentBoard();
@ -507,8 +680,305 @@ Template.swimlane.helpers({
canSeeAddList() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
lists() {
// Return per-swimlane lists for this swimlane
return this.myLists();
}
});
// Initialize sortable on DOM elements
setTimeout(() => {
const $swimlaneElements = $('.swimlane');
const $listsGroupElements = $('.list-group');
// Initialize sortable on ALL swimlane elements (even empty ones)
$swimlaneElements.each(function(index) {
const $swimlane = $(this);
const $lists = $swimlane.find('.js-list');
// Only initialize on swimlanes that have the .js-lists class (the container for lists)
if ($swimlane.hasClass('js-lists')) {
$swimlane.sortable({
connectWith: '.js-swimlane, .js-lists',
tolerance: 'pointer',
appendTo: '.board-canvas',
helper: 'clone',
items: '.js-list:not(.js-list-composer)',
placeholder: 'list placeholder',
distance: 7,
handle: '.js-list-handle',
disabled: !Utils.canModifyBoard(),
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
ui.placeholder.width(ui.helper.width());
EscapeActions.executeUpTo('popup-close');
// Try to get board component
try {
const boardComponent = BlazeComponent.getComponentForElement(ui.item[0]);
if (boardComponent && boardComponent.setIsDragging) {
boardComponent.setIsDragging(true);
}
} catch (e) {
// Silent fail
}
},
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) {
return;
}
let list;
try {
list = Blaze.getData(listDomElement);
} catch (error) {
return;
}
if (!list) {
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) {
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
}
// Allow reordering within the same swimlane by not canceling the sortable
try {
Lists.update(list._id, {
$set: updateData,
});
} catch (error) {
return;
}
// Try to get board component
try {
const boardComponent = BlazeComponent.getComponentForElement(ui.item[0]);
if (boardComponent && boardComponent.setIsDragging) {
boardComponent.setIsDragging(false);
}
} 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');
});
}
});
}
});
// Initialize sortable on ALL listsGroup elements (even empty ones)
$listsGroupElements.each(function(index) {
const $listsGroup = $(this);
const $lists = $listsGroup.find('.js-list');
// Only initialize on listsGroup elements that have the .js-lists class
if ($listsGroup.hasClass('js-lists')) {
$listsGroup.sortable({
connectWith: '.js-swimlane, .js-lists',
tolerance: 'pointer',
appendTo: '.board-canvas',
helper: 'clone',
items: '.js-list:not(.js-list-composer)',
placeholder: 'list placeholder',
distance: 7,
handle: '.js-list-handle',
disabled: !Utils.canModifyBoard(),
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
ui.placeholder.width(ui.helper.width());
EscapeActions.executeUpTo('popup-close');
// Try to get board component
try {
const boardComponent = BlazeComponent.getComponentForElement(ui.item[0]);
if (boardComponent && boardComponent.setIsDragging) {
boardComponent.setIsDragging(true);
}
} catch (e) {
// Silent fail
}
},
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) {
return;
}
let list;
try {
list = Blaze.getData(listDomElement);
} catch (error) {
return;
}
if (!list) {
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) {
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
}
// Allow reordering within the same swimlane by not canceling the sortable
try {
Lists.update(list._id, {
$set: updateData,
});
} catch (error) {
return;
}
// Try to get board component
try {
const boardComponent = BlazeComponent.getComponentForElement(ui.item[0]);
if (boardComponent && boardComponent.setIsDragging) {
boardComponent.setIsDragging(false);
}
} 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');
});
}
});
}
});
}, 1000);
BlazeComponent.extendComponent({
currentCardIsInThisList(listId, swimlaneId) {
return currentCardIsInThisList(listId, swimlaneId);
@ -539,14 +1009,58 @@ BlazeComponent.extendComponent({
const boardComponent = this.parentComponent();
const $listsDom = this.$('.js-lists');
if (!Utils.getCurrentCardId()) {
boardComponent.scrollLeft();
}
initSortable(boardComponent, $listsDom);
// Try a simpler approach for listsGroup too
// Wait for DOM to be ready
setTimeout(() => {
const $lists = this.$('.js-list');
const $parent = $lists.parent();
if ($lists.length > 0) {
// Check for drag handles
const $handles = $parent.find('.js-list-handle');
// Test if drag handles are clickable
$handles.on('click', function(e) {
e.preventDefault();
e.stopPropagation();
});
$parent.sortable({
connectWith: '.js-swimlane, .js-lists',
tolerance: 'pointer',
appendTo: '.board-canvas',
helper: 'clone',
items: '.js-list:not(.js-list-composer)',
placeholder: 'list placeholder',
distance: 7,
handle: '.js-list-handle',
disabled: !Utils.canModifyBoard(),
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
ui.placeholder.width(ui.helper.width());
EscapeActions.executeUpTo('popup-close');
boardComponent.setIsDragging(true);
},
stop(evt, ui) {
boardComponent.setIsDragging(false);
}
});
} else {
}
}, 100);
},
}).register('listsGroup');
class MoveSwimlaneComponent extends BlazeComponent {
serverMethod = 'moveSwimlane';

View file

@ -1,7 +1,7 @@
template(name="userAvatar")
a.member(class="js-{{#if assignee}}assignee{{else}}member{{/if}}" title="{{userData.profile.fullname}} ({{userData.username}}) {{_ memberType}}")
if userData.profile.avatarUrl
img.avatar.avatar-image(src="{{userData.profile.avatarUrl}}")
img.avatar.avatar-image(src="{{avatarUrl}}")
else
+userAvatarInitials(userId=userData._id)
@ -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
| ✅

View file

@ -15,6 +15,21 @@ Template.userAvatar.helpers({
});
},
avatarUrl() {
const user = ReactiveCache.getUser(this.userId, { fields: { profile: 1 } });
const base = (user && user.profile && user.profile.avatarUrl) || '';
if (!base) return '';
// Append current boardId when available so public viewers can access avatars on public boards
try {
const boardId = Utils.getCurrentBoardId && Utils.getCurrentBoardId();
if (boardId) {
const sep = base.includes('?') ? '&' : '?';
return `${base}${sep}boardId=${encodeURIComponent(boardId)}`;
}
} catch (_) {}
return base;
},
memberType() {
const user = ReactiveCache.getUser(this.userId);
return user && user.isBoardAdmin() ? 'admin' : 'normal';
@ -179,7 +194,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 +235,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'() {

View file

@ -12,11 +12,6 @@ template(name="headerUserBar")
template(name="memberMenuPopup")
ul.pop-over-list
// Bookmarks at the very top
li
a.js-open-bookmarks
| 🔖
| {{_ 'bookmarks'}}
with currentUser
li
a.js-my-cards(href="{{pathFor 'my-cards'}}")
@ -32,6 +27,7 @@ template(name="memberMenuPopup")
| {{_ 'globalSearch-title'}}
li
a(href="{{pathFor 'home'}}")
| 🏠
| 🏠
| {{_ 'all-boards'}}
li

View file

@ -73,6 +73,10 @@ Blaze.registerHelper('canModifyCard', () =>
Utils.canModifyCard(),
);
Blaze.registerHelper('canMoveCard', () =>
Utils.canMoveCard(),
);
Blaze.registerHelper('canModifyBoard', () =>
Utils.canModifyBoard(),
);

View file

@ -0,0 +1,73 @@
import { ReactiveCache } from '/imports/reactiveCache';
BoardMultiSelection = {
_selectedBoards: new ReactiveVar([]),
_isActive: new ReactiveVar(false),
reset() {
this._selectedBoards.set([]);
},
isActive() {
return this._isActive.get();
},
count() {
return this._selectedBoards.get().length;
},
isEmpty() {
return this.count() === 0;
},
getSelectedBoardIds() {
return this._selectedBoards.get();
},
activate() {
if (!this.isActive()) {
this._isActive.set(true);
Tracker.flush();
}
},
disable() {
if (this.isActive()) {
this._isActive.set(false);
this.reset();
}
},
add(boardIds) {
return this.toggle(boardIds, { add: true, remove: false });
},
remove(boardIds) {
return this.toggle(boardIds, { add: false, remove: true });
},
toogle(boardIds) {
return this.toggle(boardIds, { add: true, remove: true });
},
toggle(boardIds, { add, remove } = {}) {
boardIds = _.isString(boardIds) ? [boardIds] : boardIds;
let selectedBoards = this._selectedBoards.get();
boardIds.forEach(boardId => {
const index = selectedBoards.indexOf(boardId);
if (index > -1 && remove) {
selectedBoards = selectedBoards.filter(id => id !== boardId);
} else if (index === -1 && add) {
selectedBoards.push(boardId);
}
});
this._selectedBoards.set(selectedBoards);
},
isSelected(boardId) {
return this._selectedBoards.get().includes(boardId);
},
};

View file

@ -29,21 +29,86 @@ export class CardSearchPagedComponent extends BlazeComponent {
const that = this;
this.subscriptionCallbacks = {
onReady() {
that.getResults();
that.searching.set(false);
that.hasResults.set(true);
that.serverError.set(false);
if (process.env.DEBUG === 'true') {
console.log('Subscription ready, getting results...');
console.log('Subscription ready - sessionId:', that.sessionId);
}
// Wait for session data to be available (with timeout)
let waitCount = 0;
const maxWaitCount = 50; // 10 seconds max wait
const waitForSessionData = () => {
waitCount++;
const sessionData = that.getSessionData();
if (process.env.DEBUG === 'true') {
console.log('waitForSessionData - attempt', waitCount, 'session data:', sessionData);
}
if (sessionData) {
const results = that.getResults();
if (process.env.DEBUG === 'true') {
console.log('Search results count:', results ? results.length : 0);
}
// If no results and this is a due cards search, try to retry
if ((!results || results.length === 0) && that.searchRetryCount !== undefined && that.searchRetryCount < that.maxRetries) {
if (process.env.DEBUG === 'true') {
console.log('No results found, retrying search...');
}
that.searchRetryCount++;
Meteor.setTimeout(() => {
if (that.performSearch) {
that.performSearch();
}
}, 500);
return;
}
that.searching.set(false);
that.hasResults.set(true);
that.serverError.set(false);
} else if (waitCount < maxWaitCount) {
// Session data not available yet, wait a bit more
if (process.env.DEBUG === 'true') {
console.log('Session data not available yet, waiting... (attempt', waitCount, 'of', maxWaitCount, ')');
}
Meteor.setTimeout(waitForSessionData, 200);
} else {
// Timeout reached, try fallback search
if (process.env.DEBUG === 'true') {
console.log('Timeout reached waiting for session data, trying fallback search');
}
const results = that.getResults();
if (process.env.DEBUG === 'true') {
console.log('Fallback search results count:', results ? results.length : 0);
}
if (results && results.length > 0) {
that.searching.set(false);
that.hasResults.set(true);
that.serverError.set(false);
} else {
that.searching.set(false);
that.hasResults.set(false);
that.serverError.set(true);
}
}
};
// Start waiting for session data
Meteor.setTimeout(waitForSessionData, 100);
},
onError(error) {
if (process.env.DEBUG === 'true') {
console.log('Subscription error:', error);
console.log('Error.reason:', error.reason);
console.log('Error.message:', error.message);
console.log('Error.stack:', error.stack);
}
that.searching.set(false);
that.hasResults.set(false);
that.serverError.set(true);
// eslint-disable-next-line no-console
//console.log('Error.reason:', error.reason);
// eslint-disable-next-line no-console
//console.log('Error.message:', error.message);
// eslint-disable-next-line no-console
//console.log('Error.stack:', error.stack);
},
};
}
@ -62,9 +127,28 @@ export class CardSearchPagedComponent extends BlazeComponent {
}
getSessionData(sessionId) {
return ReactiveCache.getSessionData({
sessionId: sessionId || SessionData.getSessionId(),
const sessionIdToUse = sessionId || SessionData.getSessionId();
if (process.env.DEBUG === 'true') {
console.log('getSessionData - looking for sessionId:', sessionIdToUse);
}
// Try using the raw SessionData collection instead of ReactiveCache
const sessionData = SessionData.findOne({
sessionId: sessionIdToUse,
});
if (process.env.DEBUG === 'true') {
console.log('getSessionData - found session data (raw):', sessionData);
}
// Also try ReactiveCache for comparison
const reactiveSessionData = ReactiveCache.getSessionData({
sessionId: sessionIdToUse,
});
if (process.env.DEBUG === 'true') {
console.log('getSessionData - found session data (reactive):', reactiveSessionData);
}
return sessionData || reactiveSessionData;
}
getResults() {
@ -72,33 +156,87 @@ export class CardSearchPagedComponent extends BlazeComponent {
// console.log('getting results');
this.sessionData = this.getSessionData();
// eslint-disable-next-line no-console
console.log('session data:', this.sessionData);
if (process.env.DEBUG === 'true') {
console.log('getResults - sessionId:', this.sessionId);
console.log('getResults - session data:', this.sessionData);
}
const cards = [];
this.sessionData.cards.forEach(cardId => {
cards.push(ReactiveCache.getCard(cardId));
});
this.queryErrors = this.sessionData.errors;
if (this.sessionData && this.sessionData.cards) {
if (process.env.DEBUG === 'true') {
console.log('getResults - cards array length:', this.sessionData.cards.length);
}
this.sessionData.cards.forEach(cardId => {
const card = ReactiveCache.getCard(cardId);
if (process.env.DEBUG === 'true') {
console.log('getResults - card:', cardId, card);
}
cards.push(card);
});
this.queryErrors = this.sessionData.errors || [];
} else {
if (process.env.DEBUG === 'true') {
console.log('getResults - no sessionData or no cards array, trying direct card search');
}
// Fallback: try to get cards directly from the client-side collection
// Use a more efficient query with limit and sort
const selector = {
type: 'cardType-card',
dueAt: { $exists: true, $nin: [null, ''] }
};
const options = {
sort: { dueAt: 1 }, // Sort by due date ascending (oldest first)
limit: 100 // Limit to 100 cards for performance
};
const allCards = Cards.find(selector, options).fetch();
if (process.env.DEBUG === 'true') {
console.log('getResults - direct card search found:', allCards ? allCards.length : 0, 'cards');
}
if (allCards && allCards.length > 0) {
allCards.forEach(card => {
if (card && card._id) {
cards.push(card);
}
});
}
this.queryErrors = [];
}
if (this.queryErrors.length) {
// console.log('queryErrors:', this.queryErrorMessages());
this.hasQueryErrors.set(true);
// return null;
}
this.debug.set(new QueryDebug(this.sessionData.debug));
console.log('debug:', this.debug.get().get());
console.log('debug.show():', this.debug.get().show());
console.log('debug.showSelector():', this.debug.get().showSelector());
this.debug.set(new QueryDebug(this.sessionData ? this.sessionData.debug : null));
if (process.env.DEBUG === 'true') {
console.log('debug:', this.debug.get().get());
console.log('debug.show():', this.debug.get().show());
console.log('debug.showSelector():', this.debug.get().showSelector());
}
if (cards) {
this.totalHits = this.sessionData.totalHits;
this.resultsCount = cards.length;
this.resultsStart = this.sessionData.lastHit - this.resultsCount + 1;
this.resultsEnd = this.sessionData.lastHit;
this.resultsHeading.set(this.getResultsHeading());
this.results.set(cards);
this.hasNextPage.set(this.sessionData.lastHit < this.sessionData.totalHits);
this.hasPreviousPage.set(
this.sessionData.lastHit - this.sessionData.resultsCount > 0,
);
if (this.sessionData) {
this.totalHits = this.sessionData.totalHits || 0;
this.resultsCount = cards.length;
this.resultsStart = this.sessionData.lastHit - this.resultsCount + 1;
this.resultsEnd = this.sessionData.lastHit;
this.resultsHeading.set(this.getResultsHeading());
this.results.set(cards);
this.hasNextPage.set(this.sessionData.lastHit < this.sessionData.totalHits);
this.hasPreviousPage.set(
this.sessionData.lastHit - this.sessionData.resultsCount > 0,
);
} else {
this.totalHits = cards.length;
this.resultsCount = cards.length;
this.resultsStart = 1;
this.resultsEnd = cards.length;
this.resultsHeading.set(this.getResultsHeading());
this.results.set(cards);
this.hasNextPage.set(false);
this.hasPreviousPage.set(false);
}
return cards;
}
@ -113,13 +251,29 @@ export class CardSearchPagedComponent extends BlazeComponent {
}
getSubscription(queryParams) {
return Meteor.subscribe(
if (process.env.DEBUG === 'true') {
console.log('Subscribing to globalSearch with:', {
sessionId: this.sessionId,
params: queryParams.params,
text: queryParams.text
});
}
// Subscribe to both globalSearch and sessionData
const globalSearchHandle = Meteor.subscribe(
'globalSearch',
this.sessionId,
queryParams.params,
queryParams.text,
this.subscriptionCallbacks,
);
const sessionDataHandle = Meteor.subscribe('sessionData', this.sessionId);
if (process.env.DEBUG === 'true') {
console.log('Subscribed to sessionData with sessionId:', this.sessionId);
}
return globalSearchHandle;
}
runGlobalSearch(queryParams) {

View file

@ -1,32 +1,27 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import {
formatDateTime,
formatDate,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar
} from '/imports/lib/dateUtils';
// Helper function to get time format for 24 hours
function adjustedTimeFormat() {
return 'HH:mm';
// Helper to check if a date is valid
function isValidDate(date) {
return date instanceof Date && !isNaN(date);
}
// .replace(/HH/i, 'H');
// Format date as YYYY-MM-DD
function formatDate(date) {
if (!isValidDate(date)) return '';
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// Format time as HH:mm
function formatTime(date) {
if (!isValidDate(date)) return '';
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}
export class DatePicker extends BlazeComponent {
template() {
@ -76,10 +71,10 @@ export class DatePicker extends BlazeComponent {
return '';
}
dateFormat() {
return 'L';
return 'YYYY-MM-DD';
}
timeFormat() {
return 'LT';
return 'HH:mm';
}
events() {
@ -89,7 +84,8 @@ export class DatePicker extends BlazeComponent {
// Native HTML date input validation
const dateValue = this.find('#date').value;
if (dateValue) {
const dateObj = new Date(dateValue);
// HTML date input format is always YYYY-MM-DD
const dateObj = new Date(dateValue + 'T12:00:00');
if (isValidDate(dateObj)) {
this.error.set('');
} else {
@ -101,7 +97,8 @@ export class DatePicker extends BlazeComponent {
// Native HTML time input validation
const timeValue = this.find('#time').value;
if (timeValue) {
const timeObj = new Date(`1970-01-01T${timeValue}`);
// HTML time input format is always HH:mm
const timeObj = new Date(`1970-01-01T${timeValue}:00`);
if (isValidDate(timeObj)) {
this.error.set('');
} else {
@ -121,7 +118,9 @@ export class DatePicker extends BlazeComponent {
return;
}
const newCompleteDate = new Date(`${dateValue}T${timeValue}`);
// Combine date and time: HTML date input is YYYY-MM-DD, time input is HH:mm
const dateTimeString = `${dateValue}T${timeValue}:00`;
const newCompleteDate = new Date(dateTimeString);
if (!isValidDate(newCompleteDate)) {
this.error.set('invalid');

View file

@ -0,0 +1,94 @@
import { Meteor } from 'meteor/meteor';
/**
* Client-side interface for fixing duplicate lists
*/
export const fixDuplicateLists = {
/**
* Get a report of all boards with duplicate lists/swimlanes
*/
async getReport() {
try {
const result = await Meteor.callAsync('fixDuplicateLists.getReport');
return result;
} catch (error) {
console.error('Error getting duplicate lists report:', error);
throw error;
}
},
/**
* Fix duplicate lists for a specific board
*/
async fixBoard(boardId) {
try {
const result = await Meteor.callAsync('fixDuplicateLists.fixBoard', boardId);
console.log(`Fixed duplicate lists for board ${boardId}:`, result);
return result;
} catch (error) {
console.error(`Error fixing board ${boardId}:`, error);
throw error;
}
},
/**
* Fix duplicate lists for all boards
*/
async fixAllBoards() {
try {
console.log('Starting fix for all boards...');
const result = await Meteor.callAsync('fixDuplicateLists.fixAllBoards');
console.log('Fix completed:', result);
return result;
} catch (error) {
console.error('Error fixing all boards:', error);
throw error;
}
},
/**
* Interactive fix with user confirmation
*/
async interactiveFix() {
try {
// Get report first
console.log('Getting duplicate lists report...');
const report = await this.getReport();
if (report.boardsWithDuplicates === 0) {
console.log('No duplicate lists found!');
return { message: 'No duplicate lists found!' };
}
console.log(`Found ${report.boardsWithDuplicates} boards with duplicate lists:`);
report.report.forEach(board => {
console.log(`- Board "${board.boardTitle}" (${board.boardId}): ${board.duplicateSwimlanes} duplicate swimlanes, ${board.duplicateLists} duplicate lists`);
});
// Ask for confirmation
const confirmed = confirm(
`Found ${report.boardsWithDuplicates} boards with duplicate lists. ` +
`This will fix ${report.report.reduce((sum, board) => sum + board.duplicateSwimlanes + board.duplicateLists, 0)} duplicates. ` +
'Continue?'
);
if (!confirmed) {
return { message: 'Fix cancelled by user' };
}
// Perform the fix
const result = await this.fixAllBoards();
return result;
} catch (error) {
console.error('Error in interactive fix:', error);
throw error;
}
}
};
// Make it available globally for console access
if (typeof window !== 'undefined') {
window.fixDuplicateLists = fixDuplicateLists;
}

View file

@ -646,6 +646,23 @@ class MigrationManager {
}
}
/**
* Fix boards that are stuck in migration loop
*/
fixStuckBoards() {
try {
Meteor.call('boardMigration.fixStuckBoards', (error, result) => {
if (error) {
console.error('Failed to fix stuck boards:', error);
} else {
console.log('Fix stuck boards result:', result);
}
});
} catch (error) {
console.error('Error fixing stuck boards:', error);
}
}
/**
* Start migration process using cron system
*/

View file

@ -101,6 +101,10 @@ window.Popup = new (class {
// our internal dependency, and since we just changed the top element of
// our internal stack, the popup will be updated with the new data.
if (!self.isOpen()) {
if (!Template[popupName]) {
console.error('Template not found:', popupName);
return;
}
self.current = Blaze.renderWithData(
self.template,
() => {
@ -214,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();

View file

@ -3,43 +3,25 @@ import DOMPurify from 'dompurify';
// Centralized secure DOMPurify configuration to prevent XSS and CSS injection attacks
export function getSecureDOMPurifyConfig() {
return {
// Block dangerous elements that can cause XSS and CSS injection
FORBID_TAGS: [
'svg', 'defs', 'use', 'g', 'symbol', 'marker', 'pattern', 'mask', 'clipPath',
'linearGradient', 'radialGradient', 'stop', 'animate', 'animateTransform',
'animateMotion', 'set', 'switch', 'foreignObject', 'script', 'style', 'link',
'meta', 'iframe', 'object', 'embed', 'applet', 'form', 'input', 'textarea',
'select', 'option', 'button', 'label', 'fieldset', 'legend', 'frameset',
'frame', 'noframes', 'base', 'basefont', 'isindex', 'dir', 'menu', 'menuitem'
],
// Block dangerous attributes that can cause XSS and CSS injection
FORBID_ATTR: [
'xlink:href', 'href', 'onload', 'onerror', 'onclick', 'onmouseover',
'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect',
'onunload', 'onresize', 'onscroll', 'onkeydown', 'onkeyup', 'onkeypress',
'onmousedown', 'onmouseup', 'onmouseover', 'onmouseout', 'onmousemove',
'ondblclick', 'oncontextmenu', 'onwheel', 'ontouchstart', 'ontouchend',
'ontouchmove', 'ontouchcancel', 'onabort', 'oncanplay', 'oncanplaythrough',
'ondurationchange', 'onemptied', 'onended', 'onerror', 'onloadeddata',
'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying',
'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled',
'onsuspend', 'ontimeupdate', 'onvolumechange', 'onwaiting', 'onbeforeunload',
'onhashchange', 'onpagehide', 'onpageshow', 'onpopstate', 'onstorage',
'onunload', 'style', 'class', 'id', 'data-*', 'aria-*'
],
// Allow only safe image formats and protocols
// Allow common markdown elements including anchor tags
ALLOWED_TAGS: ['a', 'p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'pre', 'code', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr', 'div', 'span', 's'],
// Allow safe attributes including href for anchor tags
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'width', 'height', 'target', 'rel'],
// Allow safe protocols for links
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
// Remove dangerous protocols
// Allow unknown protocols but be cautious
ALLOW_UNKNOWN_PROTOCOLS: false,
// Sanitize URLs to prevent malicious content loading
// Sanitize DOM for security
SANITIZE_DOM: true,
// Remove dangerous elements completely
KEEP_CONTENT: false,
// Additional security measures
ADD_ATTR: [],
// Keep content but sanitize it
KEEP_CONTENT: true,
// Block dangerous elements that can cause XSS
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'applet', 'svg', 'defs', 'use', 'g', 'symbol', 'marker', 'pattern', 'mask', 'clipPath', 'linearGradient', 'radialGradient', 'stop', 'animate', 'animateTransform', 'animateMotion', 'set', 'switch', 'foreignObject', 'link', 'meta', 'form', 'input', 'textarea', 'select', 'option', 'button', 'label', 'fieldset', 'legend', 'frameset', 'frame', 'noframes', 'base', 'basefont', 'isindex', 'dir', 'menu', 'menuitem'],
// Block dangerous attributes but allow safe href
FORBID_ATTR: ['xlink:href', 'onload', 'onerror', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect', 'onunload', 'onresize', 'onscroll', 'onkeydown', 'onkeyup', 'onkeypress', 'onmousedown', 'onmouseup', 'onmouseover', 'onmouseout', 'onmousemove', 'ondblclick', 'oncontextmenu', 'onwheel', 'ontouchstart', 'ontouchend', 'ontouchmove', 'ontouchcancel', 'onabort', 'oncanplay', 'oncanplaythrough', 'ondurationchange', 'onemptied', 'onended', 'onerror', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying', 'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled', 'onsuspend', 'ontimeupdate', 'onvolumechange', 'onwaiting', 'onbeforeunload', 'onhashchange', 'onpagehide', 'onpageshow', 'onpopstate', 'onstorage', 'onunload', 'style', 'class', 'id', 'data-*', 'aria-*'],
// Block data URIs that could contain malicious content
ALLOW_DATA_ATTR: false,
// Custom hook to further sanitize content
// Custom hooks for additional security
HOOKS: {
uponSanitizeElement: function(node, data) {
// Block any remaining dangerous elements
@ -51,14 +33,37 @@ export function getSecureDOMPurifyConfig() {
return false;
}
// Block img tags with SVG data URIs
// Block img tags with SVG data URIs that could contain malicious JavaScript
if (node.tagName && node.tagName.toLowerCase() === 'img') {
const src = node.getAttribute('src');
if (src && (src.startsWith('data:image/svg') || src.endsWith('.svg'))) {
if (process.env.DEBUG === 'true') {
console.warn('Blocked potentially malicious SVG image:', src);
if (src) {
// Block all SVG data URIs to prevent XSS via embedded JavaScript
if (src.startsWith('data:image/svg') || src.endsWith('.svg')) {
if (process.env.DEBUG === 'true') {
console.warn('Blocked potentially malicious SVG image:', src);
}
return false;
}
// Additional check for base64 encoded SVG with script tags
if (src.startsWith('data:image/svg+xml;base64,')) {
try {
const base64Content = src.split(',')[1];
const decodedContent = atob(base64Content);
if (decodedContent.includes('<script') || decodedContent.includes('javascript:')) {
if (process.env.DEBUG === 'true') {
console.warn('Blocked SVG with embedded JavaScript:', src.substring(0, 100) + '...');
}
return false;
}
} catch (e) {
// If decoding fails, block it as a safety measure
if (process.env.DEBUG === 'true') {
console.warn('Blocked malformed SVG data URI:', src);
}
return false;
}
}
return false;
}
}
@ -100,6 +105,19 @@ export function getSecureDOMPurifyConfig() {
return false;
}
// Allow href attribute for anchor tags only
if (data.attrName === 'href') {
// Only allow href on anchor tags
if (node.tagName && node.tagName.toLowerCase() === 'a') {
return true;
} else {
if (process.env.DEBUG === 'true') {
console.warn('Blocked href attribute on non-anchor element:', node.tagName);
}
return false;
}
}
return true;
}
}

View file

@ -214,6 +214,15 @@ Utils = {
);
return ret;
},
canMoveCard() {
const currentUser = ReactiveCache.getCurrentUser();
const ret = (
currentUser &&
currentUser.isBoardMember() &&
!currentUser.isCommentOnly()
);
return ret;
},
canModifyBoard() {
const currentUser = ReactiveCache.getCurrentUser();
const ret = (
@ -231,9 +240,21 @@ Utils = {
window.location.reload();
},
setBoardView(view) {
currentUser = ReactiveCache.getCurrentUser();
const currentUser = ReactiveCache.getCurrentUser();
if (currentUser) {
ReactiveCache.getCurrentUser().setBoardView(view);
// Update localStorage first
window.localStorage.setItem('boardView', view);
// Update user profile via Meteor method
Meteor.call('setBoardView', view, (error) => {
if (error) {
console.error('[setBoardView] Update failed:', error);
} else {
// Reload to apply the view change
Utils.reload();
}
});
} else if (view === 'board-view-swimlanes') {
window.localStorage.setItem('boardView', 'board-view-swimlanes'); //true
Utils.reload();
@ -433,10 +454,14 @@ Utils = {
// we can easily debug with a small window of desktop browser. :-)
isMiniScreen() {
this.windowResizeDep.depend();
// Also depend on mobile mode changes to make this reactive
Session.get('wekan-mobile-mode');
// Show mobile view when:
// 1. Screen width is 800px or less (matches CSS media queries)
// 2. Mobile phones in portrait mode
// 3. iPad in very small screens (≤ 600px)
// 4. All iPhone models by default (including largest models), but respect user preference
const isSmallScreen = window.innerWidth <= 800;
const isVerySmallScreen = window.innerWidth <= 600;
const isPortrait = window.innerWidth < window.innerHeight || window.matchMedia("(orientation: portrait)").matches;
@ -445,13 +470,19 @@ Utils = {
const isIPad = /iPad/i.test(navigator.userAgent);
const isUbuntuTouch = /Ubuntu/i.test(navigator.userAgent);
// For iPhone: always show mobile view regardless of orientation
// For other mobile phones: show mobile view in portrait, desktop view in landscape
// For iPad: show mobile view only in very small screens (≤ 600px)
// For Ubuntu Touch: smartphones behave like mobile phones, tablets like iPad
// For desktop: show mobile view when screen width <= 800px
// Check if user has explicitly set mobile mode preference
const userMobileMode = this.getMobileMode();
// For iPhone: default to mobile view, but respect user's mobile mode toggle preference
// This ensures all iPhone models (including iPhone 15 Pro Max, 14 Pro Max, etc.) start with mobile view
// but users can still switch to desktop mode if they prefer
if (isIPhone) {
return true; // iPhone: always mobile view
// If user has explicitly set a preference, respect it
if (userMobileMode !== null && userMobileMode !== undefined) {
return userMobileMode;
}
// Otherwise, default to mobile view for iPhones
return true;
} else if (isMobilePhone) {
return isPortrait; // Other mobile phones: portrait = mobile, landscape = desktop
} else if (isIPad) {
@ -496,30 +527,14 @@ Utils = {
// returns if desktop drag handles are enabled
isShowDesktopDragHandles() {
if (this.isTouchScreen()) {
return true;
/*
const currentUser = ReactiveCache.getCurrentUser();
if (currentUser) {
return (currentUser.profile || {}).showDesktopDragHandles;
} else if (window.localStorage.getItem('showDesktopDragHandles')) {
//
if (window.localStorage.getItem('showDesktopDragHandles')) {
return true;
} else {
return false;
*/
} else {
return false;
}
// Always show drag handles on all displays
return true;
},
// returns if mini screen or desktop drag handles
isTouchScreenOrShowDesktopDragHandles() {
// Always enable drag handles for mobile screens (touch devices)
return this.isTouchScreen() || this.isMiniScreen();
//return this.isTouchScreen() || this.isShowDesktopDragHandles();
//return this.isShowDesktopDragHandles();
// Always enable drag handles for all displays
return true;
},
calculateIndexData(prevData, nextData, nItems = 1) {

View file

@ -1,6 +1,7 @@
import { TAPi18n } from '/imports/i18n';
const passwordField = AccountsTemplates.removeField('password');
passwordField.autocomplete = 'current-password';
const emailField = AccountsTemplates.removeField('email');
let disableRegistration = false;
let disableForgotPassword = false;
@ -61,6 +62,14 @@ AccountsTemplates.addFields([
},
emailField,
passwordField,
{
_id: 'password_again',
type: 'password',
displayName: 'Password (again)',
required: true,
minLength: 6,
autocomplete: 'new-password',
},
{
_id: 'invitationcode',
type: 'text',

View file

@ -0,0 +1,103 @@
# Building the Wekan snap without timeouts
This guide focuses on macOS hosts (Multipass VM) and common timeout fixes. It also applies to Linux hosts with LXD.
## Quick options
- Fastest: use Canonical builders (no local VM)
```zsh
# One-time: login to the store (required for remote-build)
snapcraft login
# Build for amd64 on Canonical builders
snapcraft remote-build --build-for=amd64
```
- Local VM (macOS + Multipass): increase resources and build verbosely
```zsh
# Give the builder more CPU/RAM/disk to avoid sluggish downloads
export SNAPCRAFT_BUILD_ENVIRONMENT=hosted-multipass
export SNAPCRAFT_BUILD_ENVIRONMENT_CPU=4
export SNAPCRAFT_BUILD_ENVIRONMENT_MEMORY=8G
export SNAPCRAFT_BUILD_ENVIRONMENT_DISK=40G
# Clean previous state and build
snapcraft clean
snapcraft --verbose --debug
```
## What changed to reduce timeouts
- Downloads in `wekan` part now retry with exponential backoff.
- `caddy` part now attempts APT with retries and falls back to a static binary from the official GitHub release if APT is slow or unreachable.
These changes make the build resilient to transient network issues and slow mirrors.
## Diagnosing where it stalls
- Run a single step for a part to reproduce:
```zsh
snapcraft pull wekan -v
snapcraft build wekan -v
```
- Drop into the build environment when it fails:
```zsh
snapcraft --debug
# Then run the failing commands manually
```
## Tips for macOS + Multipass
- Check networking:
```zsh
multipass list
multipass exec snapcraft-*-wekan -- ping -c2 github.com
```
- If the instance looks wedged, recreate it:
```zsh
snapcraft clean --use-lxd || true # harmless on macOS
snapcraft clean --step pull
multipass delete --purge $(multipass list | awk '/snapcraft-/{print $1}')
snapcraft --verbose
```
## Linux hosts (optional)
On Linux, using LXD is often faster and more reliable than Multipass:
```bash
sudo snap install lxd --channel=5.21/stable
newgrp lxd
snapcraft --use-lxd -v
```
## Common environment knobs
- Proxy/mirror environments inside the build VM if needed:
```zsh
export http_proxy=http://proxy.example:3128
export https_proxy=$http_proxy
export SNAPCRAFT_PROXY_HTTP=$http_proxy
export SNAPCRAFT_PROXY_HTTPS=$https_proxy
```
- Speed up apt by pinning retries (already set in the recipe) or switching to a closer mirror by customizing sources in an override if needed.
## Cleaning up caches
If repeated attempts keep hitting corrupt downloads, clean Snapcraft caches:
```zsh
snapcraft clean --destructive-mode || true
rm -rf ~/.cache/snapcraft/*
```
## Reporting
If you still hit timeouts, capture and share:
- The exact step (pull/build/stage/prime) and part name
- Output of `snapcraft --verbose --debug`
- Host OS and Snapcraft version: `snapcraft --version`
- Multipass resources: `multipass list`

View file

@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
Right click and download files 1-4:
1. [wekan-8.05-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.05/wekan-8.05-amd64-windows.zip)
1. [wekan-8.17-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.17/wekan-8.17-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.05-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files:
7. Unzip `wekan-8.17-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files:
```
bundle (directory)

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "deleted comment %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Templates",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@ -190,7 +202,9 @@
"board-view-collapse": "Collapse",
"board-view-gantt": "Gantt",
"board-view-lists": "Lists",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel",
"card-archived": "This card is moved to Archive.",
"board-archived": "This board is moved to Archive.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Create",
"createBoardPopup-title": "Create Board",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Import board",
"createLabelPopup-title": "Create Label",
"createCustomField": "Create Field",
@ -354,6 +369,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",
"decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Delete",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edit Notification",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "deleted comment %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Templates",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@ -190,7 +202,9 @@
"board-view-collapse": "Collapse",
"board-view-gantt": "Gantt",
"board-view-lists": "Lists",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel",
"card-archived": "This card is moved to Archive.",
"board-archived": "This board is moved to Archive.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Create",
"createBoardPopup-title": "Create Board",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Import board",
"createLabelPopup-title": "Create Label",
"createCustomField": "Create Field",
@ -354,6 +369,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",
"decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Delete",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edit Notification",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "deleted comment %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Templates",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@ -190,7 +202,9 @@
"board-view-collapse": "Collapse",
"board-view-gantt": "Gantt",
"board-view-lists": "Lists",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel",
"card-archived": "This card is moved to Archive.",
"board-archived": "This board is moved to Archive.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Create",
"createBoardPopup-title": "Create Board",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Import board",
"createLabelPopup-title": "Create Label",
"createCustomField": "Create Field",
@ -354,6 +369,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",
"decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Delete",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edit Notification",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "deleted comment %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Templates",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@ -190,7 +202,9 @@
"board-view-collapse": "Collapse",
"board-view-gantt": "Gantt",
"board-view-lists": "Lists",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel",
"card-archived": "This card is moved to Archive.",
"board-archived": "This board is moved to Archive.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Create",
"createBoardPopup-title": "Create Board",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Import board",
"createLabelPopup-title": "Create Label",
"createCustomField": "Create Field",
@ -354,6 +369,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",
"decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Delete",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edit Notification",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "تعليق محذوف %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "نماذج",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "إضافة مرفق",
@ -190,7 +202,9 @@
"board-view-collapse": "انهيار",
"board-view-gantt": "Gantt",
"board-view-lists": "القائمات",
"bucket-example": "مثل « todo list » على سبيل المثال",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "إلغاء",
"card-archived": "البطاقة منقولة الى الارشيف",
"board-archived": "اللوحات منقولة الى الارشيف",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "إنشاء",
"createBoardPopup-title": "إنشاء لوحة",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "استيراد لوحة",
"createLabelPopup-title": "إنشاء علامة",
"createCustomField": "انشاء حقل",
@ -354,6 +369,10 @@
"custom-field-text": "نص",
"custom-fields": "Custom Fields",
"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",
"decline": "Decline",
"default-avatar": "صورة شخصية افتراضية",
"delete": "حذف",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "تصحيح الإشعار",
"editProfilePopup-title": "تعديل الملف الشخصي",
"email": "البريد الإلكتروني",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,8 @@
"dueCardsViewChange-choice-me": "انا",
"dueCardsViewChange-choice-all": "كل المستخدمين",
"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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "تفاصيل",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "deleted comment %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Templates",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@ -190,7 +202,9 @@
"board-view-collapse": "Collapse",
"board-view-gantt": "Gantt",
"board-view-lists": "Lists",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel",
"card-archived": "This card is moved to Archive.",
"board-archived": "This board is moved to Archive.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Create",
"createBoardPopup-title": "Create Board",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Import board",
"createLabelPopup-title": "Create Label",
"createCustomField": "Create Field",
@ -354,6 +369,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",
"decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Delete",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edit Notification",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "deleted comment %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Templates",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@ -190,7 +202,9 @@
"board-view-collapse": "Collapse",
"board-view-gantt": "Gantt",
"board-view-lists": "Lists",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel",
"card-archived": "This card is moved to Archive.",
"board-archived": "This board is moved to Archive.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Create",
"createBoardPopup-title": "Create Board",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Import board",
"createLabelPopup-title": "Create Label",
"createCustomField": "Create Field",
@ -354,6 +369,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",
"decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Delete",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edit Notification",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "deleted comment %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Templates",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@ -190,7 +202,9 @@
"board-view-collapse": "Collapse",
"board-view-gantt": "Gantt",
"board-view-lists": "Lists",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel",
"card-archived": "This card is moved to Archive.",
"board-archived": "This board is moved to Archive.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Create",
"createBoardPopup-title": "Create Board",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Import board",
"createLabelPopup-title": "Create Label",
"createCustomField": "Create Field",
@ -354,6 +369,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",
"decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Delete",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edit Notification",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "deleted comment %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Templates",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@ -190,7 +202,9 @@
"board-view-collapse": "Collapse",
"board-view-gantt": "Gantt",
"board-view-lists": "Lists",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel",
"card-archived": "This card is moved to Archive.",
"board-archived": "This board is moved to Archive.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Create",
"createBoardPopup-title": "Create Board",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Import board",
"createLabelPopup-title": "Create Label",
"createCustomField": "Create Field",
@ -354,6 +369,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",
"decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Delete",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edit Notification",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "deleted comment %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Templates",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@ -190,7 +202,9 @@
"board-view-collapse": "Collapse",
"board-view-gantt": "Gantt",
"board-view-lists": "Lists",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel",
"card-archived": "This card is moved to Archive.",
"board-archived": "This board is moved to Archive.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Create",
"createBoardPopup-title": "Create Board",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Import board",
"createLabelPopup-title": "Create Label",
"createCustomField": "Create Field",
@ -354,6 +369,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",
"decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Delete",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edit Notification",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "изтрит коментар %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Шаблони",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Добави прикачен файл",
@ -190,7 +202,9 @@
"board-view-collapse": "Събери",
"board-view-gantt": "План",
"board-view-lists": "Списъци",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Отмени",
"card-archived": "Тази карта е преместена в Архива.",
"board-archived": "Това табло е преместено в Архива.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Създай",
"createBoardPopup-title": "Създай Табло",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Импортирай Табло",
"createLabelPopup-title": "Създай Табло",
"createCustomField": "Създай Поле",
@ -354,6 +369,10 @@
"custom-field-text": "Текст",
"custom-fields": "Собствени полета",
"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",
"decline": "Отказ",
"default-avatar": "Основен аватар",
"delete": "Изтрий",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Промени известията",
"editProfilePopup-title": "Промяна на профила",
"email": "Имейл",
"email-address": "Email Address",
"email-enrollAccount-subject": "Ваш профил беше създаден на __siteName__",
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
"email-fail": "Неуспешно изпращане на имейла",
@ -748,6 +768,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": "по подразбиране",
@ -755,7 +777,7 @@
"subtask-settings": "Настройки на Подзадачите",
"card-settings": "Настройки на Карта",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Настройки за Подзадачите за това Табло",
"boardSubtaskSettingsPopup-title": "Настройки на Подзадачите",
"boardCardSettingsPopup-title": "Настройки на Карта",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Вложете под-задачи към тази дъска:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Състояние",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Завършено",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "deleted comment %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Templates",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@ -190,7 +202,9 @@
"board-view-collapse": "Collapse",
"board-view-gantt": "Gantt",
"board-view-lists": "Lists",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel",
"card-archived": "This card is moved to Archive.",
"board-archived": "This board is moved to Archive.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Krouiñ",
"createBoardPopup-title": "Create Board",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Import board",
"createLabelPopup-title": "Create Label",
"createCustomField": "Create Field",
@ -354,6 +369,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",
"decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Diverkañ",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edit Notification",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "Ha esborrar el comentari %s",
"activity-receivedDate": "editat la data de recepció a %s de %s",
"activity-startDate": "data d'inici editada a %s de %s",
"allboards.starred": "Starred",
"allboards.templates": "Plantilles",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "data de venciment editada a %s de %s",
"activity-endDate": "data de finalització editada a %s de %s",
"add-attachment": "Afegeix adjunt",
@ -190,7 +202,9 @@
"board-view-collapse": "Contraure",
"board-view-gantt": "Gantt",
"board-view-lists": "Llistes",
"bucket-example": "Igual que “Bucket List”, per exemple",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel·la",
"card-archived": "Aquesta fitxa ha estat moguda al Arxiu.",
"board-archived": "Aquest tauler s'ha mogut a l'arxiu",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"Títol de la primera fitxa\", \"description\":\"Descripció de la primera fitxa\"}, {\"title\":\"Títol de la segona fitxa\",\"description\":\"Descripció de la segona fitxa \"},{\"title\":\"Títol de l'última fitxa\",\"description\":\"Descripció de l'última fitxa\"} ]",
"create": "Crea",
"createBoardPopup-title": "Crea tauler",
"createTemplateContainerPopup-title": "Afegeix un Contenidor de plantilles",
"chooseBoardSourcePopup-title": "Importa tauler",
"createLabelPopup-title": "Crea etiqueta",
"createCustomField": "Crear campament",
@ -354,6 +369,10 @@
"custom-field-text": "Text",
"custom-fields": "Camps Personalitzats",
"date": "Dades",
"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",
"decline": "Declina",
"default-avatar": "Avatar per defecte",
"delete": "Esborra",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edita la notificació",
"editProfilePopup-title": "Edita el teu Perfil",
"email": "Correu electrònic",
"email-address": "Email Address",
"email-enrollAccount-subject": "Un compte creat per a tu a __siteName__",
"email-enrollAccount-text": "Hola __user__,\n\nPer començar a utilitzar el servei, segueix l'enllaç següent.\n\n__url__\n\nGràcies.",
"email-fail": "Error enviant el correu",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Configuració de subtasques",
"card-settings": "Configuració de fitxa",
"minicard-settings": "Configuració de la minifitxa",
"boardSubtaskSettingsPopup-title": "Configuració de les subtasques del tauler",
"boardSubtaskSettingsPopup-title": "Configuració de subtasques",
"boardCardSettingsPopup-title": "Configuració de fitxa",
"boardMinicardSettingsPopup-title": "Configuració de la minifitxa",
"deposit-subtasks-board": "Diposita subtasques a aquest tauler:",
@ -1009,6 +1031,8 @@
"dueCardsViewChange-choice-me": "jo",
"dueCardsViewChange-choice-all": "Tots els usuaris",
"dueCardsViewChange-choice-all-description": "Mostra totes les fitxes incompletes amb una *data de venciment* de taulers per als quals l'usuari té permís.",
"dueCards-noResults-title": "No Due Cards Found",
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
"broken-cards": "Fitxes Trencades",
"board-title-not-found": "No s'ha trobat el tauler '%s'.",
"swimlane-title-not-found": "No s'ha trobat el carril '%s'.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Estat",
"migration-progress-details": "Detalls",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completat",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "deleted comment %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Templates",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@ -190,7 +202,9 @@
"board-view-collapse": "Collapse",
"board-view-gantt": "Gantt",
"board-view-lists": "Lists",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel",
"card-archived": "This card is moved to Archive.",
"board-archived": "This board is moved to Archive.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Create",
"createBoardPopup-title": "Create Board",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Import board",
"createLabelPopup-title": "Create Label",
"createCustomField": "Create Field",
@ -354,6 +369,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",
"decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Delete",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edit Notification",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "deleted comment %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Templates",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@ -190,7 +202,9 @@
"board-view-collapse": "Collapse",
"board-view-gantt": "Gantt",
"board-view-lists": "Lists",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel",
"card-archived": "This card is moved to Archive.",
"board-archived": "This board is moved to Archive.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Create",
"createBoardPopup-title": "Create Board",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Import board",
"createLabelPopup-title": "Create Label",
"createCustomField": "Create Field",
@ -354,6 +369,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",
"decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Delete",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edit Notification",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "deleted comment %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Templates",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@ -190,7 +202,9 @@
"board-view-collapse": "Collapse",
"board-view-gantt": "Gantt",
"board-view-lists": "Lists",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel",
"card-archived": "This card is moved to Archive.",
"board-archived": "This board is moved to Archive.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Create",
"createBoardPopup-title": "Create Board",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Import board",
"createLabelPopup-title": "Create Label",
"createCustomField": "Create Field",
@ -354,6 +369,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",
"decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Delete",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edit Notification",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "smazat komentář %s",
"activity-receivedDate": "editoval(a) datum přijetí na %s z %s",
"activity-startDate": "editoval(a) datum zahájení na %s z %s",
"allboards.starred": "Starred",
"allboards.templates": "Šablony",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "editoval(a) termín dokončení na %s z %s",
"activity-endDate": "editoval(a) datum ukončení na %s z %s",
"add-attachment": "Přidat přílohu",
@ -190,7 +202,9 @@
"board-view-collapse": "Sbalit",
"board-view-gantt": "Gannt",
"board-view-lists": "Sloupce",
"bucket-example": "Například \"O čem sním\"",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Zrušit",
"card-archived": "Karta byla přesunuta do archivu.",
"board-archived": "Toto tablo je přesunuto do archivu.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"Nadpis první karty\", \"description\":\"Popis první karty\"}, {\"title\":\"Nadpis druhé karty\",\"description\":\"Popis druhé karty\"},{\"title\":\"Nadpis poslední kary\",\"description\":\"Popis poslední karty\"} ]",
"create": "Vytvořit",
"createBoardPopup-title": "Vytvořit tablo",
"createTemplateContainerPopup-title": "Přidat kontejner šablony",
"chooseBoardSourcePopup-title": "Importovat tablo",
"createLabelPopup-title": "Vytvořit štítek",
"createCustomField": "Vytvořit pole",
@ -354,6 +369,10 @@
"custom-field-text": "Text",
"custom-fields": "Vlastní pole",
"date": "Datum",
"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",
"decline": "Zamítnout",
"default-avatar": "Výchozí avatar",
"delete": "Smazat",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Změnit notifikace",
"editProfilePopup-title": "Upravit profil",
"email": "Email",
"email-address": "Email Address",
"email-enrollAccount-subject": "Byl vytvořen účet na __siteName__",
"email-enrollAccount-text": "Ahoj __user__,\n\nMůžeš začít používat službu kliknutím na odkaz níže.\n\n__url__\n\nDěkujeme.",
"email-fail": "Odeslání emailu selhalo",
@ -748,6 +768,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í",
@ -755,7 +777,7 @@
"subtask-settings": "Nastavení podúkolů",
"card-settings": "Nastavení karet",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Nastavení podúkolů tabla",
"boardSubtaskSettingsPopup-title": "Nastavení podúkolů",
"boardCardSettingsPopup-title": "Nastavení karet",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Vložit podúkoly do tohoto tabla",
@ -1009,6 +1031,8 @@
"dueCardsViewChange-choice-me": "Moje",
"dueCardsViewChange-choice-all": "Všechny",
"dueCardsViewChange-choice-all-description": "Zobrazí všechny nedokončené karty s *Termínem dokončení* z každého tabla, ke kterému má uživatel oprávnění.",
"dueCards-noResults-title": "No Due Cards Found",
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
"broken-cards": "Rozbité karty",
"board-title-not-found": "Tablo '%s' nenalezeno.",
"swimlane-title-not-found": "Swimlane '%s' nenalezena.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Stav",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Dokončeno",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "smazat komentář %s",
"activity-receivedDate": "editoval(a) datum přijetí na %s z %s",
"activity-startDate": "editoval(a) datum zahájení na %s z %s",
"allboards.starred": "Starred",
"allboards.templates": "Šablony",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "editoval(a) termín dokončení na %s z %s",
"activity-endDate": "editoval(a) datum ukončení na %s z %s",
"add-attachment": "Přidat přílohu",
@ -190,7 +202,9 @@
"board-view-collapse": "Sbalit",
"board-view-gantt": "Gannt",
"board-view-lists": "Sloupce",
"bucket-example": "Například \"O čem sním\"",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Zrušit",
"card-archived": "Karta byla přesunuta do archivu.",
"board-archived": "Toto tablo je přesunuto do archivu.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"Nadpis první karty\", \"description\":\"Popis první karty\"}, {\"title\":\"Nadpis druhé karty\",\"description\":\"Popis druhé karty\"},{\"title\":\"Nadpis poslední kary\",\"description\":\"Popis poslední karty\"} ]",
"create": "Vytvořit",
"createBoardPopup-title": "Vytvořit tablo",
"createTemplateContainerPopup-title": "Přidat kontejner šablony",
"chooseBoardSourcePopup-title": "Importovat tablo",
"createLabelPopup-title": "Vytvořit štítek",
"createCustomField": "Vytvořit pole",
@ -354,6 +369,10 @@
"custom-field-text": "Text",
"custom-fields": "Vlastní pole",
"date": "Datum",
"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",
"decline": "Zamítnout",
"default-avatar": "Výchozí avatar",
"delete": "Smazat",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Změnit notifikace",
"editProfilePopup-title": "Upravit profil",
"email": "Email",
"email-address": "Email Address",
"email-enrollAccount-subject": "Byl vytvořen účet na __siteName__",
"email-enrollAccount-text": "Ahoj __user__,\n\nMůžeš začít používat službu kliknutím na odkaz níže.\n\n__url__\n\nDěkujeme.",
"email-fail": "Odeslání emailu selhalo",
@ -748,6 +768,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í",
@ -755,7 +777,7 @@
"subtask-settings": "Nastavení podúkolů",
"card-settings": "Nastavení karet",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Nastavení podúkolů tabla",
"boardSubtaskSettingsPopup-title": "Nastavení podúkolů",
"boardCardSettingsPopup-title": "Nastavení karet",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Vložit podúkoly do tohoto tabla",
@ -1009,6 +1031,8 @@
"dueCardsViewChange-choice-me": "Moje",
"dueCardsViewChange-choice-all": "Všechny",
"dueCardsViewChange-choice-all-description": "Zobrazí všechny nedokončené karty s *Termínem dokončení* z každého tabla, ke kterému má uživatel oprávnění.",
"dueCards-noResults-title": "No Due Cards Found",
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
"broken-cards": "Rozbité karty",
"board-title-not-found": "Tablo '%s' nenalezeno.",
"swimlane-title-not-found": "Swimlane '%s' nenalezena.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Stav",
"migration-progress-details": "Podrobnosti",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Dokončeno",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "deleted comment %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Templates",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@ -190,7 +202,9 @@
"board-view-collapse": "Collapse",
"board-view-gantt": "Gantt",
"board-view-lists": "Lists",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel",
"card-archived": "This card is moved to Archive.",
"board-archived": "This board is moved to Archive.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Create",
"createBoardPopup-title": "Create Board",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Import board",
"createLabelPopup-title": "Create Label",
"createCustomField": "Create Field",
@ -354,6 +369,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",
"decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Delete",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edit Notification",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "deleted comment %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Templates",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Add Attachment",
@ -190,7 +202,9 @@
"board-view-collapse": "Collapse",
"board-view-gantt": "Gantt",
"board-view-lists": "Lists",
"bucket-example": "Like “Bucket List” for example",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Cancel",
"card-archived": "This card is moved to Archive.",
"board-archived": "This board is moved to Archive.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Create",
"createBoardPopup-title": "Create Board",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Import board",
"createLabelPopup-title": "Create Label",
"createCustomField": "Create Field",
@ -354,6 +369,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",
"decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Delete",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Edit Notification",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
"email-address": "Email Address",
"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",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
"boardCardSettingsPopup-title": "Card Settings",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Completed",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "slettede kommentar %s",
"activity-receivedDate": "edited received date to %s of %s",
"activity-startDate": "edited start date to %s of %s",
"allboards.starred": "Starred",
"allboards.templates": "Skabeloner",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "edited due date to %s of %s",
"activity-endDate": "edited end date to %s of %s",
"add-attachment": "Tilføj vedhæftning",
@ -190,7 +202,9 @@
"board-view-collapse": "Sammenfold",
"board-view-gantt": "Gantt",
"board-view-lists": "Lister",
"bucket-example": "Eksempelvis \"Bucked-liste\"",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Annullér",
"card-archived": "Dette kort blev flyttet til arkivet.",
"board-archived": "Denne tavle blev flyttet til arkivet.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
"create": "Opret",
"createBoardPopup-title": "Opret tavle",
"createTemplateContainerPopup-title": "Add Template Container",
"chooseBoardSourcePopup-title": "Importér tavle",
"createLabelPopup-title": "Opret etikette",
"createCustomField": "Opret felt",
@ -354,6 +369,10 @@
"custom-field-text": "Tekst",
"custom-fields": "Brugerdefinerede felter",
"date": "Dato",
"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",
"decline": "Afslå",
"default-avatar": "Standard-avatar",
"delete": "Slet",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Redigér notifikation",
"editProfilePopup-title": "Redigér profil",
"email": "E-mail",
"email-address": "Email Address",
"email-enrollAccount-subject": "Der er oprettet konto til dig på __siteName__",
"email-enrollAccount-text": "Hej __user__,\n\nFor at begynde at benytte tjenesten, så klik linket nedenfor.\n\n__url__\n\nTak.",
"email-fail": "Afsendelse af e-mail mislykkedes",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Indstillinger for delopgaver",
"card-settings": "Indstillinger for kort",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Indstillinger for delopgaver i tavle",
"boardSubtaskSettingsPopup-title": "Indstillinger for delopgaver",
"boardCardSettingsPopup-title": "Indstillinger for kort",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Indsæt delopgaver på denne tavle:",
@ -1009,6 +1031,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.",
"broken-cards": "Broken Cards",
"board-title-not-found": "Board '%s' not found.",
"swimlane-title-not-found": "Swimlane '%s' not found.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "Fuldført",

View file

@ -78,6 +78,18 @@
"activity-deleteComment": "löschte Kommentar %s",
"activity-receivedDate": "hat Empfangsdatum zu %s geändert auf %s",
"activity-startDate": "hat Startdatum zu %s geändert auf %s",
"allboards.starred": "Starred",
"allboards.templates": "Vorlagen",
"allboards.remaining": "Remaining",
"allboards.workspaces": "Workspaces",
"allboards.add-workspace": "Add Workspace",
"allboards.add-workspace-prompt": "Workspace name",
"allboards.add-subworkspace": "Add Subworkspace",
"allboards.add-subworkspace-prompt": "Subworkspace name",
"allboards.edit-workspace": "Edit workspace",
"allboards.edit-workspace-name": "Workspace name",
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
"multi-selection-active": "Click checkboxes to select boards",
"activity-dueDate": "hat Fälligkeitsdatum zu %s geändert auf %s",
"activity-endDate": "hat Enddatum zu %s geändert auf %s",
"add-attachment": "Datei anhängen",
@ -190,7 +202,9 @@
"board-view-collapse": "Einklappen",
"board-view-gantt": "Gantt",
"board-view-lists": "Listen",
"bucket-example": "z.B. \"Löffelliste\"",
"bucket-example": "Like \"Bucket List\" for example",
"calendar-previous-month-label": "Previous Month",
"calendar-next-month-label": "Next Month",
"cancel": "Abbrechen",
"card-archived": "Diese Karte wurde ins Archiv verschoben",
"board-archived": "Dieses Board wurde ins Archiv verschoben.",
@ -335,6 +349,7 @@
"copyManyCardsPopup-format": "[ {\"title\": \"Titel der ersten Karte\", \"description\":\"Beschreibung der ersten Karte\"}, {\"title\":\"Titel der zweiten Karte\",\"description\":\"Beschreibung der zweiten Karte\"},{\"title\":\"Titel der letzten Karte\",\"description\":\"Beschreibung der letzten Karte\"} ]",
"create": "Erstellen",
"createBoardPopup-title": "Board erstellen",
"createTemplateContainerPopup-title": "Vorlagen-Container hinzufügen",
"chooseBoardSourcePopup-title": "Board importieren",
"createLabelPopup-title": "Label erstellen",
"createCustomField": "Feld erstellen",
@ -354,6 +369,10 @@
"custom-field-text": "Text",
"custom-fields": "Benutzerdefinierte Felder",
"date": "Datum",
"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",
"decline": "Ablehnen",
"default-avatar": "Standard Profilbild",
"delete": "Löschen",
@ -379,6 +398,7 @@
"editNotificationPopup-title": "Benachrichtigung ändern",
"editProfilePopup-title": "Profil ändern",
"email": "E-Mail",
"email-address": "Email Address",
"email-enrollAccount-subject": "Ihr Benutzerkonto auf __siteName__ wurde erstellt",
"email-enrollAccount-text": "Hallo __user__,\n\num den Dienst nutzen zu können, klicken Sie bitte auf folgenden Link:\n\n__url__\n\nDanke.",
"email-fail": "Senden der E-Mail fehlgeschlagen",
@ -748,6 +768,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",
@ -755,7 +777,7 @@
"subtask-settings": "Einstellungen für Teilaufgaben",
"card-settings": "Karten-Einstellungen",
"minicard-settings": "Minicard Settings",
"boardSubtaskSettingsPopup-title": "Boardeinstellungen für Teilaufgaben",
"boardSubtaskSettingsPopup-title": "Einstellungen für Teilaufgaben",
"boardCardSettingsPopup-title": "Karten-Einstellungen",
"boardMinicardSettingsPopup-title": "Minicard Settings",
"deposit-subtasks-board": "Teilaufgaben in diesem Board ablegen:",
@ -1009,6 +1031,8 @@
"dueCardsViewChange-choice-me": "Ich",
"dueCardsViewChange-choice-all": "alle Benutzer",
"dueCardsViewChange-choice-all-description": "Zeigt alle unvollständigen Karten mit einem *Fälligkeits*-Datum auf Boards, für die der Benutzer Berechtigungen hat.",
"dueCards-noResults-title": "No Due Cards Found",
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
"broken-cards": "Fehlerhafte Karten",
"board-title-not-found": "Board „%s“ nicht gefunden.",
"swimlane-title-not-found": "Swimlane „%s“ nicht gefunden.",
@ -1393,7 +1417,70 @@
"back-to-settings": "Back to Settings",
"board-id": "Board ID",
"board-migration": "Board Migration",
"board-migrations": "Board Migrations",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"comprehensive-board-migration": "Comprehensive Board Migration",
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
"lost-cards": "Lost Cards",
"lost-cards-list": "Restored Items",
"restore-lost-cards-migration": "Restore Lost Cards",
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
"restore-all-archived-migration": "Restore All Archived",
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
"fix-missing-lists-migration": "Fix Missing Lists",
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
"fix-avatar-urls-migration": "Fix Avatar URLs",
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
"fix-all-file-urls-migration": "Fix All File URLs",
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
"migration-needed": "Migration Needed",
"migration-complete": "Complete",
"migration-running": "Running...",
"migration-successful": "Migration completed successfully",
"migration-failed": "Migration failed",
"migrations": "Migrations",
"migrations-admin-only": "Only board administrators can run migrations",
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
"no-issues-found": "No issues found",
"run-migration": "Run Migration",
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
"migration-progress-status": "Status",
"migration-progress-details": "Details",
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
"step-validate-migration": "Validate Migration",
"step-fix-avatar-urls": "Fix Avatar URLs",
"step-fix-attachment-urls": "Fix Attachment URLs",
"step-analyze-lists": "Analyze Lists",
"step-create-missing-lists": "Create Missing Lists",
"step-update-cards": "Update Cards",
"step-finalize": "Finalize",
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
"step-restore-lists": "Restore Lists",
"step-restore-cards": "Restore Cards",
"step-restore-swimlanes": "Restore Swimlanes",
"step-fix-missing-ids": "Fix Missing IDs",
"step-scan-users": "Checking board member avatars",
"step-scan-files": "Checking board file attachments",
"step-fix-file-urls": "Fixing file URLs",
"cleanup": "Cleanup",
"cleanup-old-jobs": "Cleanup Old Jobs",
"completed": "abgeschlossen",

Some files were not shown because too many files have changed in this diff Show more