Compare commits

...

295 commits
v8.01 ... main

Author SHA1 Message Date
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
Lauri Ojansivu
b8942b728f v8.05
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-17 17:53:00 +03:00
Lauri Ojansivu
8e6eabd9e8 Updated transations. 2025-10-17 17:38:54 +03:00
Lauri Ojansivu
290dd6c4d1 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-17 07:56:49 +03:00
Lauri Ojansivu
088bc16072 Font Awesome to Unicode icons. Part 4.
Thanks to xet7 !
2025-10-17 07:55:04 +03:00
Lauri Ojansivu
daad2fbd71 Updated ChangeLog. 2025-10-17 07:10:07 +03:00
Lauri Ojansivu
3af94c2a90 Font Awesome to Unicode icons. Part 3.
Thanks to xet7 !
2025-10-17 07:08:01 +03:00
Lauri Ojansivu
a3ca76d3c4 Updated ChangeLog. 2025-10-17 06:09:42 +03:00
Lauri Ojansivu
62ede48196 Removed not needed visible text from mobile desktop switch button.
Thanks to xet7 !
2025-10-17 06:07:24 +03:00
Lauri Ojansivu
390a86a7a7 Updated ChangeLog. 2025-10-17 06:02:46 +03:00
Lauri Ojansivu
09631d6b0c Resize height of swimlane by dragging. Font Awesome to Unicode icons.
Thanks to xet7 !
2025-10-17 05:58:53 +03:00
Lauri Ojansivu
2947238a02 Convert Font Awesome to Unicode Icons. Part 1. In Progress.
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-10-17 02:19:43 +03:00
Lauri Ojansivu
a7af4b4809 Updated ChangeLog. 2025-10-17 00:27:30 +03:00
Lauri Ojansivu
cb6afe67a7 Replaced moment.js with Javascript date.
Thanks to xet7 !
2025-10-17 00:26:11 +03:00
Lauri Ojansivu
8c5b43295d Updated ChangeLog. 2025-10-16 23:21:47 +03:00
Lauri Ojansivu
79b94824ef Changed wekan-boostrap-datepicker to HTML datepicker.
Thanks to xet7 !
2025-10-16 23:19:26 +03:00
Lauri Ojansivu
33e4b046e8 Updated ChangeLog. 2025-10-16 22:25:20 +03:00
Lauri Ojansivu
386aea7c78 Popup fixes. Part 2.
Thanks to xet7 !
2025-10-16 22:24:11 +03:00
Lauri Ojansivu
4a7bccd983 Updated ChangeLog. 2025-10-16 21:46:35 +03:00
Lauri Ojansivu
1f0cae9e76 Updated ChangeLog. 2025-10-16 21:42:41 +03:00
Lauri Ojansivu
87ae085e6d Fix Edit avatar UI issue.
Thanks to xet7 !

Fixes #5940
2025-10-16 21:40:49 +03:00
Lauri Ojansivu
640ac2330f 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-16 20:24:46 +03:00
Lauri Ojansivu
2543df9425 Show original positions of swimlanes, lists and cards.
Thanks to xet7 !

Fixes #5939
2025-10-16 20:23:05 +03:00
Lauri Ojansivu
915ab47a72 v8.04 2025-10-16 17:59:24 +03:00
Lauri Ojansivu
09ff287da2 Updated translations. 2025-10-16 17:52:03 +03:00
Lauri Ojansivu
2896180f80 Updated ChangeLog. 2025-10-16 17:50:52 +03:00
Lauri Ojansivu
4283b5b0e3 Disable not working minio and s3 support temporarily.
Thanks to xet7 !
2025-10-16 17:49:39 +03:00
Lauri Ojansivu
bbbd3abf06 Try to fix Broken Hyperlinks in Markdown to HTML conversion.
Thanks to xet7 !

Fixes #5932
2025-10-16 17:47:59 +03:00
Lauri Ojansivu
dd88483ec7 Removed extra npm packages.
Thanks to xet7 !
2025-10-15 23:26:33 +03:00
Lauri Ojansivu
00ddec7575 Fix popups positioning. Part 2.
Thanks to xet7 !
2025-10-15 23:19:07 +03:00
Lauri Ojansivu
ab0ebab240 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-15 07:57:18 +03:00
Lauri Ojansivu
79e83e33ec Use only MongoDB 7 at Snap.
Thanks to xet7 !
2025-10-15 07:56:11 +03:00
Lauri Ojansivu
aa402d652d Updated ChangeLog. 2025-10-15 07:49:05 +03:00
Lauri Ojansivu
690481c138 Remove using fork with MongoDB at Snap.
Thanks to xet7 !
2025-10-15 07:47:57 +03:00
Lauri Ojansivu
881125aa98 Updated ChangeLog. 2025-10-15 07:46:54 +03:00
Lauri Ojansivu
77eea4d494 Fix popups positioning.
Thanks to xet7 !

Fixes #5924
2025-10-15 07:44:46 +03:00
Lauri Ojansivu
b26e16abb8 Updated ChangeLog. 2025-10-15 07:39:18 +03:00
Lauri Ojansivu
f08c7702ee Fix wide screen.
Thanks to xet7 !

Fixes #5915
2025-10-15 07:38:14 +03:00
Lauri Ojansivu
a4399c7ef4 Updated translations. 2025-10-15 07:25:33 +03:00
Lauri Ojansivu
d6e50ed9a0 Updated ChangeLog. 2025-10-15 07:21:15 +03:00
Lauri Ojansivu
6b848b318d Make sure that all cards are visible.
Thanks to xet7 !

Related #5915
2025-10-15 07:15:46 +03:00
Lauri Ojansivu
70ce70cf0e Try to fix Snap to not fork mongodb.
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-10-14 14:29:42 +03:00
Lauri Ojansivu
5a79bc5ee3 v8.03 2025-10-14 13:51:47 +03:00
Lauri Ojansivu
5792a86959 Fix Snap MongoDB to not fork at systemd, so it stays running.
Thanks to xet7 !
2025-10-14 13:45:51 +03:00
Lauri Ojansivu
37c5436087 v8.02
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-14 11:58:14 +03:00
Lauri Ojansivu
6592102e8f v8.02 2025-10-14 11:56:11 +03:00
Lauri Ojansivu
06a5a8f70d Try to fix Docker secrets to be optional.
Thanks to xet7 !

Fixes #5920
2025-10-14 11:54:09 +03:00
Lauri Ojansivu
ef54ebada6 Updated ChangeLog. 2025-10-14 11:25:36 +03:00
Lauri Ojansivu
d4f13de1d9 Try to fix swimlane hamburger menu popup positioning. In progress.
Thanks to xet7 !
2025-10-14 11:24:22 +03:00
Lauri Ojansivu
4fcedde529 Updated ChangeLog. 2025-10-14 11:04:40 +03:00
Lauri Ojansivu
a4518bbefc Fix drag drop reorder swimlanes.
Thanks to xet7 !
2025-10-14 11:03:36 +03:00
Lauri Ojansivu
95f771aa26 Updated ChangeLog. 2025-10-14 10:47:00 +03:00
Lauri Ojansivu
da98942cce Updated mobile Bookmarks/Starred boards. Part 1. In Progress.
Thanks to xet7 !
2025-10-14 10:43:39 +03:00
Lauri Ojansivu
f3efaf59e1 Updated translations. 2025-10-14 09:39:19 +03:00
Lauri Ojansivu
d64032f2a3 Updated ChangeLog. 2025-10-14 09:37:49 +03:00
Lauri Ojansivu
abad8cc4d5 Change list width by dragging between lists.
Thanks to xet7 !
2025-10-14 09:36:11 +03:00
Lauri Ojansivu
0d9536e2f9 Updated ChangeLog. 2025-10-14 08:26:46 +03:00
Lauri Ojansivu
67b078b805 Accessibility improvements.
Thanks to xet7 !
2025-10-14 08:25:39 +03:00
Lauri Ojansivu
6f02eeae53 Updated ChangeLog. 2025-10-14 07:24:46 +03:00
Lauri Ojansivu
5bc03b23ea Updated dependencies.
Thanks to developers of dependencies !
2025-10-14 07:23:13 +03:00
Lauri Ojansivu
3d4acd8c8f Updated translations. 2025-10-14 07:21:41 +03:00
Lauri Ojansivu
448bec8181 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-14 01:54:28 +03:00
Lauri Ojansivu
0a34ee1b64 Removed not needed console log message.
Thanks to xet7 !
2025-10-14 01:52:58 +03:00
Lauri Ojansivu
34e8e4d4c3 Updated translations. 2025-10-14 01:38:38 +03:00
Lauri Ojansivu
32627c03f4 Updated ChangeLog. 2025-10-14 01:32:43 +03:00
Lauri Ojansivu
63c314ca18 Fixed migrations.
Thanks to xet7 !
2025-10-14 01:30:59 +03:00
Lauri Ojansivu
e8453783da Updated translations. 2025-10-13 23:48:30 +03:00
Lauri Ojansivu
96522ec3a3 Updated translations. 2025-10-13 23:38:17 +03:00
Lauri Ojansivu
17dedab391 Updated translations. 2025-10-13 23:37:02 +03:00
Lauri Ojansivu
283d2ee09c Updated translations. 2025-10-13 23:33:27 +03:00
Lauri Ojansivu
289ff0127e Updated translations. 2025-10-13 23:23:32 +03:00
Lauri Ojansivu
931d7217b1 Updated translations. 2025-10-13 23:04:24 +03:00
Lauri Ojansivu
3149a3927e Updated translations. 2025-10-13 22:25:41 +03:00
Lauri Ojansivu
c57bced7b1 Updated translations. 2025-10-13 22:21:23 +03:00
Lauri Ojansivu
8d794a59dc Updated ChangeLog. 2025-10-13 22:19:56 +03:00
Lauri Ojansivu
7bb1e24bda Fixed Admin Panel Settings menus Attachments and Cron.
Thanks to xet7 !
2025-10-13 22:17:32 +03:00
Lauri Ojansivu
e0013b9b63 Fix Admin Panel Settings menu to show options correctly. Part 1.
Thanks to xet7 !
2025-10-13 20:51:29 +03:00
Lauri Ojansivu
7d81aab900 Updated ChangeLog. 2025-10-13 20:35:49 +03:00
Lauri Ojansivu
cc99da5357 Fixed Error in migrate-lists-to-per-swimlane migration.
Thanks to xet7 !

Fixes #5918
2025-10-13 20:34:23 +03:00
Lauri Ojansivu
9bd21e1d1b 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-12 05:38:08 +03:00
Lauri Ojansivu
2148aeea42 Change Admin Panel "Attachment Settings" and "Cron Settings" options to be tabs, not submenu. Part 3.
Thanks to xet7 !
2025-10-12 05:36:51 +03:00
Lauri Ojansivu
6a7a5505f9 Updated ChangeLog. 2025-10-12 05:26:46 +03:00
Lauri Ojansivu
5a6faafa30 Change Admin Panel "Attachment Settings" and "Cron Settings" options to be tabs, not submenu. Part 2.
Thanks to xet7 !
2025-10-12 05:25:44 +03:00
Lauri Ojansivu
e2f3dad779 Updated ChangeLog. 2025-10-12 04:52:45 +03:00
Lauri Ojansivu
ae2aa1f5cd Change Admin Panel "Attachment Settings" and "Cron Settings" options to be tabs, not submenu.
Thanks to xet7 !
2025-10-12 04:50:17 +03:00
Lauri Ojansivu
0e3a17d922 Updated ChangeLog. 2025-10-12 04:40:27 +03:00
Lauri Ojansivu
033919a270 Fix Admin Panel menus "Attachment Settings" and "Cron Settings" and make them translateable.
Thanks to xet7 !
2025-10-12 04:39:04 +03:00
Lauri Ojansivu
f5d40a0a12 Updated ChangeLog. 2025-10-12 04:22:35 +03:00
Lauri Ojansivu
0fd781e80a Fix opening sidebar.
Thanks to xet7 !
2025-10-12 04:21:38 +03:00
Lauri Ojansivu
a8f6170fdf Updated ChangeLog. 2025-10-12 03:49:52 +03:00
Lauri Ojansivu
bd8c565415 Fixes to make board showing correctly.
Thanks to xet7 !
2025-10-12 03:48:21 +03:00
Lauri Ojansivu
ffb02fe0ec 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-11 20:39:08 +03:00
Lauri Ojansivu
114520302c Updated ChangeLog. 2025-10-11 20:35:58 +03:00
Lauri Ojansivu
317138ab72 If there is no cron jobs running, run migrations for boards that have not been opened yet.
Thanks to xet7 !
2025-10-11 20:33:31 +03:00
Lauri Ojansivu
a990109f43 Updated ChangeLog. 2025-10-11 19:43:20 +03:00
Lauri Ojansivu
da68b01502 Added Cron Manager to Admin Panel for long running jobs, like running migrations when opening board, copying or moving boards swimlanes lists cards etc.
Thanks to xet7 !
2025-10-11 19:41:09 +03:00
Lauri Ojansivu
e90bc744d9 Updated ChangeLog. 2025-10-11 19:26:07 +03:00
Lauri Ojansivu
2b5c56484a Run database migrations when opening board. Not when updating WeKan.
Thanks to xet7 !
2025-10-11 19:23:47 +03:00
467 changed files with 63021 additions and 14094 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

@ -52,8 +52,8 @@ ongoworks:speakingurl
raix:handlebar-helpers
http@2.0.0! # force new http package
# Datepicker
wekan-bootstrap-datepicker
# Datepicker (disabled - using native HTML inputs)
# wekan-bootstrap-datepicker
# UI components
ostrio:i18n
@ -93,4 +93,4 @@ ejson@1.1.3
logging@1.3.3
wekan-fullcalendar
momentjs:moment@2.29.3
wekan-fontawesome
# wekan-fontawesome

View file

@ -155,8 +155,6 @@ wekan-accounts-cas@0.1.0
wekan-accounts-lockout@1.0.0
wekan-accounts-oidc@1.0.10
wekan-accounts-sandstorm@0.8.0
wekan-bootstrap-datepicker@1.10.0
wekan-fontawesome@6.4.2
wekan-fullcalendar@3.10.5
wekan-ldap@0.0.2
wekan-markdown@1.0.9

View file

@ -19,6 +19,355 @@ 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:
- [Show original positions of swimlanes, lists and cards](https://github.com/wekan/wekan/commit/2543df94252c2789fb484ae52b9a6ff298252ceb).
Thanks to xet7.
- Fix popups issues at Edit Avatar, Archive card confirm, etc.
[Part 1](https://github.com/wekan/wekan/commit/87ae085e6d0a56a2083eec819cf7d795d3e51e1a),
[Part 2](https://github.com/wekan/wekan/commit/386aea7c788d6eaf9d486ead4d81453401adf390).
Thanks to xet7.
- [Changed wekan-boostrap-datepicker to HTML datepicker](https://github.com/wekan/wekan/commit/79b94824efedaa9e256de931fd26398eb2838d6a).
Thanks to xet7.
- [Replaced moment.js with Javascript date](https://github.com/wekan/wekan/commit/cb6afe67a7363af89663ba17392dc5f90a15f703).
Thanks to xet7.
- [Convert Font Awesome to Unicode Icons. Part 1. In Progress](https://github.com/wekan/wekan/commit/2947238a021b6952b56e828d49a8c0094520d89a).
Thanks to xet7.
- [Resize height of swimlane by dragging. Font Awesome to Unicode icons](https://github.com/wekan/wekan/commit/09631d6b0c1b8e3bbc3bf45d4bb65449b46f1288).
Thanks to xet7.
- [Removed not needed visible text from mobile desktop switch button](https://github.com/wekan/wekan/commit/62ede481966107405460f6d5b90f292c98bae254).
Thanks to xet7.
- Font Awesome to Unicode icons.
[Part 3](https://github.com/wekan/wekan/commit/3af94c2a9059a399b9f9946c387caff892ace2f9).
[Part 4](https://github.com/wekan/wekan/commit/088bc16072ea0dd02aa2dec6a2e3e9aed00a3cc9).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.04 2025-10-16 WeKan ® release
This release fixes the following bugs:
- [Make sure that all cards are visible](https://github.com/wekan/wekan/commit/6b848b318d62afe9772218febdb09c7426774f60).
Thanks to xet7.
- [Fix wide screen](https://github.com/wekan/wekan/commit/f08c7702eecf23588f7bc023beefb453edd704c6).
Thanks to xet7.
- Fix popups positioning.
[Part 1](https://github.com/wekan/wekan/commit/77eea4d494e5db8e2c0e59732bcea73aa163bc13),
[Part 1](https://github.com/wekan/wekan/commit/00ddec75754bbbccc6fb9b3096495b9609246480).
Thanks to xet7.
- [Remove using fork with MongoDB at Snap](https://github.com/wekan/wekan/commit/690481c138f9629054180310dd172295c7f6d34e).
Thanks to xet7.
- [Use only MongoDB 7 at Snap](https://github.com/wekan/wekan/commit/79e83e33ec1dcec4eea81d5fb4a9f7381c176a12).
Thanks to xet7.
- [Removed extra npm packages](https://github.com/wekan/wekan/commit/dd88483ec7526eee4a97bac5f09e03985be5d923).
Thanks to xet7.
- [Try to fix Broken Hyperlinks in Markdown to HTML conversion](https://github.com/wekan/wekan/commit/bbbd3abf06e45a3fa57c4aa987d87f1873eb11d6).
Thanks to xet7.
- [Disable not working minio and s3 support temporarily](https://github.com/wekan/wekan/commit/4283b5b0e330930fff1fa2bb73c355a4ffb4cda0).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.03 2025-10-14 WeKan ® release
This release fixes the following bugs:
- [Fix Snap MongoDB to not fork at systemd, so it stays running](https://github.com/wekan/wekan/commit/5792a869594b4c79a93db414b95a13d60013193b).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.02 2025-10-14 WeKan ® release
This release adds the following new features:
- [Run database migrations when opening board. Not when upgrading WeKan](https://github.com/wekan/wekan/commit/2b5c56484a4dd559f062ef892fd5248a903b2a10).
Thanks to xet7.
- [Added Cron Manager to Admin Panel for long running jobs, like running migrations when opening board, copying or moving boards swimlanes lists cards etc](https://github.com/wekan/wekan/commit/da68b01502afc9d5d9ea1267bee9fc98bb08b611).
Thanks to xet7.
- [If there is no cron jobs running, run migrations for boards that have not been opened yet](https://github.com/wekan/wekan/commit/317138ab7209a41715336ea8251df45f11a6d173).
Thanks to xet7.
- [Accessibility improvements](https://github.com/wekan/wekan/commit/67b078b8056ec9851caaf6ef855719de1e6d966d).
Thanks to xet7.
- [Change list width by dragging between lists](https://github.com/wekan/wekan/commit/abad8cc4d5dded0f5e1a80892a3b29aa71404a5c).
Thanks to xet7.
and adds the following updates:
- [Updated dependencies](https://github.com/wekan/wekan/commit/5bc03b23ea34816d8e1135cbe9ed5f18a2573854).
Thanks to developers of dependencies.
and fixes the following bugs:
- [Fixes to make board showing correctly](https://github.com/wekan/wekan/commit/bd8c565415998c9aaded821988d591105258b378).
Thanks to xet7.
- [Fix opening sidebar](https://github.com/wekan/wekan/commit/0fd781e80aaf841c26ce59caffc579b9c391330f).
Thanks to xet7.
- [Fix Admin Panel menus "Attachment Settings" and "Cron Settings" and make them translateable](https://github.com/wekan/wekan/commit/033919a2702fa6959b8f8c87f076d3f255ace6ba).
Thanks to xet7.
- Change Admin Panel "Attachment Settings" and "Cron Settings" options to be tabs, not submenu.
[Part 1](https://github.com/wekan/wekan/commit/ae2aa1f5cd2511e80e12a91426eb91bb968dff98),
[Part 2](https://github.com/wekan/wekan/commit/5a6faafa30fefcd5dd0af7cc52b847a54d538065),
[Part 3](https://github.com/wekan/wekan/commit/2148aeea42f69fa367bf8c451d7f1c3a63b52880).
Thanks to xet7.
- [Fixed Error in migrate-lists-to-per-swimlane migration](https://github.com/wekan/wekan/commit/cc99da5357fb1fc00e3b5aece20c57917f88301b).
Thanks to xet7.
- Fix Admin Panel Settings menu to show Attachments and Cron options correctly.
[Part 1](https://github.com/wekan/wekan/e0013b9b631eb16861b1cfdb25386bf8e9099b4e),
[Part 2](https://github.com/wekan/wekan/7bb1e24bda2ed9db0bad0fafcf256680c2c05e8a).
- [Fixed migrations](https://github.com/wekan/wekan/commit/63c314ca185aeda650c01b4a67fcde1067320d22).
Thanks to xet7.
- [Removed not needed console log message](https://github.com/wekan/wekan/commit/0a34ee1b6437dcfd65e31d9bbc9f3ccfa5718ba9).
Thanks to xet7.
- [Updated mobile Bookmarks/Starred boards. Part 1. In Progress](https://github.com/wekan/wekan/commit/da98942cce37363d6062695d3c4cf7e2df796cac).
Thanks to xet7.
- [Fix drag drop reorder swimlanes](https://github.com/wekan/wekan/commit/a4518bbefc99be74f7ccfdbb9fdf902007ca90f3).
Thanks to xet7.
- [Try to fix swimlane hamburger menu popup positioning. In progress](https://github.com/wekan/wekan/commit/d4f13de1d978b271d05e1d67d40e3c1c14761578).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.01 2025-10-11 WeKan ® release
This release adds the following new features:
@ -90,7 +439,7 @@ and adds the following new features:
- [Mobile one board per row. Board zoom size percent. Board toggle mobile/desktop mode. In Progress](https://github.com/wekan/wekan/commit/752699d1c2fb8ea9ff0f3ec9ae0b2b776443d826).
Thanks to xet7.
- [Drag any files from file manager to minicard or opened card.
- Drag any files from file manager to minicard or opened card.
[Part 1](https://github.com/wekan/wekan/commit/3e9481c5bd2c02ba501bd0a6ef1d1e6ce82bb1d9),
[Part 2](https://github.com/wekan/wekan/commit/cdd7d69c660d0b6ac06b7b75d4f59985b8a9322a).
Thanks to xet7.

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.01/wekan-8.01-amd64.zip"
unzip wekan-8.01-amd64.zip
rm wekan-8.01-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.01.0"
appVersion: "v8.17.0"
files:
userUploads:
- README.md

View file

@ -4,3 +4,61 @@ if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/pwa-service-worker.js');
});
}
// Import board converter for on-demand conversion
import '/client/lib/boardConverter';
import '/client/components/boardConversionProgress';
// Import migration manager and progress UI
import '/client/lib/migrationManager';
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

@ -0,0 +1,184 @@
/* Board Conversion Progress Styles */
.board-conversion-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 9999;
display: none;
align-items: center;
justify-content: center;
}
.board-conversion-overlay.active {
display: flex;
}
.board-conversion-modal {
background: white;
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: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.board-conversion-header {
padding: 20px 24px 16px;
border-bottom: 1px solid #e0e0e0;
text-align: center;
}
.board-conversion-header h3 {
margin: 0 0 8px 0;
color: #333;
font-size: 20px;
font-weight: 500;
}
.board-conversion-header h3 i {
margin-right: 8px;
color: #2196F3;
}
.board-conversion-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.board-conversion-content {
padding: 24px;
}
.conversion-progress {
margin-bottom: 20px;
}
.progress-bar {
width: 100%;
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #2196F3, #21CBF3);
border-radius: 4px;
transition: width 0.3s ease;
position: relative;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.3),
transparent
);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.progress-text {
text-align: center;
font-weight: 600;
color: #2196F3;
font-size: 16px;
}
.conversion-status {
text-align: center;
margin-bottom: 16px;
color: #333;
font-size: 16px;
}
.conversion-status i {
margin-right: 8px;
color: #2196F3;
}
.conversion-time {
text-align: center;
color: #666;
font-size: 14px;
background-color: #f5f5f5;
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 16px;
}
.conversion-time i {
margin-right: 6px;
color: #FF9800;
}
.board-conversion-footer {
padding: 16px 24px 20px;
border-top: 1px solid #e0e0e0;
background-color: #f9f9f9;
}
.conversion-info {
text-align: center;
color: #666;
font-size: 13px;
line-height: 1.4;
}
.conversion-info i {
margin-right: 6px;
color: #2196F3;
}
/* Responsive design */
@media (max-width: 600px) {
.board-conversion-modal {
width: 95%;
margin: 20px;
}
.board-conversion-header,
.board-conversion-content,
.board-conversion-footer {
padding-left: 16px;
padding-right: 16px;
}
.board-conversion-header h3 {
font-size: 18px;
}
}

View file

@ -0,0 +1,27 @@
template(name="boardConversionProgress")
.board-conversion-overlay(class="{{#if isConverting}}active{{/if}}")
.board-conversion-modal
.board-conversion-header
h3
| ⚙️
| {{_ 'converting-board'}}
p {{_ 'converting-board-description'}}
.board-conversion-content
.conversion-progress
.progress-bar
.progress-fill(style="width: {{conversionProgress}}%")
.progress-text {{conversionProgress}}%
.conversion-status
| ⚙️
| {{conversionStatus}}
.conversion-time(style="{{#unless conversionEstimatedTime}}display: none;{{/unless}}")
| ⏰
| {{_ 'estimated-time-remaining'}}: {{conversionEstimatedTime}}
.board-conversion-footer
.conversion-info
|
| {{_ 'conversion-info-text'}}

View file

@ -0,0 +1,37 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import {
boardConverter,
isConverting,
conversionProgress,
conversionStatus,
conversionEstimatedTime
} from '/client/lib/boardConverter';
Template.boardConversionProgress.helpers({
isConverting() {
return isConverting.get();
},
conversionProgress() {
return conversionProgress.get();
},
conversionStatus() {
return conversionStatus.get();
},
conversionEstimatedTime() {
return conversionEstimatedTime.get();
}
});
Template.boardConversionProgress.onCreated(function() {
// Subscribe to conversion state changes
this.autorun(() => {
isConverting.get();
conversionProgress.get();
conversionStatus.get();
conversionEstimatedTime.get();
});
});

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

@ -1,5 +1,10 @@
template(name="board")
if isBoardReady.get
if isMigrating.get
+migrationProgress
else if isConverting.get
+boardConversionProgress
else if isBoardReady.get
if currentBoard
if onlyShowCurrentCard
+cardDetails(currentCard)
@ -16,6 +21,10 @@ template(name="boardBody")
if notDisplayThisBoard
| {{_ 'tableVisibilityMode-allowPrivateOnly'}}
else
// Debug information (remove in production)
if debugBoardState
.debug-info(style="position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.8); color: white; padding: 10px; z-index: 9999; font-size: 12px;")
| Board: {{currentBoard.title}} | View: {{boardView}} | HasSwimlanes: {{hasSwimlanes}} | Swimlanes: {{currentBoard.swimlanes.length}}
.board-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
.board-canvas.js-swimlanes(
class="{{#if hasSwimlanes}}dragscroll{{/if}}"
@ -34,15 +43,19 @@ template(name="boardBody")
each currentBoard.swimlanes
+swimlane(this)
else
a.js-empty-board-add-swimlane(title="{{_ 'add-swimlane'}}")
h1.big-message.quiet
| {{_ 'add-swimlane'}} +
// Fallback: If no swimlanes exist, show lists instead of empty message
+listsGroup(currentBoard)
else if isViewLists
+listsGroup(currentBoard)
else if isViewCalendar
+calendarView
else
+listsGroup(currentBoard)
// Default view - show swimlanes if they exist, otherwise show lists
if hasSwimlanes
each currentBoard.swimlanes
+swimlane(this)
else
+listsGroup(currentBoard)
+sidebar
template(name="calendarView")

View file

@ -1,6 +1,12 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
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';
const subManager = new SubsManager();
const { calculateIndex } = Utils;
@ -9,6 +15,11 @@ const swimlaneWhileSortingHeight = 150;
BlazeComponent.extendComponent({
onCreated() {
this.isBoardReady = new ReactiveVar(false);
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
@ -17,22 +28,512 @@ BlazeComponent.extendComponent({
this.autorun(() => {
const currentBoardId = Session.get('currentBoard');
if (!currentBoardId) return;
const handle = subManager.subscribe('board', currentBoardId, false);
Tracker.nonreactive(() => {
Tracker.autorun(() => {
this.isBoardReady.set(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);
}
});
});
},
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)) {
return;
}
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) return;
const swimlanes = board.swimlanes();
if (swimlanes.length === 0) {
// 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);
}
} catch (error) {
console.error('Error creating default swimlane:', error);
}
},
async checkAndConvertBoard(boardId) {
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) {
this.isBoardReady.set(true);
return;
}
// Automatic migration disabled - migrations must be run manually from sidebar
// Board admins can run migrations from the sidebar Migrations menu
this.isBoardReady.set(true);
} catch (error) {
console.error('Error during board conversion check:', error);
this.isConverting.set(false);
this.isMigrating.set(false);
this.isBoardReady.set(true); // Show board even if conversion check failed
}
},
/**
* 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
Meteor.call('boardMigration.startBoardMigration', boardId, (error, result) => {
if (error) {
console.error('Failed to start background migration:', error);
} else {
if (process.env.DEBUG === 'true') {
console.log('Background migration started for board:', boardId);
}
}
});
} catch (error) {
console.error('Error starting background migration:', error);
}
},
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
if (attachmentMigrationManager.isBoardMigrated(boardId)) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been migrated, skipping`);
}
return;
}
// Check if there are unconverted attachments
const unconvertedAttachments = attachmentMigrationManager.getUnconvertedAttachments(boardId);
if (unconvertedAttachments.length > 0) {
if (process.env.DEBUG === 'true') {
console.log(`Starting attachment migration for ${unconvertedAttachments.length} attachments in board ${boardId}`);
}
await attachmentMigrationManager.startAttachmentMigration(boardId);
} else {
// No attachments to migrate, mark board as migrated
// This will be handled by the migration manager itself
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has no attachments to migrate`);
}
}
} catch (error) {
console.error('Error starting attachment migration:', error);
}
},
onlyShowCurrentCard() {
return Utils.isMiniScreen() && Utils.getCurrentCardId(true);
const isMiniScreen = Utils.isMiniScreen();
const currentCardId = Utils.getCurrentCardId(true);
return isMiniScreen && currentCardId;
},
goHome() {
FlowRouter.go('home');
},
isConverting() {
return this.isConverting.get();
},
isMigrating() {
return this.isMigrating.get();
},
isBoardReady() {
return this.isBoardReady.get();
},
currentBoard() {
return Utils.getCurrentBoard();
},
}).register('board');
BlazeComponent.extendComponent({
@ -43,36 +544,51 @@ 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();
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;
});
if (currentBoardData && Swimlanes) {
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;
});
}
this._sortFieldsFixed.add(`swimlanes-${boardId}`);
}
}
// fix lists sort field if there are null values
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;
});
if (currentBoardData && Lists) {
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;
});
}
this._sortFieldsFixed.add(`lists-${boardId}`);
}
}
},
onRendered() {
@ -98,11 +614,16 @@ BlazeComponent.extendComponent({
}
}
// Observe for new popups/menus and set focus
// Observe for new popups/menus and set focus (but exclude swimlane content)
const popupObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1 && (node.classList.contains('popup') || node.classList.contains('modal') || node.classList.contains('menu'))) {
if (node.nodeType === 1 &&
(node.classList.contains('popup') || node.classList.contains('modal') || node.classList.contains('menu')) &&
!node.closest('.js-swimlanes') &&
!node.closest('.swimlane') &&
!node.closest('.list') &&
!node.closest('.minicard')) {
setTimeout(function() { focusFirstInteractive(node); }, 10);
}
});
@ -380,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
@ -412,51 +931,122 @@ BlazeComponent.extendComponent({
notDisplayThisBoard() {
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
let currentBoard = Utils.getCurrentBoard();
if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard.permission == 'public') {
return true;
}
return false;
return allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard && currentBoard.permission == 'public';
},
isViewSwimlanes() {
const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) {
return (currentUser.profile || {}).boardView === 'board-view-swimlanes';
boardView = (currentUser.profile || {}).boardView;
} else {
return (
window.localStorage.getItem('boardView') === 'board-view-swimlanes'
);
boardView = window.localStorage.getItem('boardView');
}
},
hasSwimlanes() {
return Utils.getCurrentBoard().swimlanes().length > 0;
// If no board view is set, default to swimlanes
if (!boardView) {
boardView = 'board-view-swimlanes';
}
return boardView === 'board-view-swimlanes';
},
isViewLists() {
const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) {
return (currentUser.profile || {}).boardView === 'board-view-lists';
boardView = (currentUser.profile || {}).boardView;
} else {
return window.localStorage.getItem('boardView') === 'board-view-lists';
boardView = window.localStorage.getItem('boardView');
}
return boardView === 'board-view-lists';
},
isViewCalendar() {
const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) {
return (currentUser.profile || {}).boardView === 'board-view-cal';
boardView = (currentUser.profile || {}).boardView;
} else {
return window.localStorage.getItem('boardView') === 'board-view-cal';
boardView = window.localStorage.getItem('boardView');
}
return boardView === 'board-view-cal';
},
hasSwimlanes() {
const currentBoard = Utils.getCurrentBoard();
if (!currentBoard) {
if (process.env.DEBUG === 'true') {
console.log('hasSwimlanes: No current board');
}
return false;
}
try {
const swimlanes = currentBoard.swimlanes();
const hasSwimlanes = swimlanes && swimlanes.length > 0;
if (process.env.DEBUG === 'true') {
console.log('hasSwimlanes: Board has', swimlanes ? swimlanes.length : 0, 'swimlanes');
}
return hasSwimlanes;
} catch (error) {
console.error('hasSwimlanes: Error getting swimlanes:', error);
return false;
}
},
isVerticalScrollbars() {
const user = ReactiveCache.getCurrentUser();
return user && user.isVerticalScrollbars();
},
boardView() {
return Utils.boardView();
},
debugBoardState() {
// Enable debug mode by setting ?debug=1 in URL
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('debug') === '1';
},
debugBoardStateData() {
const currentBoard = Utils.getCurrentBoard();
const currentBoardId = Session.get('currentBoard');
const isBoardReady = this.isBoardReady.get();
const isConverting = this.isConverting.get();
const isMigrating = this.isMigrating.get();
const boardView = Utils.boardView();
if (process.env.DEBUG === 'true') {
console.log('=== BOARD DEBUG STATE ===');
console.log('currentBoardId:', currentBoardId);
console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none');
console.log('isBoardReady:', isBoardReady);
console.log('isConverting:', isConverting);
console.log('isMigrating:', isMigrating);
console.log('boardView:', boardView);
console.log('========================');
}
return {
currentBoardId,
hasCurrentBoard: !!currentBoard,
currentBoardTitle: currentBoard ? currentBoard.title : 'none',
isBoardReady,
isConverting,
isMigrating,
boardView
};
},
openNewListForm() {
if (this.isViewSwimlanes()) {
// The form had been removed in 416b17062e57f215206e93a85b02ef9eb1ab4902
@ -480,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');
}
},
},
];
},
@ -756,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

@ -14,41 +14,41 @@ template(name="boardHeaderBar")
with currentBoard
if currentUser.isBoardAdmin
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
i.fa.fa-pencil-square-o
| ✏️
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}}")
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
if showStarCounter
span
= currentBoard.stars
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
| {{#if currentBoard.isPublic}}🌐{{else}}🔒{{/if}}
span {{_ currentBoard.permission}}
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
span {{_ currentBoard.permission}}
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
i.fa.fa-eye
if $eq watchLevel "tracking"
i.fa.fa-bell
if $eq watchLevel "muted"
i.fa.fa-bell-slash
span {{_ watchLevel}}
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
| 👁️
if $eq watchLevel "tracking"
| 🔔
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}}
if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin
| ❌
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
| 🚪
span {{_ 'log-in'}}
.board-header-btns.center
@ -59,40 +59,41 @@ template(name="boardHeaderBar")
if currentUser
with currentBoard
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
i.fa.fa-pencil-square-o
| ✏️
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'}}")
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
| {{#if currentBoard.isPublic}}🌐{{else}}🔒{{/if}}
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
i.fa.fa-eye
if $eq watchLevel "tracking"
i.fa.fa-bell
if $eq watchLevel "muted"
i.fa.fa-bell-slash
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
| 👁️
if $eq watchLevel "tracking"
| 🔔
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
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin
| ❌
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
| 🚪
if isSandstorm
if currentUser
a.board-header-btn.js-open-archived-board
i.fa.fa-archive
| 📦
//if showSort
// a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
@ -102,56 +103,56 @@ template(name="boardHeaderBar")
a.board-header-btn.js-open-filter-view(
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
class="{{#if Filter.isActive}}emphasis{{/if}}")
i.fa.fa-filter
| 🔽
if Filter.isActive
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
| ❌
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
i.fa.fa-search
| 🔍
unless currentBoard.isTemplatesBoard
a.board-header-btn.js-toggle-board-view(
title="{{_ 'board-view'}}")
i.fa.fa-caret-down
| ▼
if $eq boardView 'board-view-swimlanes'
i.fa.fa-th-large
| 🏊
if $eq boardView 'board-view-lists'
i.fa.fa-trello
| 📋
if $eq boardView 'board-view-cal'
i.fa.fa-calendar
| 📅
if canModifyBoard
a.board-header-btn.js-multiselection-activate(
title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
i.fa.fa-check-square-o
if MultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
| ☑️
if MultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
| ❌
.separator
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
i.fa.fa-navicon
| ☰
template(name="boardVisibilityList")
ul.pop-over-list
li
with "private"
a.js-select-visibility
i.fa.fa-lock.colorful
| 🔒
| {{_ 'private'}}
if visibilityCheck
i.fa.fa-check
| ✅
span.sub-name {{_ 'private-desc'}}
if notAllowPrivateVisibilityOnly
li
with "public"
a.js-select-visibility
i.fa.fa-globe.colorful
| 🌐
| {{_ 'public'}}
if visibilityCheck
i.fa.fa-check
| ✅
span.sub-name {{_ 'public-desc'}}
template(name="boardChangeVisibilityPopup")
@ -162,26 +163,26 @@ template(name="boardChangeWatchPopup")
li
with "watching"
a.js-select-watch
i.fa.fa-eye.colorful
| 👁️
| {{_ 'watching'}}
if watchCheck
i.fa.fa-check
| ✅
span.sub-name {{_ 'watching-info'}}
li
with "tracking"
a.js-select-watch
i.fa.fa-bell.colorful
| 🔔
| {{_ 'tracking'}}
if watchCheck
i.fa.fa-check
| ✅
span.sub-name {{_ 'tracking-info'}}
li
with "muted"
a.js-select-watch
i.fa.fa-bell-slash.colorful
| 🔕
| {{_ 'muted'}}
if watchCheck
i.fa.fa-check
| ✅
span.sub-name {{_ 'muted-info'}}
template(name="boardChangeViewPopup")
@ -189,24 +190,24 @@ template(name="boardChangeViewPopup")
li
with "board-view-swimlanes"
a.js-open-swimlanes-view
i.fa.fa-th-large.colorful
| 🏊
| {{_ 'board-view-swimlanes'}}
if $eq Utils.boardView "board-view-swimlanes"
i.fa.fa-check
| ✅
li
with "board-view-lists"
a.js-open-lists-view
i.fa.fa-trello.colorful
| 📋
| {{_ 'board-view-lists'}}
if $eq Utils.boardView "board-view-lists"
i.fa.fa-check
| ✅
li
with "board-view-cal"
a.js-open-cal-view
i.fa.fa-calendar.colorful
| 📅
| {{_ 'board-view-cal'}}
if $eq Utils.boardView "board-view-cal"
i.fa.fa-check
| ✅
template(name="createBoard")
form
@ -218,11 +219,70 @@ template(name="createBoard")
else
p.quiet
if $eq visibility.get 'public'
span.fa.fa-globe.colorful
span 🌐
= " "
| {{{_ 'board-public-info'}}}
else
span.fa.fa-lock.colorful
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="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'}}.
@ -246,10 +306,10 @@ template(name="createBoard")
// li
// a.js-sort-by(name="{{value.name}}")
// if $eq sortby value.name
// i(class="fa {{Direction}}")
// | {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
// | {{_ value.label }}{{_ value.shortLabel}}
// if $eq sortby value.name
// i(class="fa fa-check")
// | ✅
template(name="boardChangeTitlePopup")
form
@ -269,14 +329,22 @@ template(name="boardCreateRulePopup")
template(name="cardsSortPopup")
ul.pop-over-list
li
a.js-sort-due {{_ 'due-date'}}
a.js-sort-due
| 📅
| {{_ 'due-date'}}
hr
li
a.js-sort-title {{_ 'title-alphabetically'}}
a.js-sort-title
| 🔤
| {{_ 'title-alphabetically'}}
hr
li
a.js-sort-created-desc {{_ 'created-at-newest-first'}}
a.js-sort-created-desc
| ⬇️
| {{_ 'created-at-newest-first'}}
hr
li
a.js-sort-created-asc {{_ 'created-at-oldest-first'}}
a.js-sort-created-asc
| ⬆️
| {{_ 'created-at-oldest-first'}}

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,10 +85,37 @@ BlazeComponent.extendComponent({
},
'click .js-toggle-board-view': Popup.open('boardChangeView'),
'click .js-toggle-sidebar'() {
Sidebar.toggle();
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') {
if (process.env.DEBUG === 'true') {
console.log('Using Sidebar.toggle()');
}
Sidebar.toggle();
} else {
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') {
if (process.env.DEBUG === 'true') {
console.log('Using Blaze helper Sidebar.toggle()');
}
sidebar.toggle();
}
}
}
},
'click .js-open-filter-view'() {
Sidebar.setView('filter');
if (Sidebar) {
Sidebar.setView('filter');
} else {
console.warn('Sidebar not available for setView');
}
},
'click .js-sort-cards': Popup.open('cardsSort'),
/*
@ -102,14 +132,22 @@ BlazeComponent.extendComponent({
*/
'click .js-filter-reset'(event) {
event.stopPropagation();
Sidebar.setView();
if (Sidebar) {
Sidebar.setView();
} else {
console.warn('Sidebar not available for setView');
}
Filter.reset();
},
'click .js-sort-reset'() {
Session.set('sortBy', '');
},
'click .js-open-search-view'() {
Sidebar.setView('search');
if (Sidebar) {
Sidebar.setView('search');
} else {
console.warn('Sidebar not available for setView');
}
},
'click .js-multiselection-activate'() {
const currentCard = Utils.getCurrentCardId();
@ -128,6 +166,7 @@ BlazeComponent.extendComponent({
},
];
},
}).register('boardHeaderBar');
Template.boardHeaderBar.helpers({
@ -137,6 +176,23 @@ Template.boardHeaderBar.helpers({
isSortActive() {
return Session.get('sortBy') ? true : false;
},
sortCardsIcon() {
const sortBy = Session.get('sortBy');
if (!sortBy) {
return '🃏'; // Card icon when nothing is selected
}
// Determine which sort option is active based on sortBy object
if (sortBy.dueAt) {
return '📅'; // Due date icon
} else if (sortBy.title) {
return '🔤'; // Alphabet icon
} else if (sortBy.createdAt) {
return sortBy.createdAt === 1 ? '⬆️' : '⬇️'; // Up/down arrow based on direction
}
return '🃏'; // Default card icon
},
});
Template.boardChangeViewPopup.events({
@ -203,6 +259,7 @@ const CreateBoard = BlazeComponent.extendComponent({
title: title,
permission: 'private',
type: 'template-container',
migrationVersion: 1, // Latest version - no migration needed
}),
);
@ -237,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 {
@ -246,6 +312,7 @@ const CreateBoard = BlazeComponent.extendComponent({
Boards.insert({
title,
permission: visibility,
migrationVersion: 1, // Latest version - no migration needed
}),
);
@ -254,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());
}
},
@ -275,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

@ -0,0 +1,263 @@
/* Original Positions View Styles */
.original-positions-view {
margin: 10px 0;
padding: 15px;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
}
.original-positions-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.original-positions-header .btn {
display: flex;
align-items: center;
gap: 5px;
}
.original-positions-content {
background-color: white;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 15px;
}
.original-positions-loading {
text-align: center;
padding: 20px;
color: #6c757d;
font-style: italic;
}
.original-positions-loading i {
margin-right: 8px;
}
.original-positions-filters {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #dee2e6;
}
.original-positions-filters .btn-group {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.original-positions-filters .btn {
display: flex;
align-items: center;
gap: 5px;
white-space: nowrap;
}
.original-positions-list {
max-height: 400px;
overflow-y: auto;
}
.original-position-item {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
margin-bottom: 10px;
padding: 12px;
transition: all 0.2s ease;
}
.original-position-item:hover {
background-color: #e9ecef;
border-color: #ced4da;
}
.original-position-item:last-child {
margin-bottom: 0;
}
.original-position-item-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-weight: 600;
color: #495057;
}
.original-position-item-header i {
color: #6c757d;
width: 16px;
text-align: center;
}
.entity-type {
background-color: #007bff;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
}
.entity-name {
color: #212529;
font-weight: 600;
}
.entity-id {
color: #6c757d;
font-size: 11px;
font-family: monospace;
}
.original-position-item-details {
margin-left: 24px;
}
.original-position-description {
color: #495057;
margin-bottom: 6px;
font-size: 13px;
}
.original-title {
color: #6c757d;
font-size: 12px;
margin-bottom: 6px;
padding: 4px 6px;
background-color: #e9ecef;
border-radius: 3px;
}
.original-title strong {
color: #495057;
}
.original-position-date {
color: #6c757d;
font-size: 11px;
}
.no-original-positions {
text-align: center;
padding: 40px 20px;
color: #6c757d;
font-style: italic;
}
.no-original-positions i {
font-size: 24px;
margin-bottom: 10px;
display: block;
color: #adb5bd;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.original-positions-view {
margin: 5px 0;
padding: 10px;
}
.original-positions-header {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.original-positions-header .btn {
justify-content: center;
}
.original-positions-filters .btn-group {
justify-content: center;
}
.original-position-item-header {
flex-wrap: wrap;
gap: 6px;
}
.entity-name {
flex: 1;
min-width: 0;
word-break: break-word;
}
.original-position-item-details {
margin-left: 0;
margin-top: 8px;
}
}
/* Dark theme support */
@media (prefers-color-scheme: dark) {
.original-positions-view {
background-color: #2d3748;
border-color: #4a5568;
color: #e2e8f0;
}
.original-positions-content {
background-color: #1a202c;
border-color: #4a5568;
}
.original-position-item {
background-color: #2d3748;
border-color: #4a5568;
color: #e2e8f0;
}
.original-position-item:hover {
background-color: #4a5568;
border-color: #718096;
}
.original-position-item-header {
color: #e2e8f0;
}
.original-position-item-header i {
color: #a0aec0;
}
.entity-name {
color: #e2e8f0;
}
.entity-id {
color: #a0aec0;
}
.original-position-description {
color: #e2e8f0;
}
.original-title {
background-color: #4a5568;
color: #a0aec0;
}
.original-title strong {
color: #e2e8f0;
}
.original-position-date {
color: #a0aec0;
}
.no-original-positions {
color: #a0aec0;
}
.no-original-positions i {
color: #718096;
}
}

View file

@ -0,0 +1,82 @@
<template name="originalPositionsView">
<div class="original-positions-view">
<div class="original-positions-header">
<button class="btn btn-sm btn-outline-secondary" onclick="{{toggleOriginalPositions}}">
<i class="fa fa-history"></i>
{{#if isShowingOriginalPositions}}Hide{{else}}Show{{/if}} Original Positions
</button>
{{#if isShowingOriginalPositions}}
<button class="btn btn-sm btn-outline-primary" onclick="{{refreshHistory}}">
<i class="fa fa-refresh"></i> Refresh
</button>
{{/if}}
</div>
{{#if isShowingOriginalPositions}}
<div class="original-positions-content">
{{#if isLoading}}
<div class="original-positions-loading">
<i class="fa fa-spinner fa-spin"></i> Loading original positions...
</div>
{{else}}
<div class="original-positions-filters">
<div class="btn-group btn-group-sm" role="group">
<button type="button"
class="btn {{#if isFilterType 'all'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
onclick="{{setFilterType 'all'}}">
All
</button>
<button type="button"
class="btn {{#if isFilterType 'swimlane'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
onclick="{{setFilterType 'swimlane'}}">
<i class="fa fa-bars"></i> Swimlanes
</button>
<button type="button"
class="btn {{#if isFilterType 'list'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
onclick="{{setFilterType 'list'}}">
<i class="fa fa-columns"></i> Lists
</button>
<button type="button"
class="btn {{#if isFilterType 'card'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
onclick="{{setFilterType 'card'}}">
<i class="fa fa-sticky-note"></i> Cards
</button>
</div>
</div>
<div class="original-positions-list">
{{#each getFilteredHistory}}
<div class="original-position-item">
<div class="original-position-item-header">
<i class="fa {{getEntityTypeIcon entityType}}"></i>
<span class="entity-type">{{getEntityTypeLabel entityType}}</span>
<span class="entity-name">{{getEntityDisplayName this}}</span>
<span class="entity-id">({{entityId}})</span>
</div>
<div class="original-position-item-details">
<div class="original-position-description">
{{getEntityOriginalPositionDescription this}}
</div>
{{#if originalTitle}}
<div class="original-title">
<strong>Original title:</strong> {{originalTitle}}
</div>
{{/if}}
<div class="original-position-date">
<small class="text-muted">Created: {{formatDate createdAt}}</small>
</div>
</div>
</div>
{{else}}
<div class="no-original-positions">
<i class="fa fa-info-circle"></i>
No original position data available for this board.
</div>
{{/each}}
</div>
{{/if}}
</div>
{{/if}}
</div>
</template>

View file

@ -0,0 +1,148 @@
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
import { ReactiveVar } from 'meteor/reactive-var';
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import './originalPositionsView.html';
/**
* Component to display original positions for all entities on a board
*/
class OriginalPositionsViewComponent extends BlazeComponent {
onCreated() {
super.onCreated();
this.showOriginalPositions = new ReactiveVar(false);
this.boardHistory = new ReactiveVar([]);
this.isLoading = new ReactiveVar(false);
this.filterType = new ReactiveVar('all'); // 'all', 'swimlane', 'list', 'card'
}
onRendered() {
super.onRendered();
this.loadBoardHistory();
}
loadBoardHistory() {
const boardId = Session.get('currentBoard');
if (!boardId) return;
this.isLoading.set(true);
Meteor.call('positionHistory.getBoardHistory', boardId, (error, result) => {
this.isLoading.set(false);
if (error) {
console.error('Error loading board history:', error);
this.boardHistory.set([]);
} else {
this.boardHistory.set(result);
}
});
}
toggleOriginalPositions() {
this.showOriginalPositions.set(!this.showOriginalPositions.get());
}
isShowingOriginalPositions() {
return this.showOriginalPositions.get();
}
isLoading() {
return this.isLoading.get();
}
getBoardHistory() {
return this.boardHistory.get();
}
getFilteredHistory() {
const history = this.getBoardHistory();
const filterType = this.filterType.get();
if (filterType === 'all') {
return history;
}
return history.filter(item => item.entityType === filterType);
}
getSwimlanesHistory() {
return this.getBoardHistory().filter(item => item.entityType === 'swimlane');
}
getListsHistory() {
return this.getBoardHistory().filter(item => item.entityType === 'list');
}
getCardsHistory() {
return this.getBoardHistory().filter(item => item.entityType === 'card');
}
setFilterType(type) {
this.filterType.set(type);
}
getFilterType() {
return this.filterType.get();
}
getEntityDisplayName(entity) {
const position = entity.originalPosition || {};
return position.title || `Entity ${entity.entityId}`;
}
getEntityOriginalPositionDescription(entity) {
const position = entity.originalPosition || {};
let description = `Position: ${position.sort || 0}`;
if (entity.entityType === 'list' && entity.originalSwimlaneId) {
description += ` in swimlane ${entity.originalSwimlaneId}`;
} else if (entity.entityType === 'card') {
if (entity.originalSwimlaneId) {
description += ` in swimlane ${entity.originalSwimlaneId}`;
}
if (entity.originalListId) {
description += ` in list ${entity.originalListId}`;
}
}
return description;
}
getEntityTypeIcon(entityType) {
switch (entityType) {
case 'swimlane':
return 'fa-bars';
case 'list':
return 'fa-columns';
case 'card':
return 'fa-sticky-note';
default:
return 'fa-question';
}
}
getEntityTypeLabel(entityType) {
switch (entityType) {
case 'swimlane':
return 'Swimlane';
case 'list':
return 'List';
case 'card':
return 'Card';
default:
return 'Unknown';
}
}
formatDate(date) {
return new Date(date).toLocaleString();
}
refreshHistory() {
this.loadBoardHistory();
}
}
OriginalPositionsViewComponent.register('originalPositionsView');
export default OriginalPositionsViewComponent;

View file

@ -336,3 +336,36 @@
margin-top: 10px;
}
}
/* Attachment migration styles */
.attachment-item.migrating {
position: relative;
opacity: 0.7;
}
.attachment-migration-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
border-radius: 4px;
}
.migration-spinner {
font-size: 24px;
color: #007cba;
margin-bottom: 8px;
}
.migration-text {
font-size: 12px;
color: #666;
text-align: center;
}

View file

@ -34,10 +34,10 @@ template(name="attachmentViewer")
#viewer-overlay.hidden
#viewer-top-bar
span#attachment-name
a#viewer-close.fa.fa-times-thin
a#viewer-close
#viewer-container
i.fa.fa-chevron-left.attachment-arrow#prev-attachment
| ◀️
#viewer-content
img#image-viewer.hidden
video#video-viewer.hidden(controls="true")
@ -45,7 +45,7 @@ template(name="attachmentViewer")
object#pdf-viewer.hidden(type="application/pdf")
span.pdf-preview-error {{_ 'preview-pdf-not-supported' }}
object#txt-viewer.hidden(type="text/plain")
i.fa.fa-chevron-right.attachment-arrow#next-attachment
| ▶️
template(name="attachmentGallery")
@ -53,11 +53,11 @@ template(name="attachmentGallery")
if canModifyCard
a.attachment-item.add-attachment.js-add-attachment
i.fa.fa-plus.icon
|
each attachments
.attachment-item
.attachment-item(class="{{#if isAttachmentMigrating _id}}migrating{{/if}}")
.attachment-thumbnail-container.open-preview(data-attachment-id="{{_id}}" data-card-id="{{ meta.cardId }}")
if link
if(isImage)
@ -86,25 +86,32 @@ template(name="attachmentGallery")
= name
span.file-size ({{fileSize size}})
.attachment-actions
a.js-download(href="{{link}}?download=true", download="{{name}}")
i.fa.fa-download.icon(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
i.fa.fa-pencil-square-o.icon(title="{{_ 'rename'}}")
a.js-confirm-delete
i.fa.fa-trash.icon(title="{{_ 'delete'}}")
a.fa.fa-navicon.icon.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
.attachment-migration-overlay
.migration-spinner
| ⚙️
.migration-text {{_ 'migrating-attachment'}}
template(name="attachmentActionsPopup")
ul.pop-over-list
li
if isImage
a(class="{{#if isCover}}js-remove-cover{{else}}js-add-cover{{/if}}")
i.fa.fa-book
i.fa.fa-picture-o
| 📖
| 🖼️
if isCover
| {{_ 'remove-cover'}}
else
@ -112,7 +119,7 @@ template(name="attachmentActionsPopup")
if currentUser.isBoardAdmin
if isImage
a(class="{{#if isBackgroundImage}}js-remove-background-image{{else}}js-add-background-image{{/if}}")
i.fa.fa-picture-o
| 🖼️
if isBackgroundImage
| {{_ 'remove-background-image'}}
else
@ -120,19 +127,19 @@ template(name="attachmentActionsPopup")
if $neq versions.original.storage "fs"
a.js-move-storage-fs
i.fa.fa-arrow-right
| ▶️
| {{_ 'attachment-move-storage-fs'}}
if $neq versions.original.storage "gridfs"
if versions.original.storage
a.js-move-storage-gridfs
i.fa.fa-arrow-right
| ▶️
| {{_ 'attachment-move-storage-gridfs'}}
if $neq versions.original.storage "s3"
if versions.original.storage
a.js-move-storage-s3
i.fa.fa-arrow-right
| ▶️
| {{_ 'attachment-move-storage-s3'}}
template(name="attachmentRenamePopup")

View file

@ -3,6 +3,7 @@ import { ObjectID } from 'bson';
import DOMPurify from 'dompurify';
import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
import uploadProgressManager from '../../lib/uploadProgressManager';
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
const filesize = require('filesize');
const prettyMilliseconds = require('pretty-ms');
@ -342,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');
}
@ -576,3 +577,20 @@ BlazeComponent.extendComponent({
]
}
}).register('attachmentRenamePopup');
// Template helpers for attachment migration status
Template.registerHelper('attachmentMigrationStatus', function(attachmentId) {
return attachmentMigrationManager.getAttachmentMigrationStatus(attachmentId);
});
Template.registerHelper('isAttachmentMigrating', function(attachmentId) {
return attachmentMigrationManager.isAttachmentBeingMigrated(attachmentId);
});
Template.registerHelper('attachmentMigrationProgress', function() {
return attachmentMigrationManager.attachmentMigrationProgress.get();
});
Template.registerHelper('attachmentMigrationStatusText', function() {
return attachmentMigrationManager.attachmentMigrationStatus.get();
});

View file

@ -6,10 +6,10 @@ template(name="cardCustomFieldsPopup")
span.full-name
= name
if hasCustomField
i.fa.fa-check
| ✅
hr
a.quiet-button.full.js-settings
i.fa.fa-cog
| ⚙️
span {{_ 'settings'}}
template(name="cardCustomField")
@ -22,7 +22,7 @@ template(name="cardCustomField-text")
= value
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
else
a.js-open-inlined-form
if value
@ -41,7 +41,7 @@ template(name="cardCustomField-number")
input(type="number" value=data.value)
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
else
a.js-open-inlined-form
if value
@ -66,7 +66,7 @@ template(name="cardCustomField-currency")
input(type="text" value=data.value autofocus)
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
else
a.js-open-inlined-form
if value
@ -113,7 +113,7 @@ template(name="cardCustomField-dropdown")
= name
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
else
a.js-open-inlined-form
if value
@ -134,7 +134,7 @@ template(name="cardCustomField-stringtemplate")
input.js-card-customfield-stringtemplate-item.last(type="text" value="" placeholder="{{_ 'custom-field-stringtemplate-item-placeholder'}}" autofocus)
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
else
a.js-open-inlined-form
if value

View file

@ -1,6 +1,27 @@
import moment from 'moment/min/moment-with-locales';
import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker';
import { ReactiveCache } from '/imports/reactiveCache';
import {
formatDateTime,
formatDate,
formatDateByUserPreference,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar
} from '/imports/lib/dateUtils';
import Cards from '/models/cards';
import { CustomFieldStringTemplate } from '/client/lib/customFields'
@ -134,31 +155,33 @@ CardCustomField.register('cardCustomField');
super.onCreated();
const self = this;
self.date = ReactiveVar();
self.now = ReactiveVar(moment());
self.now = ReactiveVar(now());
window.setInterval(() => {
self.now.set(moment());
self.now.set(now());
}, 60000);
self.autorun(() => {
self.date.set(moment(self.data().value));
self.date.set(new Date(self.data().value));
});
}
showWeek() {
return this.date.get().week().toString();
return getISOWeek(this.date.get()).toString();
}
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() {
// this will start working once mquandalle:moment
// is updated to at least moment.js 2.10.5
// until then, the date is displayed in the "L" format
return this.date.get().calendar(null, {
sameElse: 'llll',
});
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
showISODate() {
@ -167,8 +190,8 @@ CardCustomField.register('cardCustomField');
classes() {
if (
this.date.get().isBefore(this.now.get(), 'minute') &&
this.now.get().isBefore(this.data().value)
isBefore(this.date.get(), this.now.get(), 'minute') &&
isBefore(this.now.get(), this.data().value, 'minute')
) {
return 'current';
}
@ -176,7 +199,7 @@ CardCustomField.register('cardCustomField');
}
showTitle() {
return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
return `${TAPi18n.__('card-start-on')} ${this.date.get().toLocaleString()}`;
}
events() {
@ -195,7 +218,7 @@ CardCustomField.register('cardCustomField');
const self = this;
self.card = Utils.getCurrentCard();
self.customFieldId = this.data()._id;
this.data().value && this.date.set(moment(this.data().value));
this.data().value && this.date.set(new Date(this.data().value));
}
_storeDate(date) {

View file

@ -8,57 +8,134 @@
.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: #dbdbdb; /* Light grey for received */
}
.card-date.received-date:hover,
.card-date.received-date.is-active {
background-color: #b3b3b3;
}
.card-date.start-date {
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: #7dd87d;
}
.card-date.due-date {
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: #e6c200;
}
.card-date.end-date {
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: #ff9999;
}
.card-date.end-date time::before {
content: "\f253";
content: "🏁"; /* Finish flag - represents end/completion */
}
.card-date.due-date time::before {
content: "\f090";
content: "⏰"; /* Alarm clock - represents due/deadline */
}
.card-date.start-date time::before {
content: "\f251";
content: "🚀"; /* Rocket - represents start/launch */
}
.card-date.received-date time::before {
content: "\f08b";
content: "📥"; /* Inbox tray - represents received/incoming */
}
/* 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 */
}
.card-date time::before {
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
-webkit-font-smoothing: antialiased;
margin-right: 0.3em;
display: inline-block;
}
.customfield-date {
display: block;

View file

@ -21,3 +21,132 @@ template(name="dateCustomField")
if showWeekOfYear
b
| {{showWeek}}
template(name="minicardReceivedDate")
if canModifyCard
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.received-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="minicardStartDate")
if canModifyCard
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.start-date(title="{{_ 'card-start'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="minicardDueDate")
if canModifyCard
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.due-date(title="{{_ 'card-due'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="minicardEndDate")
if canModifyCard
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.end-date(title="{{_ 'card-end'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="minicardCustomFieldDate")
a(title="{{_ 'date'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
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

@ -1,17 +1,38 @@
import moment from 'moment/min/moment-with-locales';
import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker';
import {
formatDateTime,
formatDate,
formatDateByUserPreference,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar,
diff
} from '/imports/lib/dateUtils';
// editCardReceivedDatePopup
(class extends DatePicker {
onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
super.onCreated(formatDateTime(now()));
this.data().getReceived() &&
this.date.set(moment(this.data().getReceived()));
this.date.set(new Date(this.data().getReceived()));
}
_storeDate(date) {
this.card.setReceived(moment(date).format('YYYY-MM-DD HH:mm'));
this.card.setReceived(formatDateTime(date));
}
_deleteDate() {
@ -22,22 +43,28 @@ import { DatePicker } from '/client/lib/datepicker';
// editCardStartDatePopup
(class extends DatePicker {
onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
this.data().getStart() && this.date.set(moment(this.data().getStart()));
super.onCreated(formatDateTime(now()));
this.data().getStart() && this.date.set(new Date(this.data().getStart()));
}
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) {
this.card.setStart(moment(date).format('YYYY-MM-DD HH:mm'));
this.card.setStart(formatDateTime(date));
}
_deleteDate() {
@ -49,18 +76,16 @@ import { DatePicker } from '/client/lib/datepicker';
(class extends DatePicker {
onCreated() {
super.onCreated('1970-01-01 17:00:00');
this.data().getDue() && this.date.set(moment(this.data().getDue()));
this.data().getDue() && this.date.set(new Date(this.data().getDue()));
}
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) {
this.card.setDue(moment(date).format('YYYY-MM-DD HH:mm'));
this.card.setDue(formatDateTime(date));
}
_deleteDate() {
@ -71,19 +96,17 @@ import { DatePicker } from '/client/lib/datepicker';
// editCardEndDatePopup
(class extends DatePicker {
onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
this.data().getEnd() && this.date.set(moment(this.data().getEnd()));
super.onCreated(formatDateTime(now()));
this.data().getEnd() && this.date.set(new Date(this.data().getEnd()));
}
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) {
this.card.setEnd(moment(date).format('YYYY-MM-DD HH:mm'));
this.card.setEnd(formatDateTime(date));
}
_deleteDate() {
@ -100,27 +123,29 @@ const CardDate = BlazeComponent.extendComponent({
onCreated() {
const self = this;
self.date = ReactiveVar();
self.now = ReactiveVar(moment());
self.now = ReactiveVar(now());
window.setInterval(() => {
self.now.set(moment());
self.now.set(now());
}, 60000);
},
showWeek() {
return this.date.get().week().toString();
return getISOWeek(this.date.get()).toString();
},
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() {
// this will start working once mquandalle:moment
// is updated to at least moment.js 2.10.5
// until then, the date is displayed in the "L" format
return this.date.get().calendar(null, {
sameElse: 'llll',
});
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
},
showISODate() {
@ -133,7 +158,7 @@ class CardReceivedDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getReceived()));
self.date.set(new Date(self.data().getReceived()));
});
}
@ -143,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() {
@ -173,26 +203,35 @@ class CardStartDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getStart()));
self.date.set(new Date(self.data().getStart()));
});
}
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() {
@ -208,27 +247,48 @@ class CardDueDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getDue()));
self.date.set(new Date(self.data().getDue()));
});
}
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() {
@ -244,22 +304,33 @@ class CardEndDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getEnd()));
self.date.set(new Date(self.data().getEnd()));
});
}
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() {
@ -279,16 +350,21 @@ class CardCustomFieldDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().value));
self.date.set(new Date(self.data().value));
});
}
showWeek() {
return this.date.get().week().toString();
return getISOWeek(this.date.get()).toString();
}
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() {
@ -301,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() {
@ -315,32 +394,62 @@ class CardCustomFieldDate extends CardDate {
CardCustomFieldDate.register('cardCustomFieldDate');
(class extends CardReceivedDate {
template() {
return 'minicardReceivedDate';
}
showDate() {
return this.date.get().format('L');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardReceivedDate'));
(class extends CardStartDate {
template() {
return 'minicardStartDate';
}
showDate() {
return this.date.get().format('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'));
(class extends CardDueDate {
template() {
return 'minicardDueDate';
}
showDate() {
return this.date.get().format('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'));
(class extends CardEndDate {
template() {
return 'minicardEndDate';
}
showDate() {
return this.date.get().format('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'));
(class extends CardCustomFieldDate {
template() {
return 'minicardCustomFieldDate';
}
showDate() {
return this.date.get().format('L');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardCustomFieldDate'));
@ -349,7 +458,7 @@ class VoteEndDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getVoteEnd()));
self.date.set(new Date(self.data().getVoteEnd()));
});
}
classes() {
@ -357,10 +466,12 @@ class VoteEndDate 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')} ${this.date.get().toLocaleString()}`;
}
events() {
@ -376,7 +487,7 @@ class PokerEndDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getPokerEnd()));
self.date.set(new Date(self.data().getPokerEnd()));
});
}
classes() {
@ -384,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,11 +1,39 @@
/* 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: 0.4vw;
border-radius: 3px;
display: block;
position: relative;
float: left;
height: 4vw;
width: 4vw;
margin: 0.4vh;
height: 30px;
width: 30px;
margin: .3vh;
cursor: pointer;
user-select: none;
z-index: 1;
@ -34,11 +62,11 @@
background-color: #b3b3b3;
border: 1px solid #fff;
border-radius: 50%;
height: 1vw;
width: 1vw;
height: 7px;
width: 7px;
position: absolute;
right: -0.1vw;
bottom: -0.1vw;
right: -1px;
bottom: -1px;
border: 1px solid #fff;
z-index: 15;
}

View file

@ -12,15 +12,19 @@ template(name="cardDetails")
else
unless isMiniScreen
unless isPopup
a.fa.fa-times-thin.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
| ❌
if canModifyCard
if cardMaximized
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
| 🔽
else
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
a.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
| 🔼
if canModifyCard
a.fa.fa-navicon.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
a.fa.fa-link.card-copy-button.js-copy-link(
a.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
| ☰
a.card-copy-button.js-copy-link(
id="cardURL_copy"
class="fa-link"
title="{{_ 'copy-card-link-to-clipboard'}}"
@ -29,10 +33,12 @@ template(name="cardDetails")
span.copied-tooltip {{_ 'copied'}}
else
unless isPopup
a.fa.fa-times-thin.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
| ❌
if canModifyCard
a.fa.fa-navicon.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
a.fa.fa-link.card-copy-mobile-button.js-copy-link(
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
| ☰
a.card-copy-mobile-button.js-copy-link(
id="cardURL_copy"
class="fa-link"
title="{{_ 'copy-card-link-to-clipboard'}}"
@ -47,7 +53,8 @@ template(name="cardDetails")
| ##{getCardNumber}
= getTitle
if isWatching
i.card-details-watch.fa.fa-eye
i.card-details-watch
| 👁️
.card-details-path
each parentList
| &nbsp; &gt; &nbsp;
@ -69,7 +76,7 @@ template(name="cardDetails")
if hasActiveUploads
.card-details-upload-progress
.upload-progress-header
i.fa.fa-upload
| 📤
span {{_ 'uploading-files'}} ({{uploadCount}})
each uploads
.upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
@ -78,11 +85,11 @@ template(name="cardDetails")
.upload-progress-fill(style="width: {{progress}}%")
if $eq status 'error'
.upload-progress-error
i.fa.fa-exclamation-triangle
| ⚠️
span {{_ 'upload-failed'}}
else if $eq status 'completed'
.upload-progress-success
i.fa.fa-check
| ✅
span {{_ 'upload-completed'}}
.card-details-left
@ -91,7 +98,7 @@ template(name="cardDetails")
if currentBoard.allowsLabels
.card-details-item.card-details-item-labels
h3.card-details-item-title
i.fa.fa-tags
| 🏷️
| {{_ 'labels'}}
a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
each labels
@ -101,15 +108,25 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
i.fa.fa-plus
|
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
i.fa.fa-sign-out
| 📥
| {{_ 'card-received'}}
if getReceived
+cardReceivedDate
@ -117,12 +134,12 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-received-date
i.fa.fa-plus
|
if currentBoard.allowsStartDate
.card-details-item.card-details-item-start
h3.card-details-item-title
i.fa.fa-hourglass-start
| 🚀
| {{_ 'card-start'}}
if getStart
+cardStartDate
@ -130,12 +147,12 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-start-date
i.fa.fa-plus
|
if currentBoard.allowsDueDate
.card-details-item.card-details-item-due
h3.card-details-item-title
i.fa.fa-sign-in
| ⏰
| {{_ 'card-due'}}
if getDue
+cardDueDate
@ -143,12 +160,12 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-due-date
i.fa.fa-plus
|
if currentBoard.allowsEndDate
.card-details-item.card-details-item-end
h3.card-details-item-title
i.fa.fa-hourglass-end
| 🏁
| {{_ 'card-end'}}
if getEnd
+cardEndDate
@ -156,7 +173,7 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-end-date
i.fa.fa-plus
|
if currentBoard.hasAnyAllowsUser
hr
@ -164,7 +181,7 @@ template(name="cardDetails")
if currentBoard.allowsCreator
.card-details-item.card-details-item-creator
h3.card-details-item-title
i.fa.fa-user
| 👤
| {{_ 'creator'}}
+userAvatar(userId=userId noRemove=true)
@ -174,7 +191,7 @@ template(name="cardDetails")
if currentBoard.allowsMembers
.card-details-item.card-details-item-members
h3.card-details-item-title
i.fa.fa-users
| &#x1F465;
| {{_ 'members'}}
each userId in getMembers
+userAvatar(userId=userId cardId=_id)
@ -182,30 +199,30 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
i.fa.fa-plus
|
//if assigneeSelected
if currentBoard.allowsAssignee
.card-details-item.card-details-item-assignees
h3.card-details-item-title
i.fa.fa-user
| 👤
| {{_ 'assignee'}}
each userId in getAssignees
+userAvatar(userId=userId cardId=_id assignee=true)
| {{! XXX Hack to hide syntaxic coloration /// }}
if canModifyCard
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
i.fa.fa-plus
|
if currentUser.isWorker
unless assigneeSelected
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
i.fa.fa-plus
|
//.card-details-items
if currentBoard.allowsRequestedBy
.card-details-item.card-details-item-name
h3.card-details-item-title
i.fa.fa-shopping-cart
| 🛒
| {{_ 'requested-by'}}
if canModifyCard
unless currentUser.isWorker
@ -225,7 +242,7 @@ template(name="cardDetails")
if currentBoard.allowsAssignedBy
.card-details-item.card-details-item-name
h3.card-details-item-title
i.fa.fa-user-plus
| ✍️
| {{_ 'assigned-by'}}
if canModifyCard
unless currentUser.isWorker
@ -248,7 +265,7 @@ template(name="cardDetails")
if currentBoard.allowsCardSortingByNumber
.card-details-item.card-details-sort-order
h3.card-details-item-title
i.fa.fa-sort
| 🔢
| {{_ 'sort'}}
if canModifyCard
+inlinedForm(classNames="js-card-details-sort")
@ -261,7 +278,7 @@ template(name="cardDetails")
if currentBoard.allowsShowLists
.card-details-item.card-details-show-lists
h3.card-details-item-title
i.fa.fa-list
| 📋
| {{_ 'list'}}
select.js-select-card-details-lists(disabled="{{#unless canModifyCard}}disabled{{/unless}}")
each currentBoard.lists
@ -287,7 +304,7 @@ template(name="cardDetails")
hr
.card-details-item.card-details-item-customfield
h3.card-details-item-title
i.fa.fa-list-alt
| 📋-alt
= definition.name
+cardCustomField
@ -298,14 +315,14 @@ template(name="cardDetails")
else
input.toggle-switch(type="checkbox" id="toggleCustomFieldsGridButton")
label.toggle-label(for="toggleCustomFieldsGridButton")
a.fa.fa-plus.js-custom-fields.card-details-item.custom-fields(title="{{_ 'custom-fields'}}")
a.js-custom-fields.card-details-item.custom-fields(title="{{_ 'custom-fields'}}")
if getVoteQuestion
hr
.vote-title
div.flex
h3
i.fa.fa-thumbs-up
| 👍
| {{_ 'vote-question'}}
if getVoteEnd
+voteEndDate
@ -323,11 +340,11 @@ template(name="cardDetails")
if showVotingButtons
button.card-details-green.js-vote.js-vote-positive(class="{{#if voteState}}voted{{/if}}")
if voteState
i.fa.fa-thumbs-up
| 👍
| {{_ 'vote-for-it'}}
button.card-details-red.js-vote.js-vote-negative(class="{{#if $eq voteState false}}voted{{/if}}")
if $eq voteState false
i.fa.fa-thumbs-down
| 👎
| {{_ 'vote-against'}}
if getPokerQuestion
@ -335,7 +352,7 @@ template(name="cardDetails")
.poker-title
div.flex
h3
i.fa.fa-thumbs-up
| 👍
| {{_ 'poker-question'}}
if getPokerEnd
+pokerEndDate
@ -350,52 +367,52 @@ template(name="cardDetails")
.poker-card
span.inner.js-poker.js-poker-vote-one(class="{{#if $eq pokerState 'one'}}poker-voted{{/if}}") {{_ 'poker-one'}}
if $eq pokerState "one"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-two(class="{{#if $eq pokerState 'two'}}poker-voted{{/if}}") {{_ 'poker-two'}}
if $eq pokerState "two"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-three(class="{{#if $eq pokerState 'three'}}poker-voted{{/if}}") {{_ 'poker-three'}}
if $eq pokerState "three"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-five(class="{{#if $eq pokerState 'five'}}poker-voted{{/if}}") {{_ 'poker-five'}}
if $eq pokerState "five"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-eight(class="{{#if $eq pokerState 'eight'}}poker-voted{{/if}}") {{_ 'poker-eight'}}
if $eq pokerState "eight"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-thirteen(class="{{#if $eq pokerState 'thirteen'}}poker-voted{{/if}}") {{_ 'poker-thirteen'}}
if $eq pokerState "thirteen"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-twenty(class="{{#if $eq pokerState 'twenty'}}poker-voted{{/if}}") {{_ 'poker-twenty'}}
if $eq pokerState "twenty"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-forty(class="{{#if $eq pokerState 'forty'}}poker-voted{{/if}}") {{_ 'poker-forty'}}
if $eq pokerState "forty"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-one-hundred(class="{{#if $eq pokerState 'oneHundred'}}poker-voted{{/if}}") {{_ 'poker-oneHundred'}}
if $eq pokerState "oneHundred"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-unsure(class="{{#if $eq pokerState 'unsure'}}poker-voted{{/if}}") {{_ 'poker-unsure'}}
if $eq pokerState "unsure"
i.fa.fa-check
| ✅
if currentUser.isBoardAdmin
button.card-details-blue.js-poker-finish(class="{{#if $eq voteState false}}poker-voted{{/if}}") {{_ 'poker-finish'}}
@ -525,7 +542,7 @@ template(name="cardDetails")
button.card-details-red.js-poker-replay(class="{{#if $eq voteState false}}voted{{/if}}") {{_ 'poker-replay'}}
div.estimation-add
button.js-poker-estimation
i.fa.fa-plus
|
| {{_ 'set-estimation'}}
input(type=text,autofocus value=getPokerEstimation,id="pokerEstimation")
@ -535,18 +552,18 @@ template(name="cardDetails")
if currentBoard.allowsDescriptionTitle
hr
h3.card-details-item-title
i.fa.fa-align-left
| 📝
| {{_ 'description'}}
if currentBoard.allowsDescriptionText
+inlinedCardDescription(classNames="card-description js-card-description")
+descriptionForm
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
else
if currentBoard.allowsDescriptionText
a.js-open-inlined-form(title="{{_ 'edit'}}" value=title)
i.fa.fa-pencil-square-o
| ✏️
a.js-open-inlined-form(title="{{_ 'edit'}}" value=title)
if getDescription
+viewer
@ -576,7 +593,7 @@ template(name="cardDetails")
if currentBoard.allowsAttachments
hr
h3.card-details-item-title
i.fa.fa-paperclip
| 📎
| {{_ 'attachments'}}
if Meteor.settings.public.attachmentsUploadMaxSize
| {{_ 'max-upload-filesize'}} {{Meteor.settings.public.attachmentsUploadMaxSize}}
@ -592,7 +609,7 @@ template(name="cardDetails")
unless currentUser.isNoComments
.comment-title
h3.card-details-item-title
i.fa.fa-comment-o
| 💬
| {{_ 'comments'}}
if currentBoard.allowsComments
@ -607,7 +624,7 @@ template(name="cardDetails")
unless currentUser.isNoComments
.activity-title
h3.card-details-item-title
i.fa.fa-history
| 📜
| {{ _ 'activities'}}
if currentUser.isBoardMember
.material-toggle-switch(title="{{_ 'show-activities'}}")
@ -627,41 +644,41 @@ template(name="cardDetails")
+activities(card=this mode="card")
template(name="editCardTitleForm")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
a(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
textarea.js-edit-card-title(rows='1' autofocus dir="auto")
= getTitle
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-title-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
template(name="editCardRequesterForm")
input.js-edit-card-requester(type='text' autofocus value=getRequestedBy dir="auto")
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-requester-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
template(name="editCardAssignerForm")
input.js-edit-card-assigner(type='text' autofocus value=getAssignedBy dir="auto")
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-assigner-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
template(name="editCardSortOrderForm")
input.js-edit-card-sort(type='text' autofocus value=sort dir="auto")
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-sort-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
template(name="cardDetailsActionsPopup")
ul.pop-over-list
li
a.js-toggle-watch-card
if isWatching
i.fa.fa-eye
| 👁️
| {{_ 'unwatch'}}
else
i.fa.fa-eye-slash
| 👁️-slash
| {{_ 'watch'}}
hr
if canModifyCard
@ -672,16 +689,16 @@ template(name="cardDetailsActionsPopup")
//li: a.js-attachments {{_ 'card-edit-attachments'}}
li
a.js-start-voting
i.fa.fa-thumbs-up
| 👍
| {{_ 'card-edit-voting'}}
li
a.js-start-planning-poker
i.fa.fa-thumbs-up
| 👍
| {{_ 'card-edit-planning-poker'}}
if currentUser.isBoardAdmin
li
a.js-custom-fields
i.fa.fa-list-alt
| 📋-alt
| {{_ 'card-edit-custom-fields'}}
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
@ -689,75 +706,75 @@ template(name="cardDetailsActionsPopup")
//li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
li
a.js-spent-time
i.fa.fa-clock-o
| 🕐
| {{_ 'editCardSpentTimePopup-title'}}
li
a.js-set-card-color
i.fa.fa-paint-brush
| 🎨
| {{_ 'setCardColorPopup-title'}}
li
a.js-toggle-show-list-on-minicard
if showListOnMinicard
i.fa.fa-eye
| 👁️
| {{_ 'hide-list-on-minicard'}}
else
i.fa.fa-eye-slash
| 👁️-slash
| {{_ 'show-list-on-minicard'}}
hr
ul.pop-over-list
li
a.js-export-card
i.fa.fa-share-alt
| 📤
| {{_ 'export-card'}}
hr
ul.pop-over-list
li
a.js-move-card-to-top
i.fa.fa-arrow-up
| ⬆️
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
i.fa.fa-arrow-down
| ⬇️
| {{_ 'moveCardToBottom-title'}}
hr
ul.pop-over-list
if currentUser.isBoardAdmin
li
a.js-move-card
i.fa.fa-arrow-right
| ➡️
| {{_ 'moveCardPopup-title'}}
unless currentUser.isWorker
li
a.js-copy-card
i.fa.fa-copy
| 📋
| {{_ 'copyCardPopup-title'}}
unless currentUser.isWorker
ul.pop-over-list
li
a.js-copy-checklist-cards
i.fa.fa-copy
i.fa.fa-copy
| 📋
| 📋
| {{_ 'copyManyCardsPopup-title'}}
unless archived
hr
ul.pop-over-list
li
a.js-archive
i.fa.fa-arrow-right
i.fa.fa-archive
| ➡️
| 📦
| {{_ 'archive-card'}}
hr
ul.pop-over-list
li
a.js-more
i.fa.fa-link
| 🔗
| {{_ 'cardMorePopup-title'}}
template(name="exportCardPopup")
ul.pop-over-list
li
a(href="{{exportUrlCardPDF}}",, download="{{exportFilenameCardPDF}}")
i.fa.fa-share-alt
| 📤
| {{_ 'export-card-pdf'}}
template(name="moveCardPopup")
@ -812,7 +829,7 @@ template(name="cardMembersPopup")
= user.profile.fullname
| (<span class="username">{{ user.username }}</span>)
if isCardMember
i.fa.fa-check
| ✅
template(name="cardAssigneesPopup")
input.card-assignees-filter(type="text" placeholder="{{_ 'search'}}")
@ -826,7 +843,7 @@ template(name="cardAssigneesPopup")
= user.profile.fullname
| (<span class="username">{{ user.username }}</span>)
if isCardAssignee
i.fa.fa-check
| ✅
if currentUser.isWorker
ul.pop-over-list.js-card-assignee-list
li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
@ -836,7 +853,7 @@ template(name="cardAssigneesPopup")
= currentUser.profile.fullname
| (<span class="username">{{ currentUser.username }}</span>)
if currentUser.isCardAssignee
i.fa.fa-check
| ✅
template(name="cardAssigneePopup")
.board-assignee-menu
@ -860,7 +877,7 @@ template(name="cardMorePopup")
span.clearfix
span {{_ 'link-card'}}
= ' '
i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
| {{#if board.isPublic}}🌐{{else}}🔒{{/if}}
input.inline-input(type="text" id="cardURL" readonly value="{{ originRelativeUrl }}" autofocus="autofocus")
button.js-copy-card-link-to-clipboard(class="btn" id="clipboard") {{_ 'copy-card-link-to-clipboard'}}
.copied-tooltip {{_ 'copied'}}
@ -902,7 +919,7 @@ template(name="setCardColorPopup")
unless $eq color 'white'
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color)
i.fa.fa-check
| ✅
button.primary.confirm.js-submit {{_ 'save'}}
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
@ -936,12 +953,12 @@ template(name="cardStartVotingPopup")
.materialCheckBox#vote-public(name="vote-public" class="{{#if votePublic}}is-checked{{/if}}")
span {{_ 'vote-public'}}
.check-div.flex
i.fa.fa-hourglass-end
| ⏰
a.js-end-date
span
| {{_ 'card-end'}}
unless getVoteEnd
i.fa.fa-plus
|
if getVoteEnd
+voteEndDate
@ -982,12 +999,12 @@ template(name="cardStartPlanningPokerPopup")
.materialCheckBox#poker-allow-non-members(name="poker-allow-non-members" class="{{#if pokerAllowNonBoardMembers}}is-checked{{/if}}")
span {{_ 'allowNonBoardMembers'}}
.check-div.flex
i.fa.fa-hourglass-end
| ⏰
a.js-end-date
span
| {{_ 'card-end'}}
unless getPokerEnd
i.fa.fa-plus
|
if getPokerEnd
+pokerEndDate

View file

@ -1,7 +1,26 @@
import { ReactiveCache } from '/imports/reactiveCache';
import moment from 'moment/min/moment-with-locales';
import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker';
import {
formatDateTime,
formatDate,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar
} from '/imports/lib/dateUtils';
import Cards from '/models/cards';
import Boards from '/models/boards';
import Checklists from '/models/checklists';
@ -287,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();
@ -407,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 = moment().format('YYYY-MM-DD HH:mm');
this.data().setPokerEnd(now);
const now = new Date();
Meteor.call('cards.setPokerEnd', this.data()._id, now);
}
},
@ -464,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) {
@ -477,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);
}
}
},
},
@ -546,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);
@ -1074,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) {
@ -1106,8 +1133,8 @@ BlazeComponent.extendComponent({
// editVoteEndDatePopup
(class extends DatePicker {
onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
this.data().getVoteEnd() && this.date.set(moment(this.data().getVoteEnd()));
super.onCreated(formatDateTime(now()));
this.data().getVoteEnd() && this.date.set(new Date(this.data().getVoteEnd()));
}
events() {
return [
@ -1118,12 +1145,12 @@ BlazeComponent.extendComponent({
// if no time was given, init with 12:00
const time =
evt.target.time.value ||
moment(new Date().setHours(12, 0, 0)).format('LT');
formatTime(new Date().setHours(12, 0, 0));
const dateString = `${evt.target.date.value} ${time}`;
/*
const newDate = moment(dateString, 'L LT', true);
const newDate = parseDate(dateString, ['L LT'], true);
if (newDate.isValid()) {
// if active vote - store it
if (this.currentData().getVoteQuestion()) {
@ -1137,28 +1164,27 @@ BlazeComponent.extendComponent({
*/
// Try to parse different date formats of all languages.
// This code is same for vote and planning poker.
const usaDate = moment(dateString, 'L LT', true);
const euroAmDate = moment(dateString, 'DD.MM.YYYY LT', true);
const euro24hDate = moment(dateString, 'DD.MM.YYYY HH.mm', true);
const eurodotDate = moment(dateString, 'DD.MM.YYYY HH:mm', true);
const minusDate = moment(dateString, 'YYYY-MM-DD HH:mm', true);
const slashDate = moment(dateString, 'DD/MM/YYYY HH.mm', true);
const dotDate = moment(dateString, 'DD/MM/YYYY HH:mm', true);
const brezhonegDate = moment(dateString, 'DD/MM/YYYY h[e]mm A', true);
const hrvatskiDate = moment(dateString, 'DD. MM. YYYY H:mm', true);
const latviaDate = moment(dateString, 'YYYY.MM.DD. H:mm', true);
const nederlandsDate = moment(dateString, 'DD-MM-YYYY HH:mm', true);
// greekDate does not work: el Greek Ελληνικά ,
// it has date format DD/MM/YYYY h:mm MM like 20/06/2021 11:15 MM
// where MM is maybe some text like AM/PM ?
// Also some other languages that have non-ascii characters in dates
// do not work.
const greekDate = moment(dateString, 'DD/MM/YYYY h:mm A', true);
const macedonianDate = moment(dateString, 'D.MM.YYYY H:mm', true);
// Try to parse different date formats using native Date parsing
const formats = [
'YYYY-MM-DD HH:mm',
'MM/DD/YYYY HH:mm',
'DD.MM.YYYY HH:mm',
'DD/MM/YYYY HH:mm',
'DD-MM-YYYY HH:mm'
];
let parsedDate = null;
for (const format of formats) {
parsedDate = parseDate(dateString, [format], true);
if (parsedDate) break;
}
// Fallback to native Date parsing
if (!parsedDate) {
parsedDate = new Date(dateString);
}
if (usaDate.isValid()) {
if (isValidDate(parsedDate)) {
// if active poker - store it
if (this.currentData().getPokerQuestion()) {
this._storeDate(usaDate.toDate());
@ -1287,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'));
@ -1312,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) {
@ -1337,9 +1360,9 @@ BlazeComponent.extendComponent({
// editPokerEndDatePopup
(class extends DatePicker {
onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
super.onCreated(formatDateTime(now()));
this.data().getPokerEnd() &&
this.date.set(moment(this.data().getPokerEnd()));
this.date.set(new Date(this.data().getPokerEnd()));
}
/*
@ -1357,7 +1380,7 @@ BlazeComponent.extendComponent({
return moment.localeData().longDateFormat('LT');
}
const newDate = moment(dateString, dateformat() + ' ' + timeformat(), true);
const newDate = parseDate(dateString, [dateformat() + ' ' + timeformat()], true);
*/
events() {
@ -1369,7 +1392,7 @@ BlazeComponent.extendComponent({
// if no time was given, init with 12:00
const time =
evt.target.time.value ||
moment(new Date().setHours(12, 0, 0)).format('LT');
formatTime(new Date().setHours(12, 0, 0));
const dateString = `${evt.target.date.value} ${time}`;
@ -1380,7 +1403,7 @@ BlazeComponent.extendComponent({
Maybe client/components/lib/datepicker.jade could have hidden input field for
datepicker format that could be used to detect date format?
const newDate = moment(dateString, dateformat() + ' ' + timeformat(), true);
const newDate = parseDate(dateString, [dateformat() + ' ' + timeformat()], true);
if (newDate.isValid()) {
// if active poker - store it
@ -1393,28 +1416,27 @@ BlazeComponent.extendComponent({
}
*/
// Try to parse different date formats of all languages.
// This code is same for vote and planning poker.
const usaDate = moment(dateString, 'L LT', true);
const euroAmDate = moment(dateString, 'DD.MM.YYYY LT', true);
const euro24hDate = moment(dateString, 'DD.MM.YYYY HH.mm', true);
const eurodotDate = moment(dateString, 'DD.MM.YYYY HH:mm', true);
const minusDate = moment(dateString, 'YYYY-MM-DD HH:mm', true);
const slashDate = moment(dateString, 'DD/MM/YYYY HH.mm', true);
const dotDate = moment(dateString, 'DD/MM/YYYY HH:mm', true);
const brezhonegDate = moment(dateString, 'DD/MM/YYYY h[e]mm A', true);
const hrvatskiDate = moment(dateString, 'DD. MM. YYYY H:mm', true);
const latviaDate = moment(dateString, 'YYYY.MM.DD. H:mm', true);
const nederlandsDate = moment(dateString, 'DD-MM-YYYY HH:mm', true);
// greekDate does not work: el Greek Ελληνικά ,
// it has date format DD/MM/YYYY h:mm MM like 20/06/2021 11:15 MM
// where MM is maybe some text like AM/PM ?
// Also some other languages that have non-ascii characters in dates
// do not work.
const greekDate = moment(dateString, 'DD/MM/YYYY h:mm A', true);
const macedonianDate = moment(dateString, 'D.MM.YYYY H:mm', true);
// Try to parse different date formats using native Date parsing
const formats = [
'YYYY-MM-DD HH:mm',
'MM/DD/YYYY HH:mm',
'DD.MM.YYYY HH:mm',
'DD/MM/YYYY HH:mm',
'DD-MM-YYYY HH:mm'
];
let parsedDate = null;
for (const format of formats) {
parsedDate = parseDate(dateString, [format], true);
if (parsedDate) break;
}
// Fallback to native Date parsing
if (!parsedDate) {
parsedDate = new Date(dateString);
}
if (usaDate.isValid()) {
if (isValidDate(parsedDate)) {
// if active poker - store it
if (this.currentData().getPokerQuestion()) {
this._storeDate(usaDate.toDate());
@ -1544,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

@ -15,8 +15,8 @@ template(name="editCardSpentTime")
template(name="timeBadge")
if canModifyCard
a.js-edit-time.card-time(title="{{showTitle}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
| {{showTime}}
a.js-edit-time.card-time(title="{{_ 'time'}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
| ⏱️ {{showTime}}
else
a.card-time(title="{{showTitle}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
| {{showTime}}
a.card-time(title="{{_ 'time'}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
| ⏱️ {{showTime}}

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

@ -1,14 +1,14 @@
template(name="checklists")
.checklists-title
h3.card-details-item-title
i.fa.fa-check
| ✅
| {{_ 'checklists'}}
if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId position="top")
+addChecklistItemForm
else
a.add-checklist-top.js-open-inlined-form(title="{{_ 'add-checklist'}}")
i.fa.fa-plus
|
if currentUser.isBoardMember
.material-toggle-switch(title="{{_ 'hide-finished-checklist'}}")
//span.toggle-switch-title
@ -28,7 +28,7 @@ template(name="checklists")
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=false)
else
a.add-checklist.js-open-inlined-form(title="{{_ 'add-checklist'}}")
i.fa.fa-plus
|
template(name="checklistDetail")
.js-checklist.checklist.nodragscroll
@ -38,7 +38,7 @@ template(name="checklistDetail")
.checklist-title
span
if canModifyCard
a.fa.fa-navicon.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
a.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
if canModifyCard
h4.title.js-open-inlined-form.is-editable
@ -63,12 +63,13 @@ template(name="checklistDeletePopup")
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
template(name="addChecklistItemForm")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
a(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
textarea.js-add-checklist-item(rows='1' autofocus)
.edit-controls.clearfix
button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
a.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
| ❌
if showNewlineBecomesNewChecklistItem
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItem'}}")
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItem")
@ -81,7 +82,7 @@ template(name="addChecklistItemForm")
| {{_ 'originOrder'}}
template(name="editChecklistItemForm")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
a(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
textarea.js-edit-checklist-item(rows='1' autofocus dir="auto")
if $eq type 'item'
@ -90,12 +91,13 @@ template(name="editChecklistItemForm")
= checklist.title
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
a.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
| ❌
span(title=createdAt) {{ moment createdAt }}
if canModifyCard
a.js-delete-checklist-item {{_ "delete"}}...
a.js-convert-checklist-item-to-card
i.fa.fa-copy
| 📋
| {{_ 'convertChecklistItemToCardPopup-title'}}
template(name="checklistItems")
@ -105,7 +107,7 @@ template(name="checklistItems")
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true position="top")
else
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
i.fa.fa-plus
|
.checklist-items.js-checklist-items
each item in checklist.items
+inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
@ -117,7 +119,7 @@ template(name="checklistItems")
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true)
else
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
i.fa.fa-plus
|
template(name='checklistItemDetail')
.js-checklist-item.checklist-item(class="{{#if item.isFinished }}is-checked{{#if checklist.hideCheckedChecklistItems}} invisible{{/if}}{{/if}}{{#if checklist.hideAllChecklistItems}} is-checked invisible{{/if}}"
@ -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
@ -140,16 +141,16 @@ template(name="checklistActionsPopup")
ul.pop-over-list
li
a.js-delete-checklist.delete-checklist
i.fa.fa-trash
| 🗑️
| {{_ "delete"}} ...
a.js-move-checklist.move-checklist
i.fa.fa-arrow-right
| ➡️
| {{_ "moveChecklist"}} ...
a.js-copy-checklist.copy-checklist
i.fa.fa-copy
| 📋
| {{_ "copyChecklist"}} ...
a.js-hide-checked-checklist-items
i.fa.fa-eye-slash
| 🙈
| {{_ "hideCheckedChecklistItems"}} ...
.material-toggle-switch(title="{{_ 'hide-checked-items'}}")
if checklist.hideCheckedChecklistItems
@ -158,7 +159,7 @@ template(name="checklistActionsPopup")
input.toggle-switch(type="checkbox" id="toggleHideCheckedChecklistItems_{{checklist._id}}")
label.toggle-label(for="toggleHideCheckedChecklistItems_{{checklist._id}}")
a.js-hide-all-checklist-items
i.fa.fa-ban
| 🚫
| {{_ "hideAllChecklistItems"}} ...
.material-toggle-switch(title="{{_ 'hideAllChecklistItems'}}")
if checklist.hideAllChecklistItems

View file

@ -38,6 +38,7 @@
.palette-colors {
display: flex;
flex-wrap: wrap;
justify-content: flex-start; /* left-align color chips in wider popovers */
}
.palette-colors .palette-color {
flex-grow: 1;

View file

@ -6,7 +6,7 @@ template(name="formLabel")
.palette-colors: each labels
span.card-label.palette-color.js-palette-color(class="card-label-{{color}}")
if(isSelected color)
i.fa.fa-check
| ✅
template(name="createLabelPopup")
form.create-label
@ -28,7 +28,8 @@ template(name="cardLabelsPopup")
ul.edit-labels-pop-over
each board.labels
li.js-card-label-item
a.card-label-edit-button.fa.fa-pencil.js-edit-label
a.card-label-edit-button.js-edit-label
| ✏️
if isTouchScreenOrShowDesktopDragHandles
span.fa.label-handle(class="fa-arrows" title="{{_ 'dragLabel'}}")
span.card-label.card-label-selectable.js-select-label.card-label-wrapper(class="card-label-{{color}}"
@ -36,5 +37,5 @@ template(name="cardLabelsPopup")
+viewer
= name
if(isLabelSelected ../_id)
i.card-label-selectable-icon.fa.fa-check
| ✅
a.quiet-button.full.js-add-label {{_ 'label-create'}}

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;
@ -168,6 +168,148 @@
.minicard .date {
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;
font-size: 0.9em;
vertical-align: middle;
}
/* Font Awesome icons in minicard spent time */
.minicard .card-time i.fa {
margin-right: 0.3vw;
font-size: 0.9em;
vertical-align: middle;
}
.minicard .badges {
float: left;
margin-top: 1vh;
@ -220,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,
@ -229,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.fa.fa-navicon.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
.handle
.fa.fa-arrows
else
a.fa.fa-navicon.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
@ -36,7 +31,7 @@ template(name="minicard")
if hasActiveUploads
.minicard-upload-progress
.upload-progress-header
i.fa.fa-upload
| 📤
span {{_ 'uploading-files'}} ({{uploadCount}})
each uploads
.upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
@ -45,11 +40,11 @@ template(name="minicard")
.upload-progress-fill(style="width: {{progress}}%")
if $eq status 'error'
.upload-progress-error
i.fa.fa-exclamation-triangle
| ⚠️
span {{_ 'upload-failed'}}
else if $eq status 'completed'
.upload-progress-success
i.fa.fa-check
| ✅
span {{_ 'upload-completed'}}
.minicard-title
@ -61,12 +56,12 @@ template(name="minicard")
| {{ parentCardName }}
if isLinkedBoard
a.js-linked-link
span.linked-icon.fa.fa-folder
span.linked-icon | 📁
else if isLinkedCard
a.js-linked-link
span.linked-icon.fa.fa-id-card
span.linked-icon | 🃏
if getArchived
span.linked-icon.linked-archived.fa.fa-archive
span.linked-icon.linked-archived | 📦
+viewer
if currentBoard.allowsCardNumber
span.card-number
@ -147,7 +142,7 @@ template(name="minicard")
if canModifyCard
if comments.length
.badge(title="{{_ 'card-comments-title' comments.length }}")
span.badge-icon.fa.fa-comment-o.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.fa.fa-align-left
span.badge-icon 📝
if getVoteQuestion
.badge.badge-state-image-only(title=getVoteQuestion)
span.badge-icon.fa.fa-thumbs-up(class="{{#if voteState}}text-green{{/if}}")
span.badge-icon(class="{{#if voteState}}text-green{{/if}}") 👍
span.badge-text {{ voteCountPositive }}
span.badge-icon.fa.fa-thumbs-down(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.fa.fa-check(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.fa.fa-paperclip
span.badge-icon 📎
span.badge-text= attachments.length
if checklists.length
.badge(class="{{#if checklistFinished}}is-finished{{/if}}")
span.badge-icon.fa.fa-check-square-o
span.badge-icon ☑️
span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
if allSubtasks.count
.badge
span.badge-icon.fa.fa-sitemap
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.fa.fa-sort
span.badge-icon 🔢
span.badge-text.check-list-sort {{ sort }}
if currentBoard.allowsDescriptionTextOnMinicard
if getDescription
@ -193,7 +188,7 @@ template(name="minicard")
| {{ getDescription }}
if shouldShowListOnMinicard
.minicard-list-name
i.fa.fa-list
| 📋
| {{ listName }}
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
.parent-subtext
@ -212,50 +207,50 @@ template(name="minicardDetailsActionsPopup")
if canModifyCard
li
a.js-move-card
i.fa.fa-arrow-right
| ➡️
| {{_ 'moveCardPopup-title'}}
li
a.js-copy-card
i.fa.fa-copy
| 📋
| {{_ 'copyCardPopup-title'}}
hr
li
a.js-archive
i.fa.fa-arrow-right
i.fa.fa-archive
| ➡️
| 📦
| {{_ 'archive-card'}}
hr
li
a.js-move-card-to-top
i.fa.fa-arrow-up
| ⬆️
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
i.fa.fa-arrow-down
| ⬇️
| {{_ 'moveCardToBottom-title'}}
hr
li
a.js-add-labels
i.fa.fa-tags
| 🏷️
| {{_ 'card-edit-labels'}}
li
a.js-due-date
i.fa.fa-sign-in
| 📥
| {{_ 'editCardDueDatePopup-title'}}
li
a.js-set-card-color
i.fa.fa-paint-brush
| 🎨
| {{_ 'setCardColorPopup-title'}}
li
a.js-link
i.fa.fa-link
| 🔗
| {{_ 'link-card'}}
li
a.js-toggle-watch-card
if isWatching
i.fa.fa-eye
| 👁️
| {{_ 'unwatch'}}
else
i.fa.fa-eye-slash
| 👁️-slash
| {{_ 'watch'}}

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);
}
}
},
}
@ -206,7 +209,9 @@ Template.minicard.helpers({
// Show list name if either:
// 1. Board-wide setting is enabled, OR
// 2. This specific card has the setting enabled
return this.currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
const currentBoard = this.currentBoard;
if (!currentBoard) return false;
return currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
}
});

View file

@ -13,7 +13,7 @@ template(name="resultCard")
.broken-cards-null
| NULL
if getBoard.archived
i.fa.fa-archive
| 📦
li.result-card-context.result-card-context-separator
= ' '
| {{_ 'context-separator'}}
@ -27,7 +27,7 @@ template(name="resultCard")
.broken-cards-null
| NULL
if getSwimlane.archived
i.fa.fa-archive
| 📦
li.result-card-context.result-card-context-separator
= ' '
| {{_ 'context-separator'}}
@ -41,4 +41,4 @@ template(name="resultCard")
.broken-cards-null
| NULL
if getList.archived
i.fa.fa-archive
| 📦

View file

@ -1,6 +1,6 @@
template(name="subtasks")
h3.card-details-item-title
i.fa.fa-sitemap
| 🌐
| {{_ 'subtasks'}}
if currentUser.isBoardAdmin
if toggleDeleteDialog.get
@ -16,7 +16,7 @@ template(name="subtasks")
+addSubtaskItemForm
else
a.js-open-inlined-form(title="{{_ 'add-subtask'}}")
i.fa.fa-plus
|
template(name="subtaskDetail")
.js-subtasks.subtask
@ -26,7 +26,7 @@ template(name="subtaskDetail")
.subtask-title
span
if canModifyCard
a.fa.fa-navicon.subtask-details-menu.js-open-subtask-details-menu(title="{{_ 'subtaskActionsPopup-title'}}")
a.subtask-details-menu.js-open-subtask-details-menu(title="{{_ 'subtaskActionsPopup-title'}}")
if canModifyCard
h2.title.js-open-inlined-form.is-editable
+viewer
@ -40,7 +40,7 @@ template(name="addSubtaskItemForm")
textarea.js-add-subtask-item(rows='1' autofocus dir="auto")
.edit-controls.clearfix
button.primary.confirm.js-submit-add-subtask-item-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
template(name="editSubtaskItemForm")
textarea.js-edit-subtask-item(rows='1' autofocus dir="auto")
@ -50,7 +50,7 @@ template(name="editSubtaskItemForm")
= subtask.title
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-subtask-item-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
span(title=createdAt) {{ moment createdAt }}
if canModifyCard
if currentUser.isBoardAdmin
@ -68,7 +68,7 @@ template(name="subtasksItems")
+addSubtaskItemForm
else
a.add-subtask-item.js-open-inlined-form
i.fa.fa-plus
|
| {{_ 'add-subtask-item'}}...
template(name='subtaskItemDetail')
@ -92,10 +92,10 @@ template(name="subtaskActionsPopup")
ul.pop-over-list
li
a.js-view-subtask(title="{{ subtask.title }}")
i.fa.fa-eye
| 👁️
| {{_ "view-it"}}
if currentUser.isBoardAdmin
a.js-delete-subtask.delete-subtask
i.fa.fa-trash
| 🗑️
| {{_ "delete"}} ...

View file

@ -0,0 +1,123 @@
/* Original Position Component Styles */
.original-position-info {
margin: 5px 0;
padding: 8px;
border-radius: 4px;
font-size: 12px;
line-height: 1.4;
}
.original-position-loading {
color: #666;
font-style: italic;
}
.original-position-loading i {
margin-right: 5px;
}
.original-position-details {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 3px;
padding: 6px 8px;
}
.original-position-moved {
color: #856404;
background-color: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 3px;
padding: 4px 6px;
margin-bottom: 4px;
}
.original-position-moved i {
color: #f39c12;
margin-right: 5px;
}
.original-position-unchanged {
color: #155724;
background-color: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 3px;
padding: 4px 6px;
margin-bottom: 4px;
}
.original-position-unchanged i {
color: #28a745;
margin-right: 5px;
}
.original-position-text {
font-weight: 500;
}
.original-title {
color: #6c757d;
font-size: 11px;
margin-top: 4px;
padding-top: 4px;
border-top: 1px solid #e9ecef;
}
.original-title strong {
color: #495057;
}
/* Integration with existing Wekan styles */
.swimlane .original-position-info,
.list .original-position-info,
.card .original-position-info {
margin: 2px 0;
padding: 4px 6px;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.original-position-info {
font-size: 11px;
padding: 6px;
}
.original-position-details {
padding: 4px 6px;
}
.original-position-moved,
.original-position-unchanged {
padding: 3px 5px;
}
}
/* Dark theme support */
@media (prefers-color-scheme: dark) {
.original-position-details {
background-color: #2d3748;
border-color: #4a5568;
color: #e2e8f0;
}
.original-position-moved {
background-color: #744210;
border-color: #b7791f;
color: #fbd38d;
}
.original-position-unchanged {
background-color: #22543d;
border-color: #38a169;
color: #9ae6b4;
}
.original-title {
color: #a0aec0;
border-color: #4a5568;
}
.original-title strong {
color: #e2e8f0;
}
}

View file

@ -0,0 +1,29 @@
<template name="originalPosition">
<div class="original-position-info">
{{#if isLoading}}
<div class="original-position-loading">
<i class="fa fa-spinner fa-spin"></i> Loading original position...
</div>
{{else if showOriginalPosition}}
<div class="original-position-details">
{{#if hasMovedFromOriginal}}
<div class="original-position-moved">
<i class="fa fa-info-circle"></i>
<span class="original-position-text">{{getOriginalPositionDescription}}</span>
</div>
{{else}}
<div class="original-position-unchanged">
<i class="fa fa-check-circle"></i>
<span class="original-position-text">In original position</span>
</div>
{{/if}}
{{#if getOriginalTitle}}
<div class="original-title">
<strong>Original title:</strong> {{getOriginalTitle}}
</div>
{{/if}}
</div>
{{/if}}
</div>
</template>

View file

@ -0,0 +1,98 @@
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
import { ReactiveVar } from 'meteor/reactive-var';
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import './originalPosition.html';
/**
* Component to display original position information for swimlanes, lists, and cards
*/
class OriginalPositionComponent extends BlazeComponent {
onCreated() {
super.onCreated();
this.originalPosition = new ReactiveVar(null);
this.isLoading = new ReactiveVar(false);
this.hasMoved = new ReactiveVar(false);
this.autorun(() => {
const data = this.data();
if (data && data.entityId && data.entityType) {
this.loadOriginalPosition(data.entityId, data.entityType);
}
});
}
loadOriginalPosition(entityId, entityType) {
this.isLoading.set(true);
const methodName = `positionHistory.get${entityType.charAt(0).toUpperCase() + entityType.slice(1)}OriginalPosition`;
Meteor.call(methodName, entityId, (error, result) => {
this.isLoading.set(false);
if (error) {
console.error('Error loading original position:', error);
this.originalPosition.set(null);
} else {
this.originalPosition.set(result);
// Check if the entity has moved
const movedMethodName = `positionHistory.has${entityType.charAt(0).toUpperCase() + entityType.slice(1)}Moved`;
Meteor.call(movedMethodName, entityId, (movedError, movedResult) => {
if (!movedError) {
this.hasMoved.set(movedResult);
}
});
}
});
}
getOriginalPosition() {
return this.originalPosition.get();
}
isLoading() {
return this.isLoading.get();
}
hasMovedFromOriginal() {
return this.hasMoved.get();
}
getOriginalPositionDescription() {
const position = this.getOriginalPosition();
if (!position) return 'No original position data';
if (position.originalPosition) {
const entityType = this.data().entityType;
let description = `Original position: ${position.originalPosition.sort || 0}`;
if (entityType === 'list' && position.originalSwimlaneId) {
description += ` in swimlane ${position.originalSwimlaneId}`;
} else if (entityType === 'card') {
if (position.originalSwimlaneId) {
description += ` in swimlane ${position.originalSwimlaneId}`;
}
if (position.originalListId) {
description += ` in list ${position.originalListId}`;
}
}
return description;
}
return 'No original position data';
}
getOriginalTitle() {
const position = this.getOriginalPosition();
return position ? position.originalTitle : '';
}
showOriginalPosition() {
return this.getOriginalPosition() !== null;
}
}
OriginalPositionComponent.register('originalPosition');
export default OriginalPositionComponent;

View file

@ -4,11 +4,10 @@ template(name="datepicker")
.fields
.left
label(for="date") {{_ 'date'}}
input.js-date-field#date(type="text" name="date" value=showDate placeholder=dateFormat autofocus)
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
.right
label(for="time") {{_ 'time'}}
input.js-time-field#time(type="text" name="time" value=showTime placeholder=timeFormat)
.js-datepicker
input.js-time-field#time(type="time" name="time" value=showTime)
if error.get
.warning {{_ error.get}}
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}

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

@ -8,6 +8,228 @@
padding: 0;
float: left;
}
/* List resize handle */
.list-resize-handle {
position: absolute;
top: 0;
right: -3px;
width: 6px;
height: 100%;
cursor: col-resize;
z-index: 10;
background: transparent;
transition: background-color 0.2s ease;
border-radius: 2px;
/* Ensure the handle is clickable */
pointer-events: auto;
}
.list-resize-handle:hover {
background: rgba(0, 123, 255, 0.4);
box-shadow: 0 0 4px rgba(0, 123, 255, 0.3);
}
.list-resize-handle:active {
background: rgba(0, 123, 255, 0.6);
box-shadow: 0 0 6px rgba(0, 123, 255, 0.4);
}
/* Show resize handle only on hover */
.list:hover .list-resize-handle {
background: rgba(0, 0, 0, 0.1);
}
.list:hover .list-resize-handle:hover {
background: rgba(0, 123, 255, 0.4);
box-shadow: 0 0 4px rgba(0, 123, 255, 0.3);
}
/* Add a subtle indicator line */
.list-resize-handle::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2px;
height: 20px;
background: rgba(0, 0, 0, 0.2);
border-radius: 1px;
opacity: 0;
transition: opacity 0.2s ease;
}
.list-resize-handle:hover::before {
opacity: 1;
}
/* Disable resize handle for collapsed lists and mobile view */
.list.list-collapsed .list-resize-handle,
.list.mobile-view .list-resize-handle {
display: none;
}
/* Disable resize handle for auto-width lists */
.list.list-auto-width .list-resize-handle {
display: none;
}
/* Visual feedback during resize */
.list.list-resizing {
transition: none !important;
box-shadow: 0 0 10px rgba(0, 123, 255, 0.3);
/* Ensure the list maintains its new width during resize */
flex: none !important;
flex-basis: auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
/* Override any conflicting layout properties */
float: left !important;
display: block !important;
position: relative !important;
/* Force width to be respected */
width: var(--list-width, auto) !important;
min-width: var(--list-width, auto) !important;
max-width: var(--list-width, auto) !important;
/* Ensure the width is applied immediately */
overflow: visible !important;
}
body.list-resizing-active {
cursor: col-resize !important;
}
body.list-resizing-active * {
cursor: col-resize !important;
}
/* Ensure swimlane container doesn't interfere with list resizing */
.swimlane .list.list-resizing {
/* Override any swimlane flex properties */
flex: none !important;
flex-basis: auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
/* Ensure width is respected */
width: var(--list-width, auto) !important;
min-width: var(--list-width, auto) !important;
max-width: var(--list-width, auto) !important;
}
/* More aggressive override for any container that might interfere */
.js-swimlane .list.list-resizing,
.dragscroll .list.list-resizing,
[id^="swimlane-"] .list.list-resizing {
/* Force the width to be applied */
width: var(--list-width, auto) !important;
min-width: var(--list-width, auto) !important;
max-width: var(--list-width, auto) !important;
flex: none !important;
flex-basis: auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
float: left !important;
display: block !important;
}
/* Ensure the width persists after resize is complete */
.js-swimlane .list[style*="--list-width"],
.dragscroll .list[style*="--list-width"],
[id^="swimlane-"] .list[style*="--list-width"] {
/* Maintain the width after resize */
width: var(--list-width, auto) !important;
min-width: var(--list-width, auto) !important;
max-width: var(--list-width, auto) !important;
flex: none !important;
flex-basis: auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
float: left !important;
display: block !important;
}
/* Ensure consistent header height for all lists */
.list-header {
/* Maintain consistent height and padding for all lists */
min-height: 2.5vh !important;
height: auto !important;
padding: 2.5vh 1.5vw 0.5vh !important;
/* Make sure the background covers the full height */
background-color: #e4e4e4 !important;
border-bottom: 0.8vh solid #e4e4e4 !important;
/* Use original display for consistent button positioning */
display: block !important;
position: relative !important;
/* Prevent vertical expansion but allow normal height */
overflow: hidden !important;
}
/* Ensure title text doesn't cause height changes for all lists */
.list-header .list-header-name {
/* Prevent text wrapping to maintain consistent height */
white-space: nowrap !important;
/* Truncate text with ellipsis if too long */
text-overflow: ellipsis !important;
/* Ensure proper line height */
line-height: 1.2 !important;
/* Ensure it doesn't overflow */
overflow: hidden !important;
/* Add margin to prevent overlap with buttons */
margin-right: 120px !important;
}
/* Position drag handle at top-right corner for ALL lists */
.list-header .list-header-handle {
/* Position at top-right corner, aligned with title text top */
position: absolute !important;
top: 2.5vh !important;
right: 1.5vw !important;
/* Ensure it's above other elements */
z-index: 15 !important;
/* Remove margin since it's absolutely positioned */
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 */
.js-swimlane .list[style*="--list-width"] .list-header .list-header-plus-top,
.js-swimlane .list[style*="--list-width"] .list-header .js-collapse,
.js-swimlane .list[style*="--list-width"] .list-header .js-open-list-menu,
.dragscroll .list[style*="--list-width"] .list-header .list-header-plus-top,
.dragscroll .list[style*="--list-width"] .list-header .js-collapse,
.dragscroll .list[style*="--list-width"] .list-header .js-open-list-menu,
[id^="swimlane-"] .list[style*="--list-width"] .list-header .list-header-plus-top,
[id^="swimlane-"] .list[style*="--list-width"] .list-header .js-collapse,
[id^="swimlane-"] .list[style*="--list-width"] .list-header .js-open-list-menu {
/* Use original positioning to maintain layout */
position: relative !important;
/* Maintain original spacing */
margin-right: 15px !important;
/* Ensure proper display */
display: inline-block !important;
}
/* Ensure watch icon and card count maintain original positioning */
.js-swimlane .list[style*="--list-width"] .list-header .list-header-watch-icon,
.dragscroll .list[style*="--list-width"] .list-header .list-header-watch-icon,
[id^="swimlane-"] .list[style*="--list-width"] .list-header .list-header-watch-icon,
.js-swimlane .list[style*="--list-width"] .list-header .cardCount,
.dragscroll .list[style*="--list-width"] .list-header .cardCount,
[id^="swimlane-"] .list[style*="--list-width"] .list-header .cardCount {
/* Use original positioning to maintain layout */
position: relative !important;
/* Maintain original spacing */
margin-right: 15px !important;
/* Ensure proper display */
display: inline-block !important;
}
[id^="swimlane-"] .list:first-child {
min-width: 2.5vw;
}
@ -37,7 +259,70 @@
}
.list.list-collapsed {
flex: none;
min-width: 60px;
max-width: 80px;
width: 60px;
min-height: 60vh;
height: 60vh;
overflow: visible;
position: relative;
}
.list.list-collapsed .list-header {
padding: 1vh 1.5vw 0.5vh;
min-height: 2.5vh !important;
height: auto !important;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
position: relative;
overflow: visible !important;
width: 100%;
max-width: 60px;
margin: 0 auto;
}
.list.list-collapsed .list-header .js-collapse {
margin: 0 auto 20px auto;
z-index: 10;
padding: 8px 12px;
font-size: 12px;
white-space: nowrap;
display: block;
width: fit-content;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
height: auto !important;
margin: 20px 0 0 0 !important;
position: relative !important;
overflow: visible !important;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
text-align: left;
overflow: visible;
white-space: nowrap;
display: block !important;
font-size: 12px;
line-height: 1.2;
color: #333;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #ddd;
padding: 8px 4px;
border-radius: 4px;
margin: 0 auto;
width: 25vh;
height: 60vh;
position: absolute;
left: 50%;
top: 50%;
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
z-index: 10;
visibility: visible !important;
opacity: 1 !important;
pointer-events: none;
}
.list.list-composer .open-list-composer,
.list .list-composer .open-list-composer {
color: #8c8c8c;
@ -93,9 +378,6 @@
position: relative;
text-overflow: ellipsis;
white-space: nowrap;
}
.list-header .list-rotated {
}
.list-header .list-header-watch-icon {
padding-left: 10px;
@ -121,11 +403,152 @@
color: #a6a6a6;
margin-right: 15px;
}
.list-header .list-header-uncollapse-left {
.list-header .js-collapse {
color: #a6a6a6;
margin-right: 15px;
display: inline-block;
vertical-align: middle;
padding: 5px 8px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f5f5f5;
cursor: pointer;
font-size: 14px;
}
.list-header .list-header-uncollapse-right {
color: #a6a6a6;
.list-header .js-collapse:hover {
background-color: #e0e0e0;
color: #333;
}
.list.list-collapsed .list-header .js-collapse {
display: inline-block !important;
visibility: visible !important;
opacity: 1 !important;
}
/* Responsive adjustments for collapsed lists */
@media (min-width: 768px) {
.list.list-collapsed {
min-width: 60px;
max-width: 80px;
width: 60px;
min-height: 60vh;
height: 60vh;
}
.list.list-collapsed .list-header {
max-width: 60px;
margin: 0 auto;
min-height: 2.5vh !important;
height: auto !important;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
height: auto !important;
margin: 20px 0 0 0 !important;
position: relative !important;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: 15vh;
font-size: 12px;
height: 30px;
line-height: 1.2;
padding: 8px 4px;
margin: 0 auto;
position: absolute;
left: 50%;
top: 50%;
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
text-align: left;
visibility: visible !important;
opacity: 1 !important;
display: block !important;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #ddd;
color: #333;
z-index: 10;
}
.list.list-collapsed .list-header .js-collapse {
margin: 0 auto 20px auto;
}
}
@media (min-width: 1024px) {
.list.list-collapsed {
min-height: 60vh;
height: 60vh;
}
.list.list-collapsed .list-header {
min-height: 2.5vh !important;
height: auto !important;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
height: auto !important;
margin: 20px 0 0 0 !important;
position: relative !important;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: 15vh;
font-size: 12px;
height: 30px;
line-height: 1.2;
padding: 8px 4px;
margin: 0 auto;
position: absolute;
left: 50%;
top: 50%;
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
text-align: left;
visibility: visible !important;
opacity: 1 !important;
display: block !important;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #ddd;
color: #333;
z-index: 10;
}
.list.list-collapsed .list-header .js-collapse {
margin: 0 auto 20px auto;
}
}
@media (min-width: 1200px) {
.list.list-collapsed {
min-height: 60vh;
height: 60vh;
}
.list.list-collapsed .list-header {
min-height: 2.5vh !important;
height: auto !important;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
height: auto !important;
margin: 20px 0 0 0 !important;
position: relative !important;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: 15vh;
font-size: 12px;
height: 30px;
line-height: 1.2;
padding: 8px 4px;
margin: 0 auto;
position: absolute;
left: 50%;
top: 50%;
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
text-align: left;
visibility: visible !important;
opacity: 1 !important;
display: block !important;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #ddd;
color: #333;
z-index: 10;
}
.list.list-collapsed .list-header .js-collapse {
margin: 0 auto 20px auto;
}
}
.list-header .list-header-collapse {
color: #a6a6a6;
@ -218,17 +641,22 @@
.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;
@ -236,9 +664,11 @@
.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;
@ -246,14 +676,17 @@
.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*/
@ -262,8 +695,9 @@
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;
@ -339,21 +773,27 @@
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;
@ -361,9 +801,11 @@
.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;
@ -371,14 +813,17 @@
.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*/
@ -387,8 +832,9 @@
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

@ -4,6 +4,7 @@ template(name='list')
class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}} {{#if isMiniScreen}}mobile-view{{/if}}")
+listHeader
+listBody
.list-resize-handle.js-list-resize-handle.nodragscroll
template(name='miniList')
a.mini-list.js-select-list.js-list(id="js-list-{{_id}}" class="{{#if isMiniScreen}}mobile-view{{/if}}")

View file

@ -24,6 +24,9 @@ BlazeComponent.extendComponent({
onRendered() {
const boardComponent = this.parentComponent().parentComponent();
// Initialize list resize functionality immediately
this.initializeListResize();
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
const $cards = this.$('.js-minicards');
@ -147,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',
@ -198,20 +197,259 @@ BlazeComponent.extendComponent({
listWidth() {
const user = ReactiveCache.getCurrentUser();
const list = Template.currentData();
return user.getListWidth(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();
return user.getListConstraint(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);
},
initializeListResize() {
// Check if we're still in a valid template context
if (!Template.currentData()) {
console.warn('No current template data available for list resize initialization');
return;
}
const list = Template.currentData();
const $list = this.$('.js-list');
const $resizeHandle = this.$('.js-list-resize-handle');
// Check if elements exist
if (!$list.length || !$resizeHandle.length) {
console.warn('List or resize handle not found, retrying in 100ms');
Meteor.setTimeout(() => {
if (!this.isDestroyed) {
this.initializeListResize();
}
}, 100);
return;
}
// Only enable resize for non-collapsed, non-auto-width lists
const isAutoWidth = this.autoWidth();
if (list.collapsed || isAutoWidth) {
$resizeHandle.hide();
return;
}
let isResizing = false;
let startX = 0;
let startWidth = 0;
let minWidth = 100; // Minimum width as defined in the existing code
let maxWidth = this.listConstraint() || 1000; // Use constraint as max width
let listConstraint = this.listConstraint(); // Store constraint value for use in event handlers
const component = this; // Store reference to component for use in event handlers
const startResize = (e) => {
isResizing = true;
startX = e.pageX || e.originalEvent.touches[0].pageX;
startWidth = $list.outerWidth();
// Add visual feedback
$list.addClass('list-resizing');
$('body').addClass('list-resizing-active');
// Prevent text selection during resize
$('body').css('user-select', 'none');
e.preventDefault();
e.stopPropagation();
};
const doResize = (e) => {
if (!isResizing) {
return;
}
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
const deltaX = currentX - startX;
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
// Apply the new width immediately for real-time feedback
$list[0].style.setProperty('--list-width', `${newWidth}px`);
$list[0].style.setProperty('width', `${newWidth}px`);
$list[0].style.setProperty('min-width', `${newWidth}px`);
$list[0].style.setProperty('max-width', `${newWidth}px`);
$list[0].style.setProperty('flex', 'none');
$list[0].style.setProperty('flex-basis', 'auto');
$list[0].style.setProperty('flex-grow', '0');
$list[0].style.setProperty('flex-shrink', '0');
e.preventDefault();
e.stopPropagation();
};
const stopResize = (e) => {
if (!isResizing) return;
isResizing = false;
// Calculate final width
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
const deltaX = currentX - startX;
const finalWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
// Ensure the final width is applied
$list[0].style.setProperty('--list-width', `${finalWidth}px`);
$list[0].style.setProperty('width', `${finalWidth}px`);
$list[0].style.setProperty('min-width', `${finalWidth}px`);
$list[0].style.setProperty('max-width', `${finalWidth}px`);
$list[0].style.setProperty('flex', 'none');
$list[0].style.setProperty('flex-basis', 'auto');
$list[0].style.setProperty('flex-grow', '0');
$list[0].style.setProperty('flex-shrink', '0');
// Remove visual feedback but keep the width
$list.removeClass('list-resizing');
$('body').removeClass('list-resizing-active');
$('body').css('user-select', '');
// Keep the CSS custom property for persistent width
// The CSS custom property will remain on the element to maintain the width
// Save the new width using the existing system
const boardId = list.boardId;
const listId = list._id;
// Use the new storage method that handles both logged-in and non-logged-in users
if (process.env.DEBUG === 'true') {
}
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();
};
// Mouse events
$resizeHandle.on('mousedown', startResize);
$(document).on('mousemove', doResize);
$(document).on('mouseup', stopResize);
// Touch events for mobile
$resizeHandle.on('touchstart', startResize, { passive: false });
$(document).on('touchmove', doResize, { passive: false });
$(document).on('touchend', stopResize, { passive: false });
// Prevent dragscroll interference
$resizeHandle.on('mousedown', (e) => {
e.stopPropagation();
});
// Reactively update resize handle visibility when auto-width changes
component.autorun(() => {
if (component.autoWidth()) {
$resizeHandle.hide();
} else {
$resizeHandle.show();
}
});
// Clean up on component destruction
component.onDestroyed(() => {
$(document).off('mousemove', doResize);
$(document).off('mouseup', stopResize);
$(document).off('touchmove', doResize);
$(document).off('touchend', stopResize);
});
},
}).register('list');
Template.miniList.events({

View file

@ -32,7 +32,7 @@ template(name="listBody")
+addCardForm(listId=_id position="bottom")
else
a.open-minicard-composer.js-card-composer.js-open-inlined-form(title="{{_ 'add-card-to-bottom-of-list'}}")
i.fa.fa-plus
|
template(name="spinnerList")
.sk-spinner.sk-spinner-list(
@ -54,7 +54,7 @@ template(name="addCardForm")
.add-controls.clearfix
button.primary.confirm(type="submit") {{_ 'add'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form | ❌
.add-controls.clearfix
unless currentBoard.isTemplatesBoard
unless currentBoard.isTemplateBoard

View file

@ -472,6 +472,14 @@ BlazeComponent.extendComponent({
if (!this.selectedBoardId.get()) {
return [];
}
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
if (!board) {
return [];
}
// Ensure default swimlane exists
board.getDefaultSwimline();
const swimlanes = ReactiveCache.getSwimlanes(
{
boardId: this.selectedBoardId.get()

View file

@ -7,12 +7,10 @@ template(name="listHeader")
else
if isMiniScreen
if currentList
a.list-header-left-icon.fa.fa-angle-left.js-unselect-list
a.list-header-left-icon.js-unselect-list
| ◀️
else
if collapsed
a.js-collapse(title="{{_ 'uncollapse'}}")
i.fa.fa-arrow-left.list-header-uncollapse-left
i.fa.fa-arrow-right.list-header-uncollapse-right
if showCardsCountForList cards.length
br
span.cardCount {{cardsCount}}
@ -29,6 +27,10 @@ template(name="listHeader")
if showCardsCountForList cards.length
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
else
if collapsed
a.js-collapse(title="{{_ 'uncollapse'}}")
| ⬅️
| ➡️
div(class="{{#if collapsed}}list-rotated{{/if}}")
h2.list-header-name(
title="{{ moment modifiedAt 'LLL' }}"
@ -45,94 +47,97 @@ template(name="listHeader")
if isMiniScreen
if currentList
if isWatching
i.list-header-watch-icon.fa.fa-eye
i.list-header-watch-icon | 👁️
div.list-header-menu
unless currentUser.isCommentOnly
if canSeeAddCard
a.js-add-card.fa.fa-plus.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
a.fa.fa-navicon.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.fa.fa-angle-right.js-select-list
a.list-header-handle.handle.fa.fa-arrows.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.fa.fa-eye
i.list-header-watch-icon | 👁️
unless collapsed
div.list-header-menu
unless currentUser.isCommentOnly
//if isBoardAdmin
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
if canSeeAddCard
a.js-add-card.fa.fa-plus.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'}}")
i.fa.fa-arrow-right.list-header-collapse-right
i.fa.fa-arrow-left.list-header-collapse-left
a.fa.fa-navicon.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
if currentUser.isBoardAdmin
if isTouchScreenOrShowDesktopDragHandles
a.list-header-handle.handle.fa.fa-arrows.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
input.list-name-input.full-line(type="text" value=title autofocus)
.edit-controls.clearfix
button.primary.confirm(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
| ❌
template(name="listActionPopup")
ul.pop-over-list
li
a.js-add-card.list-header-plus-bottom
i.fa.fa-plus
i.fa.fa-arrow-down
|
| ⬇️
| {{_ 'add-card-to-bottom-of-list'}}
hr
ul.pop-over-list
li
a.js-set-list-width
i.fa.fa-arrows-h
| ↔️
| {{_ 'set-list-width'}}
ul.pop-over-list
li
a.js-toggle-watch-list
if isWatching
i.fa.fa-eye
| 👁️
| {{_ 'unwatch'}}
else
i.fa.fa-eye-slash
| 🙈
| {{_ 'watch'}}
unless currentUser.isCommentOnly
unless currentUser.isWorker
ul.pop-over-list
li
a.js-set-color-list
i.fa.fa-paint-brush
| 🎨
| {{_ 'set-color-list'}}
ul.pop-over-list
if cards.length
li
a.js-select-cards
i.fa.fa-check-square
| ☑️
| {{_ 'list-select-cards'}}
if currentUser.isBoardAdmin
ul.pop-over-list
li
a.js-set-wip-limit
i.fa.fa-ban
| 🚫
| {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
unless currentUser.isWorker
hr
ul.pop-over-list
li
a.js-close-list
i.fa.fa-arrow-right
i.fa.fa-archive
| ➡️
| 📦
| {{_ 'archive-list'}}
hr
ul.pop-over-list
li
a.js-more
i.fa.fa-link
| 🔗
| {{_ 'listMorePopup-title'}}
template(name="boardLists")
@ -149,7 +154,7 @@ template(name="listMorePopup")
span.clearfix
span {{_ 'link-list'}}
= ' '
i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
| {{#if board.isPublic}}🌐{{else}}🔒{{/if}}
input.inline-input(type="text" readonly value="{{ rootUrl }}")
| {{_ 'added'}}
span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
@ -169,7 +174,7 @@ template(name="setWipLimitPopup")
ul.pop-over-list
li: a.js-enable-wip-limit {{_ 'enable-wip-limit'}}
if isWipLimitEnabled
i.fa.fa-check
| ✅
if isWipLimitEnabled
p
input.wip-limit-value(type="number" value="{{ wipLimitValue }}" min="1" max="99")
@ -197,7 +202,7 @@ template(name="setListWidthPopup")
br
a.js-auto-width-board(
title="{{#if isAutoWidth}}{{_ 'click-to-disable-auto-width'}}{{else}}{{_ 'click-to-enable-auto-width'}}{{/if}}")
i.fa(class="fa-solid fa-{{#if isAutoWidth}}compress{{else}}expand{{/if}}")
| {{#if isAutoWidth}}🗜️{{else}}📏{{/if}}
span {{_ 'auto-list-width'}}
template(name="listWidthErrorPopup")
@ -211,6 +216,6 @@ template(name="setListColorPopup")
// note: we use the swimlane palette to have more than just the border
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color)
i.fa.fa-check
| ✅
button.primary.confirm.js-submit {{_ 'save'}}
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}

View file

@ -0,0 +1,29 @@
template(name="bookmarks")
.panel
h2 {{_ 'bookmarks'}}
if currentUser
if hasStarredBoards
ul
each starredBoards
li
a(href="{{pathFor 'board' id=_id slug=slug}}")= title
a.js-toggle-star(title="{{_ 'star-board-short-unstar'}}")
| ⭐
else
p {{_ 'no-starred-boards'}}
else
p {{_ 'please-sign-in'}}
// Desktop popup
template(name="bookmarksPopup")
ul.pop-over-list
if hasStarredBoards
each starredBoards
li
a(href="{{pathFor 'board' id=_id slug=slug}}")
| ⭐
| #{title}
a.js-toggle-star.right(title="{{_ 'star-board-short-unstar'}}")
| ⭐
else
li {{_ 'no-starred-boards'}}

View file

@ -0,0 +1,55 @@
Template.bookmarks.helpers({
hasStarredBoards() {
const user = ReactiveCache.getCurrentUser();
if (!user) return false;
const { starredBoards = [] } = user.profile || {};
return Array.isArray(starredBoards) && starredBoards.length > 0;
},
starredBoards() {
const user = ReactiveCache.getCurrentUser();
if (!user) return [];
const { starredBoards = [] } = user.profile || {};
if (!Array.isArray(starredBoards) || starredBoards.length === 0) return [];
return Boards.find({ _id: { $in: starredBoards } }, { sort: { sort: 1 } });
},
});
Template.bookmarks.events({
'click .js-toggle-star'(e) {
e.preventDefault();
const boardId = this._id;
const user = ReactiveCache.getCurrentUser();
if (user && boardId) {
user.toggleBoardStar(boardId);
}
},
});
Template.bookmarksPopup.helpers({
hasStarredBoards() {
const user = ReactiveCache.getCurrentUser();
if (!user) return false;
const { starredBoards = [] } = user.profile || {};
return Array.isArray(starredBoards) && starredBoards.length > 0;
},
starredBoards() {
const user = ReactiveCache.getCurrentUser();
if (!user) return [];
const { starredBoards = [] } = user.profile || {};
if (!Array.isArray(starredBoards) || starredBoards.length === 0) return [];
return Boards.find({ _id: { $in: starredBoards } }, { sort: { sort: 1 } });
},
});
Template.bookmarksPopup.events({
'click .js-toggle-star'(e) {
e.preventDefault();
const boardId = this._id;
const user = ReactiveCache.getCurrentUser();
if (user && boardId) {
user.toggleBoardStar(boardId);
}
},
});

View file

@ -1,23 +1,23 @@
template(name="dueCardsHeaderBar")
if currentUser
h1
i.fa.fa-calendar
| 📅
| {{_ 'dueCards-title'}}
.board-header-btns.left
a.board-header-btn.js-due-cards-view-change(title="{{_ 'dueCardsViewChange-title'}}")
i.fa.fa-caret-down
| ▼
if $eq dueCardsView 'me'
i.fa.fa-user
| 👤
| {{_ 'dueCardsViewChange-choice-me'}}
if $eq dueCardsView 'all'
i.fa.fa-users
| 👥
| {{_ 'dueCardsViewChange-choice-all'}}
template(name="dueCardsModalTitle")
if currentUser
h2
i.fa.fa-keyboard-o
| ⌨️
| {{_ 'dueCards-title'}}
template(name="dueCards")
@ -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
@ -40,18 +49,18 @@ template(name="dueCardsViewChangePopup")
li
with "dueCardsViewChange-choice-me"
a.js-due-cards-view-me
i.fa.fa-user.colorful
| 👤
| {{_ 'dueCardsViewChange-choice-me'}}
if $eq Utils.dueCardsView "me"
i.fa.fa-check
| ✅
hr
li
with "dueCardsViewChange-choice-all"
a.js-due-cards-view-all
i.fa.fa-users.colorful
| 👥
| {{_ 'dueCardsViewChange-choice-all'}}
span.sub-name
+viewer
| {{_ 'dueCardsViewChange-choice-all-description' }}
if $eq Utils.dueCardsView "all"
i.fa.fa-check
| ✅

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);
// 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);
}
});
});
}
if (Utils.dueCardsView() !== 'all') {
queryParams.addPredicate(OPERATOR_USER, ReactiveCache.getCurrentUser().username);
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

@ -1,6 +1,8 @@
template(name="editor")
a.fa.fa-brands.fa-markdown(title="{{_ 'convert-to-markdown'}}")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
a(title="{{_ 'convert-to-markdown'}}")
| 📝
a(title="{{_ 'copy-text-to-clipboard'}}")
| 📋
span.copied-tooltip {{_ 'copied'}}
textarea.editor(
dir="auto"

View file

@ -1,20 +1,21 @@
template(name="globalSearchHeaderBar")
if currentUser
h1
i.fa.fa-search
| 🔍
| {{_ 'globalSearch-title'}}
template(name="globalSearchModalTitle")
if currentUser
h2
i.fa.fa-keyboard-o
| ⌨️
| {{_ 'globalSearch-title'}}
template(name="resultsPaged")
if resultsHeading.get
h1
= resultsHeading.get
a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}")
a(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}")
| 🔗
each card in results.get
+resultCard(card)
table.global-search-footer
@ -41,7 +42,8 @@ template(name="globalSearch")
value="{{ query.get }}"
autofocus dir="auto"
)
a.js-new-search.fa.fa-eraser
a.js-new-search
| 🧹
if debug.get.show
h1 Debug
if debug.get.showSelector

View file

@ -58,7 +58,7 @@
float: left;
overflow: hidden;
line-height: 28px;
margin: 0 2px;
margin: 0 12px;
}
#header #header-main-bar .board-header-btn i.fa {
float: left;
@ -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

@ -9,10 +9,10 @@ template(name="header")
// Home icon - always at left side of logo
span.home-icon.allBoards
a(href="{{pathFor 'home'}}")
span.fa.fa-home
| 🏠
| {{_ 'all-boards'}}
// Logo - always visible in desktop mode
// Logo - visible; on mobile constrained by CSS
unless currentSetting.hideLogo
if currentSetting.customTopLeftCornerLogoImageUrl
if currentSetting.customTopLeftCornerLogoLinkUrl
@ -80,14 +80,16 @@ template(name="header")
.mobile-mode-toggle
a.board-header-btn.js-mobile-mode-toggle(title="{{_ 'mobile-desktop-toggle'}}" class="{{#if mobileMode}}mobile-active{{else}}desktop-active{{/if}}")
i.fa.fa-mobile.mobile-icon(class="{{#if mobileMode}}active{{/if}}")
i.fa.fa-desktop.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}")
i.mobile-icon(class="{{#if mobileMode}}active{{/if}}") 📱
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}") 🖥️
// Notifications
+notifications
if currentSetting.customHelpLinkUrl
#header-help
a(href="{{currentSetting.customHelpLinkUrl}}", title="{{_ 'help'}}", target="_blank", rel="noopener noreferrer")
span.fa.fa-question
| ❓
+headerUserBar
@ -106,15 +108,15 @@ template(name="header")
if hasAnnouncement
.announcement
p
i.fa.fa-bullhorn
| 📢
+viewer
| #{announcement}
i.fa.fa-times-circle.js-close-announcement
| ❌
template(name="offlineWarning")
.offline-warning
p
i.fa.fa-warning
| ⚠️
| {{_ 'app-is-offline'}}
a.app-try-reconnect {{_ 'app-try-reconnect'}}

View file

@ -104,6 +104,9 @@ Template.header.events({
const currentMode = Utils.getMobileMode();
Utils.setMobileMode(!currentMode);
},
'click .js-open-bookmarks'(evt) {
// Already added but ensure single definition -- safe guard
},
'click .js-close-announcement'() {
$('.announcement').hide();
},
@ -124,6 +127,14 @@ Template.header.events({
location.reload();
}
},
'click .js-open-bookmarks'(evt) {
// Desktop: open popup, Mobile: route to page
if (Utils.isMiniScreen()) {
FlowRouter.go('bookmarks');
} else {
Popup.open('bookmarksPopup')(evt);
}
},
});
Template.offlineWarning.events({

View file

@ -1,12 +1,12 @@
template(name="shortcutsHeaderBar")
h1
a.back-btn(href="{{pathFor 'home'}}")
i.fa.fa-chevron-left
| ◀️
| {{_ 'keyboard-shortcuts'}}
template(name="shortcutsModalTitle")
h2
i.fa.fa-keyboard-o
| ⌨️
| {{_ 'keyboard-shortcuts'}}
template(name="keyboardShortcuts")

View file

@ -52,9 +52,15 @@ input,
select,
textarea,
button {
font: clamp(12px, 2.5vw, 16px) Roboto, Poppins, "Helvetica Neue", Arial, Helvetica, sans-serif;
line-height: 1.3;
font: clamp(14px, 2.5vw, 18px) Roboto, Poppins, "Helvetica Neue", Arial, Helvetica, sans-serif;
line-height: 1.4;
color: #4d4d4d;
/* Improve text rendering */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Better text selection */
-webkit-user-select: text;
user-select: text;
}
html {
font-size: 100%;
@ -460,20 +466,291 @@ a:not(.disabled).is-active i.fa {
.no-scrollbars::-webkit-scrollbar {
display: none !important;
}
@media screen and (max-width: 800px) {
/* ========================================
MOBILE & TABLET RESPONSIVE IMPROVEMENTS
======================================== */
/* 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);
/* Improve touch scrolling */
-webkit-overflow-scrolling: touch;
}
#content > .wrapper {
margin-top: 0px;
padding: 8px;
}
.wrapper {
height: calc(100% - 31px);
margin: 0px;
padding: 8px;
}
.panel-default {
width: 83vw;
width: 95vw;
max-width: 95vw;
margin: 0 auto;
}
/* Improve touch targets */
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 44px;
min-width: 44px;
padding: 12px 16px;
font-size: 16px; /* Prevent zoom on iOS */
touch-action: manipulation;
}
/* Form elements */
input, select, textarea {
font-size: 16px; /* Prevent zoom on iOS */
padding: 12px;
min-height: 44px;
touch-action: manipulation;
}
/* Cards and lists */
.minicard {
min-height: 48px;
padding: 12px;
margin-bottom: 8px;
touch-action: manipulation;
}
.list {
margin: 0 8px;
min-width: 280px;
}
/* Board canvas */
.board-canvas {
padding: 8px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* Header mobile layout */
#header {
padding: 8px;
/* Keep top bar on a single row on small screens */
flex-wrap: nowrap;
align-items: center;
gap: 8px;
}
#header-quick-access {
/* Keep quick-access items in one row */
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
width: 100%;
}
/* Hide elements that should move to the hamburger menu on mobile */
#header-quick-access .header-quick-access-list,
#header-quick-access #header-help {
display: none !important;
}
/* Show only the home icon (hide the trailing text) on mobile */
#header-quick-access .home-icon a {
display: inline-flex;
align-items: center;
max-width: 28px; /* enough to display the icon */
overflow: hidden;
white-space: nowrap;
}
/* Hide text in home icon on mobile, show only icon */
#header-quick-access .home-icon a span:not(.fa) {
display: none !important;
}
/* Ensure proper spacing for mobile header elements */
#header-quick-access .zoom-controls {
margin-left: auto;
margin-right: 8px;
}
.mobile-mode-toggle {
margin-right: 8px;
}
#header-user-bar {
margin-left: auto;
}
/* Ensure header elements don't wrap on very small screens */
#header-quick-access {
min-width: 0; /* Allow flexbox to shrink */
}
/* Make sure logo doesn't take too much space on mobile */
#header-quick-access img {
max-height: 24px;
max-width: 120px;
}
/* Ensure zoom controls are compact on mobile */
.zoom-controls .zoom-level {
padding: 4px 8px;
font-size: 12px;
}
/* Modal mobile optimization */
#modal .modal-content,
#modal .modal-content-wide {
width: 95vw;
max-width: 95vw;
margin: 2vh auto;
padding: 16px;
max-height: 90vh;
overflow-y: auto;
}
/* Table mobile optimization */
table {
font-size: 14px;
width: 100%;
display: block;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
/* Admin panel mobile optimization */
.setting-content .content-body {
flex-direction: column;
gap: 16px;
padding: 8px;
}
.setting-content .content-body .side-menu {
width: 100%;
order: 2;
}
.setting-content .content-body .main-body {
order: 1;
min-height: 60vh;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
/* Tablet devices (768px - 1024px) */
@media screen and (min-width: 768px) and (max-width: 1024px) {
#content > .wrapper {
padding: 12px;
}
.wrapper {
padding: 12px;
}
.panel-default {
width: 90vw;
max-width: 90vw;
}
/* Touch-friendly but more compact */
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 48px;
min-width: 48px;
padding: 10px 14px;
}
.minicard {
min-height: 40px;
padding: 10px;
}
.list {
margin: 0 12px;
min-width: 300px;
}
.board-canvas {
padding: 12px;
}
#header {
padding: 12px 16px;
}
#modal .modal-content {
width: 80vw;
max-width: 600px;
}
#modal .modal-content-wide {
width: 90vw;
max-width: 800px;
}
.setting-content .content-body {
gap: 20px;
}
.setting-content .content-body .side-menu {
width: 250px;
}
}
/* Large displays and digital signage (1920px+) */
@media screen and (min-width: 1920px) {
body {
font-size: 18px;
}
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 56px;
min-width: 56px;
padding: 16px 20px;
font-size: 18px;
}
.minicard {
min-height: 56px;
padding: 16px;
font-size: 18px;
}
.list {
margin: 0 8px;
min-width: 360px;
}
.board-canvas {
padding: 0;
}
#header {
padding: 0 8px;
}
#content > .wrapper {
padding: 0;
}
#modal .modal-content {
width: 600px;
}
#modal .modal-content-wide {
width: 1000px;
}
.setting-content .content-body {
gap: 32px;
}
.setting-content .content-body .side-menu {
width: 320px;
}
}
.inline-input {

View file

@ -2,7 +2,7 @@ template(name="main")
html(lang="{{TAPi18n.getLanguage}}")
head
title
meta(name="viewport" content="width=device-width, initial-scale=1")
meta(name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes")
meta(http-equiv="X-UA-Compatible" content="IE=edge")
//- XXX We should use pathFor in the following `href` to support the case
where the application is deployed with a path prefix, but it seems to be
@ -77,19 +77,21 @@ template(name="defaultLayout")
| {{{afterBodyStart}}}
+Template.dynamic(template=content)
| {{{beforeBodyEnd}}}
+migrationProgress
+boardConversionProgress
if (Modal.isOpen)
#modal
.overlay
if (Modal.isWide)
.modal-content-wide.modal-container
a.modal-close-btn.js-close-modal
i.fa.fa-times-thin
| ❌
+Template.dynamic(template=Modal.getHeaderName)
+Template.dynamic(template=Modal.getTemplateName)
else
.modal-content.modal-container
a.modal-close-btn.js-close-modal
i.fa.fa-times-thin
| ❌
+Template.dynamic(template=Modal.getHeaderName)
+Template.dynamic(template=Modal.getTemplateName)

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

@ -3,23 +3,23 @@ template(name="myCardsHeaderBar")
h1
//a.back-btn(href="{{pathFor 'home'}}")
// i.fa.fa-chevron-left
i.fa.fa-list
| 📋
| {{_ 'my-cards'}}
.board-header-btns.left
a.board-header-btn.js-my-cards-view-change(title="{{_ 'myCardsViewChange-title'}}")
i.fa.fa-caret-down
| ▼
if $eq myCardsView 'boards'
i.fa.fa-trello
| 📋
| {{_ 'myCardsViewChange-choice-boards'}}
if $eq myCardsView 'table'
i.fa.fa-table
| 📊
| {{_ 'myCardsViewChange-choice-table'}}
template(name="myCardsModalTitle")
if currentUser
h2
i.fa.fa-keyboard-o
| ⌨️
| {{_ 'my-cards'}}
template(name="myCards")
@ -102,15 +102,15 @@ template(name="myCardsViewChangePopup")
li
with "myCardsViewChange-choice-boards"
a.js-my-cards-view-boards
i.fa.fa-trello.colorful
| 📋
| {{_ 'myCardsViewChange-choice-boards'}}
if $eq Utils.myCardsView "boards"
i.fa.fa-check
| ✅
hr
li
with "myCardsViewChange-choice-table"
a.js-my-cards-view-table
i.fa.fa-table.colorful
| 📊
| {{_ 'myCardsViewChange-choice-table'}}
if $eq Utils.myCardsView "table"
i.fa.fa-check
| ✅

View file

@ -5,7 +5,8 @@
border-bottom-color: #c2c2c2;
box-shadow: 0 0.2vh 0.8vh rgba(0,0,0,0.3);
position: absolute;
width: min(300px, 40vw);
/* Wider default to fit full color palette */
width: min(380px, 55vw);
z-index: 99999;
margin-top: 0.7vh;
}
@ -72,23 +73,321 @@
}
.pop-over .content-wrapper {
width: 100%;
overflow: hidden;
max-height: calc(70vh + 20px);
overflow-y: auto;
overflow-x: hidden;
}
/* Allow dynamic max-height to override default constraint */
.pop-over[style*="max-height"] .content-wrapper {
max-height: inherit;
}
.pop-over .content-container {
width: 5000px;
max-height: 70vh;
width: 100%;
max-height: calc(70vh + 20px);
transition: transform 0.2s;
}
/* Allow dynamic max-height to override default constraint for content-container */
.pop-over[style*="max-height"] .content-container {
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;
overflow: visible;
}
/* Specific styling for language popup list */
.pop-over[data-popup="changeLanguage"] .pop-over-list {
max-height: none;
overflow: visible;
height: auto;
flex: 1;
}
/* Ensure content div in language popup contains all items */
.pop-over[data-popup="changeLanguage"] .content {
height: auto;
min-height: 100%;
display: flex;
flex-direction: column;
}
/* Allow dynamic height for Change Language popup */
.pop-over[data-popup="changeLanguage"] .content-wrapper {
max-height: inherit; /* Use dynamic height from JavaScript */
}
.pop-over[data-popup="changeLanguage"] .content-container {
max-height: inherit; /* Use dynamic height from JavaScript */
}
/* Make language popup extend to bottom of browser window */
.pop-over[data-popup="changeLanguage"] {
height: calc(100vh - 30px);
min-height: 300px;
/* Adjust positioning to move popup 30px higher */
transform: translateY(-30px);
}
.pop-over[data-popup="changeLanguage"] .content-wrapper {
height: calc(100% - 50px); /* Subtract header height more precisely */
min-height: 250px;
overflow-y: auto;
max-height: none; /* Remove any max-height constraints */
display: flex;
flex-direction: column;
}
.pop-over[data-popup="changeLanguage"] .content-container {
height: auto; /* Let content determine height */
min-height: 250px;
max-height: none; /* Remove any max-height constraints */
flex: 1;
display: flex;
flex-direction: column;
}
/* Date popup sizing for native HTML inputs */
.pop-over[data-popup="editCardReceivedDatePopup"],
.pop-over[data-popup="editCardStartDatePopup"],
.pop-over[data-popup="editCardDueDatePopup"],
.pop-over[data-popup="editCardEndDatePopup"],
.pop-over[data-popup*="Date"] {
width: min(400px, 90vw) !important; /* Smaller width for native inputs */
min-width: 350px !important;
max-height: 80vh !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .content-wrapper,
.pop-over[data-popup="editCardStartDatePopup"] .content-wrapper,
.pop-over[data-popup="editCardDueDatePopup"] .content-wrapper,
.pop-over[data-popup="editCardEndDatePopup"] .content-wrapper,
.pop-over[data-popup*="Date"] .content-wrapper {
max-height: 60vh !important;
overflow-y: auto !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .content-container,
.pop-over[data-popup="editCardStartDatePopup"] .content-container,
.pop-over[data-popup="editCardDueDatePopup"] .content-container,
.pop-over[data-popup="editCardEndDatePopup"] .content-container,
.pop-over[data-popup*="Date"] .content-container {
max-height: 60vh !important;
}
/* Native HTML input styling */
.pop-over[data-popup*="Date"] .datepicker-container {
width: 100% !important;
padding: 15px !important;
}
.pop-over[data-popup*="Date"] .datepicker-container .fields {
display: flex !important;
gap: 15px !important;
margin-bottom: 15px !important;
}
.pop-over[data-popup*="Date"] .datepicker-container .fields .left,
.pop-over[data-popup*="Date"] .datepicker-container .fields .right {
flex: 1 !important;
width: auto !important;
}
.pop-over[data-popup*="Date"] .datepicker-container label {
display: block !important;
margin-bottom: 5px !important;
font-weight: bold !important;
}
.pop-over[data-popup*="Date"] .datepicker-container input[type="date"],
.pop-over[data-popup*="Date"] .datepicker-container input[type="time"] {
width: 100% !important;
padding: 8px !important;
border: 1px solid #ccc !important;
border-radius: 4px !important;
font-size: 14px !important;
box-sizing: border-box !important;
}
.pop-over[data-popup*="Date"] .datepicker-container input[type="date"]:focus,
.pop-over[data-popup*="Date"] .datepicker-container input[type="time"]:focus {
outline: none !important;
border-color: #007cba !important;
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2) !important;
}
/* Ensure date popup buttons stay within popup boundaries */
.pop-over[data-popup="editCardReceivedDatePopup"] .content,
.pop-over[data-popup="editCardStartDatePopup"] .content,
.pop-over[data-popup="editCardDueDatePopup"] .content,
.pop-over[data-popup="editCardEndDatePopup"] .content,
.pop-over[data-popup*="Date"] .content {
max-height: 60vh !important; /* Leave space for buttons */
overflow-y: auto !important;
padding-bottom: 100px !important; /* More space for buttons */
margin-bottom: 0 !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .datepicker-container,
.pop-over[data-popup="editCardStartDatePopup"] .datepicker-container,
.pop-over[data-popup="editCardDueDatePopup"] .datepicker-container,
.pop-over[data-popup="editCardEndDatePopup"] .datepicker-container,
.pop-over[data-popup*="Date"] .datepicker-container {
max-height: 50vh !important; /* Limit calendar height */
overflow-y: auto !important;
margin-bottom: 20px !important; /* Space before buttons */
}
/* Ensure buttons are properly positioned */
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date,
.pop-over[data-popup="editCardStartDatePopup"] .edit-date,
.pop-over[data-popup="editCardDueDatePopup"] .edit-date,
.pop-over[data-popup="editCardEndDatePopup"] .edit-date,
.pop-over[data-popup*="Date"] .edit-date {
display: flex !important;
flex-direction: column !important;
height: 100% !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date .fields,
.pop-over[data-popup="editCardStartDatePopup"] .edit-date .fields,
.pop-over[data-popup="editCardDueDatePopup"] .edit-date .fields,
.pop-over[data-popup="editCardEndDatePopup"] .edit-date .fields,
.pop-over[data-popup*="Date"] .edit-date .fields {
flex-shrink: 0 !important;
margin-bottom: 15px !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date .js-datepicker,
.pop-over[data-popup="editCardStartDatePopup"] .edit-date .js-datepicker,
.pop-over[data-popup="editCardDueDatePopup"] .edit-date .js-datepicker,
.pop-over[data-popup="editCardEndDatePopup"] .edit-date .js-datepicker,
.pop-over[data-popup*="Date"] .edit-date .js-datepicker {
flex: 1 !important;
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,
.pop-over[data-popup="editCardEndDatePopup"] .edit-date button,
.pop-over[data-popup*="Date"] .edit-date button {
flex-shrink: 0 !important;
margin-top: 15px !important;
position: relative !important;
z-index: 10 !important;
}
.pop-over .content-container .content {
width: min(280px, 37vw);
/* Match wider popover, leave padding */
width: 100%;
padding: 0 1.3vw 1.3vh;
float: left;
box-sizing: border-box;
/* Ensure content is not shifted left */
margin-left: 0 !important;
transform: none !important;
}
/* Utility: remove left gutter inside specific popups */
.pop-over .content .flush-left {
margin-left: 0;
padding-left: 0;
width: 100%;
}
/* Swimlane popups: remove left gutter, align content fully left */
.pop-over .content form.swimlane-color-popup,
.pop-over .content .swimlane-height-popup {
margin-left: 0;
padding-left: 0;
width: 100%;
}
/* Color selection popups: ensure proper alignment */
.pop-over .content form.swimlane-color-popup .palette-colors,
.pop-over .content form.edit-label .palette-colors,
.pop-over .content form.create-label .palette-colors {
margin-left: 0;
padding-left: 0;
width: 100%;
}
/* Color palette items: ensure proper positioning */
.pop-over .content .palette-colors .palette-color {
margin-left: 0;
margin-right: 2px;
margin-bottom: 2px;
}
/* Global fix for all popup content to prevent left shifting */
.pop-over .content * {
margin-left: 0 !important;
transform: none !important;
}
/* Override any potential left shifting for specific elements */
.pop-over .content form,
.pop-over .content .palette-colors,
.pop-over .content .pop-over-list,
.pop-over .content .flush-left {
margin-left: 0 !important;
padding-left: 0 !important;
transform: none !important;
}
/* Fix popup depth containers that cause left shifting */
.pop-over .popup-container-depth-1,
.pop-over .popup-container-depth-2,
.pop-over .popup-container-depth-3,
.pop-over .popup-container-depth-4,
.pop-over .popup-container-depth-5,
.pop-over .popup-container-depth-6 {
transform: none !important;
margin-left: 0 !important;
padding-left: 0 !important;
}
/* Ensure buttons dont reserve left space; align to flow */
.pop-over .content form.swimlane-color-popup .primary.confirm,
.pop-over .content form.swimlane-color-popup .negate.wide.right,
.pop-over .content .swimlane-height-popup .primary.confirm,
.pop-over .content .swimlane-height-popup .negate.wide.right {
float: none;
margin-left: 0;
}
.pop-over .content-container .content.no-height {
height: 2.5vh;
}
.pop-over .quiet {
/* padding: 6px 6px 4px;*/
height: 0;
overflow: hidden;
padding: 0;
margin: 0;
visibility: hidden;
}
.pop-over.search-over {
background: #f0f0f0;
@ -104,7 +403,7 @@
.pop-over .at-form .at-error,
.pop-over .at-form .at-result {
padding: 8px 12px;
margin: -8px -10px 10px;
margin: 0 0 10px 0;
}
.pop-over .at-form .at-error {
background: #ef9a9a;
@ -148,7 +447,7 @@
font-weight: 700;
padding: 1.5px 10px;
position: relative;
margin: 0 -10px;
margin: 0;
text-decoration: none;
overflow: hidden;
line-height: 33px;
@ -307,12 +606,12 @@
margin: 48px 0px 0px 0px;
}
.pop-over .content-container {
width: 1000%;
width: 100%;
height: 100%;
max-height: 100%;
}
.pop-over .content-container .content {
width: calc(10% - 20px);
width: calc(100% - 20px);
height: calc(100% - 20px);
padding: 10px;
}
@ -334,21 +633,21 @@
margin: 0px 0px;
}
.pop-over .popup-container-depth-1 {
transform: translateX(-10%);
transform: none !important;
}
.pop-over .popup-container-depth-2 {
transform: translateX(-20%);
transform: none !important;
}
.pop-over .popup-container-depth-3 {
transform: translateX(-30%);
transform: none !important;
}
.pop-over .popup-container-depth-4 {
transform: translateX(-40%);
transform: none !important;
}
.pop-over .popup-container-depth-5 {
transform: translateX(-50%);
transform: none !important;
}
.pop-over .popup-container-depth-6 {
transform: translateX(-60%);
transform: none !important;
}
}

View file

@ -2,13 +2,13 @@
class="{{#unless title}}miniprofile{{/unless}}"
class=currentBoard.colorClass
class="{{#unless title}}no-title{{/unless}}"
style="left:{{offset.left}}px; top:{{offset.top}}px;")
style="left:{{offset.left}}px; top:{{offset.top}}px;{{#if offset.maxHeight}} max-height:{{offset.maxHeight}}px;{{/if}}")
.header
a.back-btn.js-back-view(class="{{#unless hasPopupParent}}is-hidden{{/unless}}")
i.fa.fa-chevron-left
| ◀️
span.header-title= title
a.close-btn.js-close-pop-over
i.fa.fa-times-thin
| ❌
.content-wrapper
//-
We display the all stack of popup content next to each other and move

View file

@ -0,0 +1,269 @@
/* Migration Progress Styles */
.migration-progress-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(2px);
}
.migration-progress-modal {
background: white;
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: migrationModalSlideIn 0.3s ease-out;
}
@keyframes migrationModalSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.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-progress-title {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.migration-progress-close {
cursor: pointer;
font-size: 16px;
opacity: 0.8;
transition: opacity 0.2s ease;
}
.migration-progress-close:hover {
opacity: 1;
}
.migration-progress-content {
padding: 30px;
}
.migration-progress-overall {
margin-bottom: 25px;
}
.migration-progress-overall-label {
font-weight: 600;
color: #333;
margin-bottom: 8px;
font-size: 14px;
}
.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%;
border-radius: 10px;
transition: width 0.3s ease;
position: relative;
}
.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.3), transparent);
animation: migrationProgressShimmer 2s infinite;
}
@keyframes migrationProgressShimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.migration-progress-overall-percentage {
text-align: right;
font-size: 12px;
color: #666;
font-weight: 600;
}
.migration-progress-current-step {
margin-bottom: 25px;
}
.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%;
border-radius: 8px;
transition: width 0.3s ease;
}
.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-left: 4px solid #007bff;
}
.migration-progress-status-label {
font-weight: 600;
color: #333;
margin-bottom: 5px;
font-size: 13px;
}
.migration-progress-status-text {
color: #555;
font-size: 14px;
line-height: 1.4;
}
.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;
font-style: italic;
}
/* Responsive design */
@media (max-width: 600px) {
.migration-progress-modal {
width: 95%;
margin: 20px;
}
.migration-progress-content {
padding: 20px;
}
.migration-progress-header {
padding: 15px;
}
.migration-progress-title {
font-size: 16px;
}
}
/* 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

@ -0,0 +1,43 @@
template(name="migrationProgress")
if isMigrating
.migration-progress-overlay
.migration-progress-modal
.migration-progress-header
h3.migration-progress-title
| 🔄 Board Migration in Progress
.migration-progress-close.js-close-migration-progress
| ❌
.migration-progress-content
.migration-progress-overall
.migration-progress-overall-label
| Overall Progress: {{currentStep}} of {{totalSteps}} steps
.migration-progress-overall-bar
.migration-progress-overall-fill(style="{{progressBarStyle}}")
.migration-progress-overall-percentage
| {{overallProgress}}%
.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}}%
.migration-progress-status
.migration-progress-status-label
| Status:
.migration-progress-status-text
| {{stepStatus}}
if stepDetailsFormatted
.migration-progress-details
.migration-progress-details-label
| Details:
.migration-progress-details-text
| {{stepDetailsFormatted}}
.migration-progress-footer
.migration-progress-note
| Please wait while we migrate your board to the latest structure...

View file

@ -0,0 +1,212 @@
/**
* 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();
},
overallProgress() {
return migrationProgress.get();
},
overallStatus() {
return migrationStatus.get();
},
currentStep() {
return migrationCurrentStep.get();
},
totalSteps() {
return migrationTotalSteps.get();
},
stepName() {
return migrationStepName.get();
},
stepProgress() {
return migrationStepProgress.get();
},
stepStatus() {
return migrationStepStatus.get();
},
stepDetails() {
return migrationStepDetails.get();
},
progressBarStyle() {
const progress = migrationProgress.get();
return `width: ${progress}%`;
},
stepProgressBarStyle() {
const progress = migrationStepProgress.get();
return `width: ${progress}%`;
},
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 events
Template.migrationProgress.events({
'click .js-close-migration-progress'() {
migrationProgressManager.clearProgress();
}
});

View file

@ -1,5 +1,6 @@
template(name='notifications')
#notifications.board-header-btns.right
a.notifications-drawer-toggle.fa.fa-bell(class="{{#if $gt unreadNotifications 0}}alert{{/if}}" title="{{_ 'notifications'}}")
a.notifications-drawer-toggle(class="{{#if $gt unreadNotifications 0}}alert{{/if}}" title="{{_ 'notifications'}}")
| 🔔
if $.Session.get 'showNotificationsDrawer'
+notificationsDrawer(unreadNotifications=unreadNotifications)

View file

@ -10,7 +10,7 @@ template(name="boardActions")
div.trigger-text
| {{_'r-its-list'}}
div.trigger-button.js-add-gen-move-action.js-goto-rules
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -38,7 +38,7 @@ template(name="boardActions")
div.trigger-dropdown
input(id="swimlaneName",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-spec-move-action.js-goto-rules
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -49,7 +49,7 @@ template(name="boardActions")
div.trigger-text
| {{_'r-card'}}
div.trigger-button.js-add-arch-action.js-goto-rules
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -58,7 +58,7 @@ template(name="boardActions")
div.trigger-dropdown
input(id="swimlane-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-swimlane-action.js-goto-rules
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -75,7 +75,7 @@ template(name="boardActions")
div.trigger-dropdown
input(id="swimlane-name2",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-create-card-action.js-goto-rules
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -99,7 +99,7 @@ template(name="boardActions")
div.trigger-dropdown
input(id="swimlaneName-link",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-link-card-action.js-goto-rules
i.fa.fa-plus
|

View file

@ -16,7 +16,7 @@ template(name="cardActions")
div.trigger-text
| {{_'r-to-current-datetime'}}
div.trigger-button.js-set-date-action.js-goto-rules
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -30,7 +30,7 @@ template(name="cardActions")
option(value="endAt") {{_'r-df-end-at'}}
option(value="receivedAt") {{_'r-df-received-at'}}
div.trigger-button.js-remove-datevalue-action.js-goto-rules
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -46,7 +46,7 @@ template(name="cardActions")
option(value="#{_id}")
= name
div.trigger-button.js-add-label-action.js-goto-rules
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -59,14 +59,14 @@ template(name="cardActions")
div.trigger-dropdown
input(id="member-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-member-action.js-goto-rules
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
div.trigger-text
| {{_'r-remove-all'}}
div.trigger-button.js-add-removeall-action.js-goto-rules
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -77,12 +77,12 @@ template(name="cardActions")
class="card-details-{{cardColorButton}}")
| {{_ cardColorButtonText }}
div.trigger-button.js-set-color-action.js-goto-rules
i.fa.fa-plus
|
template(name="setCardActionsColorPopup")
form.edit-label
.palette-colors: each colors
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color)
i.fa.fa-check
| ✅
button.primary.confirm.js-submit {{_ 'save'}}

View file

@ -10,7 +10,7 @@ template(name="checklistActions")
div.trigger-dropdown
input(id="checklist-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-checklist-action.js-goto-rules
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -23,7 +23,7 @@ template(name="checklistActions")
div.trigger-dropdown
input(id="checklist-name2",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-checkall-action.js-goto-rules
i.fa.fa-plus
|
div.trigger-item
@ -41,7 +41,7 @@ template(name="checklistActions")
div.trigger-dropdown
input(id="checklist-name3",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-check-item-action.js-goto-rules
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -54,7 +54,7 @@ template(name="checklistActions")
div.trigger-dropdown
input(id="checklist-items",type=text,placeholder="{{_'r-items-list'}}")
div.trigger-button.js-add-checklist-items-action.js-goto-rules
i.fa.fa-plus
|
div.trigger-item
div.trigger-content

View file

@ -8,4 +8,4 @@ template(name="mailActions")
input(id="email-subject",type=text,placeholder="{{_'r-subject'}}")
textarea(id="email-msg")
div.trigger-button.trigger-button-email.js-mail-action.js-goto-rules
i.fa.fa-plus
|

View file

@ -1,7 +1,7 @@
template(name="ruleDetails")
.rules
h2
i.fa.fa-magic
| ✨
| {{_ 'r-rule-details' }}
.triggers-content
.triggers-body
@ -20,5 +20,5 @@ template(name="ruleDetails")
= action
div.rules-back
button.js-goback
i.fa.fa-chevron-left
| ◀️
| {{_ 'back'}}

View file

@ -1,19 +1,19 @@
template(name="rulesActions")
h2
i.fa.fa-magic
| ✨
| {{_ 'r-rule' }} "#{data.ruleName.get}" - {{_ 'r-add-action'}}
.triggers-content
.triggers-body
.triggers-side-menu
ul
li.active.js-set-board-actions
i.fa.fa-columns
| 📊
li.js-set-card-actions
i.fa.fa-sticky-note
| 📝
li.js-set-checklist-actions
i.fa.fa-check
| ✅
li.js-set-mail-actions
i.fa.fa-at
| @
.triggers-main-body
if ($eq currentActions.get 'board')
+boardActions(ruleName=data.ruleName triggerVar=data.triggerVar)
@ -25,5 +25,5 @@ template(name="rulesActions")
+mailActions(ruleName=data.ruleName triggerVar=data.triggerVar)
div.rules-back
button.js-goback
i.fa.fa-chevron-left
| ◀️
| {{_ 'back'}}

View file

@ -1,7 +1,7 @@
template(name="rulesList")
.rules
h2
i.fa.fa-magic
| ✨
| {{_ 'r-board-rules' }}
ul.rules-list
@ -11,27 +11,27 @@ template(name="rulesList")
= title
div.rules-btns-group
button.js-goto-details
i.fa.fa-eye
| 👁️
| {{_ 'r-view-rule'}}
if currentUser.isAdmin
button.js-delete-rule
i.fa.fa-trash-o
| 🗑️
| {{_ 'r-delete-rule'}}
else if currentUser.isBoardAdmin
button.js-delete-rule
i.fa.fa-trash-o
| 🗑️
| {{_ 'r-delete-rule'}}
else
li.no-items-message {{_ 'r-no-rules' }}
if currentUser.isAdmin
div.rules-add
button.js-goto-trigger
i.fa.fa-plus
|
| {{_ 'r-add-rule'}}
input(type=text,placeholder="{{_ 'r-new-rule-name' }}",id="ruleTitle")
else if currentUser.isBoardAdmin
div.rules-add
button.js-goto-trigger
i.fa.fa-plus
|
| {{_ 'r-add-rule'}}
input(type=text,placeholder="{{_ 'r-new-rule-name' }}",id="ruleTitle")

View file

@ -1,17 +1,17 @@
template(name="rulesTriggers")
h2
i.fa.fa-magic
| ✨
| {{_ 'r-rule' }} "#{data.ruleName.get}" - {{_ 'r-add-trigger'}}
.triggers-content
.triggers-body
.triggers-side-menu
ul
li.active.js-set-board-triggers
i.fa.fa-columns
| 📊
li.js-set-card-triggers
i.fa.fa-sticky-note
| 📝
li.js-set-checklist-triggers
i.fa.fa-check
| ✅
.triggers-main-body
if showBoardTrigger.get
+boardTriggers
@ -21,5 +21,5 @@ template(name="rulesTriggers")
+checklistTriggers
div.rules-back
button.js-goback
i.fa.fa-chevron-left
| ◀️
| {{_ 'back'}}

View file

@ -4,7 +4,7 @@ template(name="boardTriggers")
div.trigger-text
| {{_'r-when-a-card'}}
div.trigger-inline-button.js-open-card-title-popup
i.fa.fa-filter
| 🔍
div.trigger-text
| {{_'r-is'}}
div.trigger-text
@ -18,39 +18,39 @@ template(name="boardTriggers")
div.trigger-dropdown
input(id="create-swimlane-name",type=text,placeholder="{{_'r-swimlane-name'}}")
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-create-trigger.js-goto-action
i.fa.fa-plus
|
div.trigger-item#trigger-three
div.trigger-content
div.trigger-text
| {{_'r-when-a-card'}}
div.trigger-inline-button.js-open-card-title-popup
i.fa.fa-filter
| 🔍
div.trigger-text
| {{_'r-is-moved'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-moved-trigger.js-goto-action
i.fa.fa-plus
|
div.trigger-item#trigger-four
div.trigger-content
div.trigger-text
| {{_'r-when-a-card'}}
div.trigger-inline-button.js-open-card-title-popup
i.fa.fa-filter
| 🔍
div.trigger-text
| {{_'r-is'}}
div.trigger-dropdown
@ -66,21 +66,21 @@ template(name="boardTriggers")
div.trigger-dropdown
input(id="create-swimlane-name-2",type=text,placeholder="{{_'r-swimlane-name'}}")
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-moved-trigger.js-goto-action
i.fa.fa-plus
|
div.trigger-item#trigger-five
div.trigger-content
div.trigger-text
| {{_'r-when-a-card'}}
div.trigger-inline-button.js-open-card-title-popup
i.fa.fa-filter
| 🔍
div.trigger-text
| {{_'r-is'}}
div.trigger-dropdown
@ -88,14 +88,14 @@ template(name="boardTriggers")
option(value="archived") {{_'r-archived'}}
option(value="unarchived") {{_'r-unarchived'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-arch-trigger.js-goto-action
i.fa.fa-plus
|
div.trigger-item
div.trigger-content

View file

@ -10,14 +10,14 @@ template(name="cardTriggers")
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-label-trigger.js-goto-action
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -37,14 +37,14 @@ template(name="cardTriggers")
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-spec-label-trigger.js-goto-action
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -57,14 +57,14 @@ template(name="cardTriggers")
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-member-trigger.js-goto-action
i.fa.fa-plus
|
div.trigger-item
@ -82,14 +82,14 @@ template(name="cardTriggers")
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-spec-member-trigger.js-goto-action
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -104,11 +104,11 @@ template(name="cardTriggers")
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-attachment-trigger.js-goto-action
i.fa.fa-plus
|

View file

@ -10,14 +10,14 @@ template(name="checklistTriggers")
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-check-trigger.js-goto-action
i.fa.fa-plus
|
div.trigger-item
@ -35,14 +35,14 @@ template(name="checklistTriggers")
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-spec-check-trigger.js-goto-action
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -53,14 +53,14 @@ template(name="checklistTriggers")
option(value="completed") {{_'r-completed'}}
option(value="uncompleted") {{_'r-made-incomplete'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-comp-trigger.js-goto-action
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -75,14 +75,14 @@ template(name="checklistTriggers")
option(value="completed") {{_'r-completed'}}
option(value="uncompleted") {{_'r-made-incomplete'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-spec-comp-trigger.js-goto-action
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -93,14 +93,14 @@ template(name="checklistTriggers")
option(value="checked") {{_'r-checked'}}
option(value="unchecked") {{_'r-unchecked'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-check-item-trigger.js-goto-action
i.fa.fa-plus
|
div.trigger-item
div.trigger-content
@ -115,11 +115,11 @@ template(name="checklistTriggers")
option(value="checked") {{_'r-checked'}}
option(value="unchecked") {{_'r-unchecked'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-spec-check-item-trigger.js-goto-action
i.fa.fa-plus
|

View file

@ -8,27 +8,27 @@ template(name="adminReports")
ul
li
a.js-report-broken(data-id="report-broken")
i.fa.fa-chain-broken
| 🔗
| {{_ 'broken-cards'}}
li
a.js-report-files(data-id="report-files")
i.fa.fa-paperclip
| 📎
| {{_ 'filesReportTitle'}}
li
a.js-report-rules(data-id="report-rules")
i.fa.fa-magic
| ✨
| {{_ 'rulesReportTitle'}}
li
a.js-report-boards(data-id="report-boards")
i.fa.fa-magic
| ✨
| {{_ 'boardsReportTitle'}}
li
a.js-report-cards(data-id="report-cards")
i.fa.fa-magic
| ✨
| {{_ 'cardsReportTitle'}}
.main-body

View file

@ -112,7 +112,7 @@ class AdminReport extends BlazeComponent {
}
resultsCount() {
return this.collection.find().countDocuments();
return this.collection.find().count();
}
fileSize(size) {

View file

@ -1,33 +1,74 @@
template(name="attachmentSettings")
.setting-content.attachment-settings-content
unless currentUser.isAdmin
| {{_ 'error-notAuthorized'}}
else
.content-body
.side-menu
ul
li
a.js-attachment-storage-settings(data-id="storage-settings")
i.fa.fa-cog
| {{_ 'attachment-storage-settings'}}
li
a.js-attachment-migration(data-id="attachment-migration")
i.fa.fa-arrow-right
| {{_ 'attachment-migration'}}
li
a.js-attachment-monitoring(data-id="attachment-monitoring")
i.fa.fa-chart-line
| {{_ 'attachment-monitoring'}}
ul#attachment-setting.setting-detail
li
h3 {{_ 'attachment-storage-configuration'}}
.form-group
label {{_ 'writable-path'}}
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
small.form-text.text-muted {{_ 'filesystem-path-description'}}
.form-group
label {{_ 'attachments-path'}}
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
small.form-text.text-muted {{_ 'attachments-path-description'}}
.form-group
label {{_ 'avatars-path'}}
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
small.form-text.text-muted {{_ 'avatars-path-description'}}
.main-body
if loading.get
+spinner
else if showStorageSettings.get
+storageSettings
else if showMigration.get
+attachmentMigration
else if showMonitoring.get
+attachmentMonitoring
li
h3 {{_ 'mongodb-gridfs-storage'}}
.form-group
label {{_ 'gridfs-enabled'}}
input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
li
h3 {{_ 's3-minio-storage'}}
.form-group
label {{_ 's3-enabled'}}
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
small.form-text.text-muted {{_ 's3-enabled-description'}}
.form-group
label {{_ 's3-endpoint'}}
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
small.form-text.text-muted {{_ 's3-endpoint-description'}}
.form-group
label {{_ 's3-bucket'}}
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
small.form-text.text-muted {{_ 's3-bucket-description'}}
.form-group
label {{_ 's3-region'}}
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
small.form-text.text-muted {{_ 's3-region-description'}}
.form-group
label {{_ 's3-access-key'}}
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
small.form-text.text-muted {{_ 's3-access-key-description'}}
.form-group
label {{_ 's3-secret-key'}}
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
small.form-text.text-muted {{_ 's3-secret-key-description'}}
.form-group
label {{_ 's3-ssl-enabled'}}
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
.form-group
label {{_ 's3-port'}}
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
small.form-text.text-muted {{_ 's3-port-description'}}
.form-group
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
template(name="storageSettings")
.storage-settings

View file

@ -1,464 +0,0 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { ReactiveVar } from 'meteor/reactive-var';
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
import { Chart } from 'chart.js';
// Global reactive variables for attachment settings
const attachmentSettings = {
loading: new ReactiveVar(false),
showStorageSettings: new ReactiveVar(false),
showMigration: new ReactiveVar(false),
showMonitoring: new ReactiveVar(false),
// Storage configuration
filesystemPath: new ReactiveVar(''),
attachmentsPath: new ReactiveVar(''),
avatarsPath: new ReactiveVar(''),
gridfsEnabled: new ReactiveVar(false),
s3Enabled: new ReactiveVar(false),
s3Endpoint: new ReactiveVar(''),
s3Bucket: new ReactiveVar(''),
s3Region: new ReactiveVar(''),
s3SslEnabled: new ReactiveVar(false),
s3Port: new ReactiveVar(443),
// Migration settings
migrationBatchSize: new ReactiveVar(10),
migrationDelayMs: new ReactiveVar(1000),
migrationCpuThreshold: new ReactiveVar(70),
migrationProgress: new ReactiveVar(0),
migrationStatus: new ReactiveVar('idle'),
migrationLog: new ReactiveVar(''),
// Monitoring data
totalAttachments: new ReactiveVar(0),
filesystemAttachments: new ReactiveVar(0),
gridfsAttachments: new ReactiveVar(0),
s3Attachments: new ReactiveVar(0),
totalSize: new ReactiveVar(0),
filesystemSize: new ReactiveVar(0),
gridfsSize: new ReactiveVar(0),
s3Size: new ReactiveVar(0),
// Migration state
isMigrationRunning: new ReactiveVar(false),
isMigrationPaused: new ReactiveVar(false),
migrationQueue: new ReactiveVar([]),
currentMigration: new ReactiveVar(null)
};
// Main attachment settings component
BlazeComponent.extendComponent({
onCreated() {
this.loading = attachmentSettings.loading;
this.showStorageSettings = attachmentSettings.showStorageSettings;
this.showMigration = attachmentSettings.showMigration;
this.showMonitoring = attachmentSettings.showMonitoring;
// Load initial data
this.loadStorageConfiguration();
this.loadMigrationSettings();
this.loadMonitoringData();
},
events() {
return [
{
'click a.js-attachment-storage-settings': this.switchToStorageSettings,
'click a.js-attachment-migration': this.switchToMigration,
'click a.js-attachment-monitoring': this.switchToMonitoring,
}
];
},
switchToStorageSettings(event) {
this.switchMenu(event, 'storage-settings');
this.showStorageSettings.set(true);
this.showMigration.set(false);
this.showMonitoring.set(false);
},
switchToMigration(event) {
this.switchMenu(event, 'attachment-migration');
this.showStorageSettings.set(false);
this.showMigration.set(true);
this.showMonitoring.set(false);
},
switchToMonitoring(event) {
this.switchMenu(event, 'attachment-monitoring');
this.showStorageSettings.set(false);
this.showMigration.set(false);
this.showMonitoring.set(true);
},
switchMenu(event, targetId) {
const target = $(event.target);
if (!target.hasClass('active')) {
this.loading.set(true);
$('.side-menu li.active').removeClass('active');
target.parent().addClass('active');
// Load data based on target
if (targetId === 'storage-settings') {
this.loadStorageConfiguration();
} else if (targetId === 'attachment-migration') {
this.loadMigrationSettings();
} else if (targetId === 'attachment-monitoring') {
this.loadMonitoringData();
}
this.loading.set(false);
}
},
loadStorageConfiguration() {
Meteor.call('getAttachmentStorageConfiguration', (error, result) => {
if (!error && result) {
attachmentSettings.filesystemPath.set(result.filesystemPath || '');
attachmentSettings.attachmentsPath.set(result.attachmentsPath || '');
attachmentSettings.avatarsPath.set(result.avatarsPath || '');
attachmentSettings.gridfsEnabled.set(result.gridfsEnabled || false);
attachmentSettings.s3Enabled.set(result.s3Enabled || false);
attachmentSettings.s3Endpoint.set(result.s3Endpoint || '');
attachmentSettings.s3Bucket.set(result.s3Bucket || '');
attachmentSettings.s3Region.set(result.s3Region || '');
attachmentSettings.s3SslEnabled.set(result.s3SslEnabled || false);
attachmentSettings.s3Port.set(result.s3Port || 443);
}
});
},
loadMigrationSettings() {
Meteor.call('getAttachmentMigrationSettings', (error, result) => {
if (!error && result) {
attachmentSettings.migrationBatchSize.set(result.batchSize || 10);
attachmentSettings.migrationDelayMs.set(result.delayMs || 1000);
attachmentSettings.migrationCpuThreshold.set(result.cpuThreshold || 70);
attachmentSettings.migrationStatus.set(result.status || 'idle');
attachmentSettings.migrationProgress.set(result.progress || 0);
}
});
},
loadMonitoringData() {
Meteor.call('getAttachmentMonitoringData', (error, result) => {
if (!error && result) {
attachmentSettings.totalAttachments.set(result.totalAttachments || 0);
attachmentSettings.filesystemAttachments.set(result.filesystemAttachments || 0);
attachmentSettings.gridfsAttachments.set(result.gridfsAttachments || 0);
attachmentSettings.s3Attachments.set(result.s3Attachments || 0);
attachmentSettings.totalSize.set(result.totalSize || 0);
attachmentSettings.filesystemSize.set(result.filesystemSize || 0);
attachmentSettings.gridfsSize.set(result.gridfsSize || 0);
attachmentSettings.s3Size.set(result.s3Size || 0);
}
});
}
}).register('attachmentSettings');
// Storage settings component
BlazeComponent.extendComponent({
onCreated() {
this.filesystemPath = attachmentSettings.filesystemPath;
this.attachmentsPath = attachmentSettings.attachmentsPath;
this.avatarsPath = attachmentSettings.avatarsPath;
this.gridfsEnabled = attachmentSettings.gridfsEnabled;
this.s3Enabled = attachmentSettings.s3Enabled;
this.s3Endpoint = attachmentSettings.s3Endpoint;
this.s3Bucket = attachmentSettings.s3Bucket;
this.s3Region = attachmentSettings.s3Region;
this.s3SslEnabled = attachmentSettings.s3SslEnabled;
this.s3Port = attachmentSettings.s3Port;
},
events() {
return [
{
'click button.js-test-s3-connection': this.testS3Connection,
'click button.js-save-s3-settings': this.saveS3Settings,
'change input#s3-secret-key': this.updateS3SecretKey
}
];
},
testS3Connection() {
const secretKey = $('#s3-secret-key').val();
if (!secretKey) {
alert(TAPi18n.__('s3-secret-key-required'));
return;
}
Meteor.call('testS3Connection', { secretKey }, (error, result) => {
if (error) {
alert(TAPi18n.__('s3-connection-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('s3-connection-success'));
}
});
},
saveS3Settings() {
const secretKey = $('#s3-secret-key').val();
if (!secretKey) {
alert(TAPi18n.__('s3-secret-key-required'));
return;
}
Meteor.call('saveS3Settings', { secretKey }, (error, result) => {
if (error) {
alert(TAPi18n.__('s3-settings-save-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('s3-settings-saved'));
$('#s3-secret-key').val(''); // Clear the password field
}
});
},
updateS3SecretKey(event) {
// This method can be used to validate the secret key format
const secretKey = event.target.value;
// Add validation logic here if needed
}
}).register('storageSettings');
// Migration component
BlazeComponent.extendComponent({
onCreated() {
this.migrationBatchSize = attachmentSettings.migrationBatchSize;
this.migrationDelayMs = attachmentSettings.migrationDelayMs;
this.migrationCpuThreshold = attachmentSettings.migrationCpuThreshold;
this.migrationProgress = attachmentSettings.migrationProgress;
this.migrationStatus = attachmentSettings.migrationStatus;
this.migrationLog = attachmentSettings.migrationLog;
this.isMigrationRunning = attachmentSettings.isMigrationRunning;
this.isMigrationPaused = attachmentSettings.isMigrationPaused;
// Subscribe to migration updates
this.subscription = Meteor.subscribe('attachmentMigrationStatus');
// Set up reactive updates
this.autorun(() => {
const status = attachmentSettings.migrationStatus.get();
if (status === 'running') {
this.isMigrationRunning.set(true);
} else {
this.isMigrationRunning.set(false);
}
});
},
onDestroyed() {
if (this.subscription) {
this.subscription.stop();
}
},
events() {
return [
{
'click button.js-migrate-all-to-filesystem': () => this.startMigration('filesystem'),
'click button.js-migrate-all-to-gridfs': () => this.startMigration('gridfs'),
'click button.js-migrate-all-to-s3': () => this.startMigration('s3'),
'click button.js-pause-migration': this.pauseMigration,
'click button.js-resume-migration': this.resumeMigration,
'click button.js-stop-migration': this.stopMigration,
'change input#migration-batch-size': this.updateBatchSize,
'change input#migration-delay-ms': this.updateDelayMs,
'change input#migration-cpu-threshold': this.updateCpuThreshold
}
];
},
startMigration(targetStorage) {
const batchSize = parseInt($('#migration-batch-size').val()) || 10;
const delayMs = parseInt($('#migration-delay-ms').val()) || 1000;
const cpuThreshold = parseInt($('#migration-cpu-threshold').val()) || 70;
Meteor.call('startAttachmentMigration', {
targetStorage,
batchSize,
delayMs,
cpuThreshold
}, (error, result) => {
if (error) {
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
} else {
this.addToLog(TAPi18n.__('migration-started') + ': ' + targetStorage);
}
});
},
pauseMigration() {
Meteor.call('pauseAttachmentMigration', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
} else {
this.addToLog(TAPi18n.__('migration-paused'));
}
});
},
resumeMigration() {
Meteor.call('resumeAttachmentMigration', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-resume-failed') + ': ' + error.reason);
} else {
this.addToLog(TAPi18n.__('migration-resumed'));
}
});
},
stopMigration() {
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
Meteor.call('stopAttachmentMigration', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
} else {
this.addToLog(TAPi18n.__('migration-stopped'));
}
});
}
},
updateBatchSize(event) {
const value = parseInt(event.target.value);
if (value >= 1 && value <= 100) {
attachmentSettings.migrationBatchSize.set(value);
}
},
updateDelayMs(event) {
const value = parseInt(event.target.value);
if (value >= 100 && value <= 10000) {
attachmentSettings.migrationDelayMs.set(value);
}
},
updateCpuThreshold(event) {
const value = parseInt(event.target.value);
if (value >= 10 && value <= 90) {
attachmentSettings.migrationCpuThreshold.set(value);
}
},
addToLog(message) {
const timestamp = new Date().toISOString();
const currentLog = attachmentSettings.migrationLog.get();
const newLog = `[${timestamp}] ${message}\n${currentLog}`;
attachmentSettings.migrationLog.set(newLog);
}
}).register('attachmentMigration');
// Monitoring component
BlazeComponent.extendComponent({
onCreated() {
this.totalAttachments = attachmentSettings.totalAttachments;
this.filesystemAttachments = attachmentSettings.filesystemAttachments;
this.gridfsAttachments = attachmentSettings.gridfsAttachments;
this.s3Attachments = attachmentSettings.s3Attachments;
this.totalSize = attachmentSettings.totalSize;
this.filesystemSize = attachmentSettings.filesystemSize;
this.gridfsSize = attachmentSettings.gridfsSize;
this.s3Size = attachmentSettings.s3Size;
// Subscribe to monitoring updates
this.subscription = Meteor.subscribe('attachmentMonitoringData');
// Set up chart
this.autorun(() => {
this.updateChart();
});
},
onDestroyed() {
if (this.subscription) {
this.subscription.stop();
}
},
events() {
return [
{
'click button.js-refresh-monitoring': this.refreshMonitoring,
'click button.js-export-monitoring': this.exportMonitoring
}
];
},
refreshMonitoring() {
Meteor.call('refreshAttachmentMonitoringData', (error, result) => {
if (error) {
alert(TAPi18n.__('monitoring-refresh-failed') + ': ' + error.reason);
}
});
},
exportMonitoring() {
Meteor.call('exportAttachmentMonitoringData', (error, result) => {
if (error) {
alert(TAPi18n.__('monitoring-export-failed') + ': ' + error.reason);
} else {
// Download the exported data
const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'wekan-attachment-monitoring.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
});
},
updateChart() {
const ctx = document.getElementById('storage-distribution-chart');
if (!ctx) return;
const filesystemCount = this.filesystemAttachments.get();
const gridfsCount = this.gridfsAttachments.get();
const s3Count = this.s3Attachments.get();
if (this.chart) {
this.chart.destroy();
}
this.chart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: [
TAPi18n.__('filesystem-storage'),
TAPi18n.__('gridfs-storage'),
TAPi18n.__('s3-storage')
],
datasets: [{
data: [filesystemCount, gridfsCount, s3Count],
backgroundColor: [
'#28a745',
'#007bff',
'#ffc107'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
}).register('attachmentMonitoring');
// Export the attachment settings for use in other components
export { attachmentSettings };

View file

@ -8,7 +8,7 @@ template(name="attachments")
ul
li
a.js-move-attachments(data-id="move-attachments")
i.fa.fa-arrow-right
| ➡️
| {{_ 'attachment-move'}}
.main-body
@ -80,17 +80,17 @@ template(name="moveAttachment")
td
if $neq version.storageName "fs"
button.js-move-storage-fs
i.fa.fa-arrow-right
| ➡️
| {{_ 'attachment-move-storage-fs'}}
if $neq version.storageName "gridfs"
if version.storageName
button.js-move-storage-gridfs
i.fa.fa-arrow-right
| ➡️
| {{_ 'attachment-move-storage-gridfs'}}
if $neq version.storageName "s3"
if version.storageName
button.js-move-storage-s3
i.fa.fa-arrow-right
| ➡️
| {{_ 'attachment-move-storage-s3'}}

View file

@ -0,0 +1,864 @@
/* Cron Settings Styles */
.cron-settings-content {
min-height: 600px;
}
.cron-migrations {
padding: 20px;
}
.migration-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.migration-header h2 {
margin: 0;
color: #333;
font-size: 24px;
font-weight: 600;
}
.migration-header h2 i {
margin-right: 10px;
color: #667eea;
}
.migration-controls {
display: flex;
gap: 10px;
}
.migration-controls .btn {
padding: 8px 16px;
font-size: 14px;
border-radius: 4px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.migration-controls .btn-primary {
background-color: #28a745;
color: white;
}
.migration-controls .btn-primary:hover {
background-color: #218838;
}
.migration-controls .btn-warning {
background-color: #ffc107;
color: #212529;
}
.migration-controls .btn-warning:hover {
background-color: #e0a800;
}
.migration-controls .btn-danger {
background-color: #dc3545;
color: white;
}
.migration-controls .btn-danger:hover {
background-color: #c82333;
}
.migration-progress {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
border-left: 4px solid #667eea;
}
.progress-overview {
margin-bottom: 20px;
}
.progress-bar {
width: 100%;
height: 12px;
background-color: #e0e0e0;
border-radius: 6px;
overflow: hidden;
margin-bottom: 8px;
position: relative;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 6px;
transition: width 0.3s ease;
position: relative;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.4),
transparent
);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.progress-text {
text-align: center;
font-weight: 700;
color: #667eea;
font-size: 18px;
}
.progress-label {
text-align: center;
color: #666;
font-size: 14px;
margin-top: 4px;
}
.current-step {
text-align: center;
color: #333;
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
}
.current-step i {
margin-right: 8px;
color: #667eea;
}
.migration-status {
text-align: center;
color: #333;
font-size: 16px;
background-color: #e3f2fd;
padding: 12px 16px;
border-radius: 6px;
border: 1px solid #bbdefb;
}
.migration-status i {
margin-right: 8px;
color: #2196f3;
}
.migration-steps {
margin-top: 30px;
}
.migration-steps h3 {
margin: 0 0 20px 0;
color: #333;
font-size: 20px;
font-weight: 600;
}
.steps-list {
max-height: 400px;
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 {
text-align: right;
min-width: 40px;
}
.step-progress .progress-text {
font-size: 12px;
font-weight: 600;
}
.step-progress-bar {
width: 100%;
height: 4px;
background-color: #e0e0e0;
border-radius: 2px;
overflow: hidden;
margin-top: 8px;
}
.step-progress-bar .progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 2px;
transition: width 0.3s ease;
}
/* Cron Jobs Styles */
.cron-jobs {
padding: 20px;
}
.jobs-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.jobs-header h2 {
margin: 0;
color: #333;
font-size: 24px;
font-weight: 600;
}
.jobs-header h2 i {
margin-right: 10px;
color: #667eea;
}
.jobs-controls .btn {
padding: 8px 16px;
font-size: 14px;
border-radius: 4px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.jobs-controls .btn-success {
background-color: #28a745;
color: white;
}
.jobs-controls .btn-success:hover {
background-color: #218838;
}
.jobs-list {
margin-top: 20px;
}
.table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.table thead {
background-color: #f8f9fa;
}
.table th,
.table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.table th {
font-weight: 600;
color: #333;
font-size: 14px;
}
.table td {
font-size: 14px;
color: #666;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.status-badge.status-running {
background-color: #d4edda;
color: #155724;
}
.status-badge.status-stopped {
background-color: #f8d7da;
color: #721c24;
}
.status-badge.status-paused {
background-color: #fff3cd;
color: #856404;
}
.status-badge.status-completed {
background-color: #d1ecf1;
color: #0c5460;
}
.status-badge.status-error {
background-color: #f8d7da;
color: #721c24;
}
.btn-group {
display: flex;
gap: 4px;
}
.btn-group .btn {
padding: 4px 8px;
font-size: 12px;
border-radius: 3px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-group .btn-success {
background-color: #28a745;
color: white;
}
.btn-group .btn-success:hover {
background-color: #218838;
}
.btn-group .btn-warning {
background-color: #ffc107;
color: #212529;
}
.btn-group .btn-warning:hover {
background-color: #e0a800;
}
.btn-group .btn-danger {
background-color: #dc3545;
color: white;
}
.btn-group .btn-danger:hover {
background-color: #c82333;
}
/* Add Job Form Styles */
.cron-add-job {
padding: 20px;
}
.add-job-header {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.add-job-header h2 {
margin: 0;
color: #333;
font-size: 24px;
font-weight: 600;
}
.add-job-header h2 i {
margin-right: 10px;
color: #667eea;
}
.add-job-form {
max-width: 600px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
font-size: 14px;
}
.form-control {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.form-control:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.form-control[type="number"] {
width: 100px;
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 30px;
}
.form-actions .btn {
padding: 10px 20px;
font-size: 14px;
border-radius: 4px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.form-actions .btn-primary {
background-color: #667eea;
color: white;
}
.form-actions .btn-primary:hover {
background-color: #5a6fd8;
}
.form-actions .btn-default {
background-color: #6c757d;
color: white;
}
.form-actions .btn-default:hover {
background-color: #5a6268;
}
/* Board Operations Styles */
.cron-board-operations {
padding: 20px;
}
.board-operations-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.board-operations-header h2 {
margin: 0;
color: #333;
font-size: 24px;
font-weight: 600;
}
.board-operations-header h2 i {
margin-right: 10px;
color: #667eea;
}
.board-operations-controls {
display: flex;
gap: 10px;
}
.board-operations-controls .btn {
padding: 8px 16px;
font-size: 14px;
border-radius: 4px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.board-operations-controls .btn-success {
background-color: #28a745;
color: white;
}
.board-operations-controls .btn-success:hover {
background-color: #218838;
}
.board-operations-controls .btn-primary {
background-color: #667eea;
color: white;
}
.board-operations-controls .btn-primary:hover {
background-color: #5a6fd8;
}
.board-operations-stats {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
border-left: 4px solid #667eea;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #667eea;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.system-resources {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
border-left: 4px solid #28a745;
}
.resource-item {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.resource-item:last-child {
margin-bottom: 0;
}
.resource-label {
min-width: 120px;
font-weight: 600;
color: #333;
font-size: 14px;
}
.resource-bar {
flex: 1;
height: 12px;
background-color: #e0e0e0;
border-radius: 6px;
overflow: hidden;
margin: 0 15px;
position: relative;
}
.resource-fill {
height: 100%;
border-radius: 6px;
transition: width 0.3s ease;
position: relative;
}
.resource-item:nth-child(1) .resource-fill {
background: linear-gradient(90deg, #28a745, #20c997);
}
.resource-item:nth-child(2) .resource-fill {
background: linear-gradient(90deg, #007bff, #6f42c1);
}
.resource-value {
min-width: 50px;
text-align: right;
font-weight: 600;
color: #333;
font-size: 14px;
}
.board-operations-search {
margin-bottom: 30px;
}
.search-box {
position: relative;
max-width: 400px;
}
.search-box .form-control {
padding-right: 40px;
}
.search-icon {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: #999;
font-size: 16px;
}
.board-operations-list {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.operations-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
}
.operations-header h3 {
margin: 0;
color: #333;
font-size: 18px;
font-weight: 600;
}
.pagination-info {
color: #666;
font-size: 14px;
}
.operations-table {
overflow-x: auto;
}
.operations-table .table {
margin: 0;
border: none;
}
.operations-table .table th {
background-color: #f8f9fa;
border-bottom: 2px solid #e0e0e0;
font-weight: 600;
color: #333;
white-space: nowrap;
}
.operations-table .table td {
vertical-align: middle;
border-bottom: 1px solid #f0f0f0;
}
.board-id {
font-family: monospace;
font-size: 12px;
color: #666;
background: #f8f9fa;
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
}
.operation-type {
font-weight: 500;
color: #333;
text-transform: capitalize;
}
.progress-container {
display: flex;
align-items: center;
gap: 8px;
min-width: 120px;
}
.progress-container .progress-bar {
flex: 1;
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.progress-container .progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 4px;
transition: width 0.3s ease;
}
.progress-container .progress-text {
font-size: 12px;
font-weight: 600;
color: #667eea;
min-width: 35px;
text-align: right;
}
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: #f8f9fa;
border-top: 1px solid #e0e0e0;
}
.pagination .btn {
padding: 6px 12px;
font-size: 12px;
border-radius: 4px;
border: 1px solid #ddd;
background: white;
color: #333;
cursor: pointer;
transition: all 0.3s ease;
}
.pagination .btn:hover {
background: #f8f9fa;
border-color: #667eea;
}
.pagination .btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-info {
color: #666;
font-size: 14px;
}
/* Responsive design */
@media (max-width: 768px) {
.migration-header,
.jobs-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.migration-controls,
.jobs-controls {
width: 100%;
justify-content: center;
}
.table {
font-size: 12px;
}
.table th,
.table td {
padding: 8px 12px;
}
.btn-group {
flex-direction: column;
}
.add-job-form {
max-width: 100%;
}
}

View file

@ -0,0 +1,309 @@
template(name="cronSettings")
ul#cron-setting.setting-detail
li
h3 {{_ 'cron-migrations'}}
.form-group
label {{_ 'migration-status'}}
.status-indicator
span.status-label {{_ 'status'}}:
span.status-value {{migrationStatus}}
.progress-section
.progress
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
| {{migrationProgress}}%
.progress-text
| {{migrationProgress}}% {{_ 'complete'}}
.form-group
button.js-start-all-migrations.btn.btn-primary {{_ 'start-all-migrations'}}
button.js-pause-all-migrations.btn.btn-warning {{_ 'pause-all-migrations'}}
button.js-stop-all-migrations.btn.btn-danger {{_ 'stop-all-migrations'}}
li
h3 {{_ 'board-operations'}}
.form-group
label {{_ 'scheduled-board-operations'}}
button.js-schedule-board-cleanup.btn.btn-primary {{_ 'schedule-board-cleanup'}}
button.js-schedule-board-archive.btn.btn-warning {{_ 'schedule-board-archive'}}
button.js-schedule-board-backup.btn.btn-info {{_ 'schedule-board-backup'}}
li
h3 {{_ 'cron-jobs'}}
.form-group
label {{_ 'active-cron-jobs'}}
each cronJobs
.job-item
.job-info
.job-name {{name}}
.job-schedule {{schedule}}
.job-description {{description}}
.job-actions
button.js-pause-job.btn.btn-sm.btn-warning(data-job-id="{{_id}}") {{_ 'pause'}}
button.js-delete-job.btn.btn-sm.btn-danger(data-job-id="{{_id}}") {{_ 'delete'}}
.add-job-section
button.js-add-cron-job.btn.btn-success {{_ 'add-cron-job'}}
template(name="cronMigrations")
.cron-migrations
.migration-header
h2
| 🗄️
| {{_ 'database-migrations'}}
.migration-controls
button.btn.btn-primary.js-start-all-migrations
| ▶️
| {{_ 'start-all-migrations'}}
button.btn.btn-warning.js-pause-all-migrations
| ⏸️
| {{_ 'pause-all-migrations'}}
button.btn.btn-danger.js-stop-all-migrations
| ⏹️
| {{_ 'stop-all-migrations'}}
.migration-progress
.progress-overview
.progress-bar
.progress-fill(style="width: {{migrationProgress}}%")
.progress-text {{migrationProgress}}%
.progress-label {{_ 'overall-progress'}}
.current-step
| ⚙️
| {{migrationCurrentStep}}
.migration-status
|
| {{migrationStatus}}
.migration-steps
h3 {{_ '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}}%")
template(name="cronBoardOperations")
.cron-board-operations
.board-operations-header
h2
| 📋
| {{_ 'board-operations'}}
.board-operations-controls
button.btn.btn-success.js-refresh-board-operations
| 🔄
| {{_ 'refresh'}}
button.btn.btn-primary.js-start-test-operation
| ▶️
| {{_ 'start-test-operation'}}
button.btn.btn-info.js-force-board-scan
| 🔍
| {{_ 'force-board-scan'}}
.board-operations-stats
.stats-grid
.stat-item
.stat-value {{operationStats.total}}
.stat-label {{_ 'total-operations'}}
.stat-item
.stat-value {{operationStats.running}}
.stat-label {{_ 'running'}}
.stat-item
.stat-value {{operationStats.completed}}
.stat-label {{_ 'completed'}}
.stat-item
.stat-value {{operationStats.error}}
.stat-label {{_ 'errors'}}
.stat-item
.stat-value {{queueStats.pending}}
.stat-label {{_ 'pending'}}
.stat-item
.stat-value {{queueStats.maxConcurrent}}
.stat-label {{_ 'max-concurrent'}}
.stat-item
.stat-value {{boardMigrationStats.unmigratedCount}}
.stat-label {{_ 'unmigrated-boards'}}
.stat-item
.stat-value {{boardMigrationStats.isScanning}}
.stat-label {{_ 'scanning-status'}}
.system-resources
.resource-item
.resource-label {{_ 'cpu-usage'}}
.resource-bar
.resource-fill(style="width: {{systemResources.cpuUsage}}%")
.resource-value {{systemResources.cpuUsage}}%
.resource-item
.resource-label {{_ 'memory-usage'}}
.resource-bar
.resource-fill(style="width: {{systemResources.memoryUsage}}%")
.resource-value {{systemResources.memoryUsage}}%
.resource-item
.resource-label {{_ 'cpu-cores'}}
.resource-value {{systemResources.cpuCores}}
.board-operations-search
.search-box
input.form-control.js-search-board-operations(type="text" placeholder="{{_ 'search-boards-or-operations'}}")
| 🔍.search-icon
.board-operations-list
.operations-header
h3 {{_ 'board-operations'}} ({{pagination.total}})
.pagination-info
| {{_ 'showing'}} {{pagination.start}} - {{pagination.end}} {{_ 'of'}} {{pagination.total}}
.operations-table
table.table.table-striped
thead
tr
th {{_ 'board-id'}}
th {{_ 'operation-type'}}
th {{_ 'status'}}
th {{_ 'progress'}}
th {{_ 'start-time'}}
th {{_ 'duration'}}
th {{_ 'actions'}}
tbody
each boardOperations
tr
td
.board-id {{boardId}}
td
.operation-type {{operationType}}
td
span.status-badge(class="status-{{status}}") {{status}}
td
.progress-container
.progress-bar
.progress-fill(style="width: {{progress}}%")
.progress-text {{progress}}%
td {{formatDateTime startTime}}
td {{formatDuration startTime endTime}}
td
.btn-group
if isRunning
button.btn.btn-sm.btn-warning.js-pause-operation(data-operation="{{id}}")
| ⏸️
else
button.btn.btn-sm.btn-success.js-resume-operation(data-operation="{{id}}")
| ▶️
button.btn.btn-sm.btn-danger.js-stop-operation(data-operation="{{id}}")
| ⏹️
button.btn.btn-sm.btn-info.js-view-details(data-operation="{{id}}")
|
.pagination
if pagination.hasPrev
button.btn.btn-sm.btn-default.js-prev-page
| ◀️
| {{_ 'previous'}}
.page-info
| {{_ 'page'}} {{pagination.page}} {{_ 'of'}} {{pagination.totalPages}}
if pagination.hasNext
button.btn.btn-sm.btn-default.js-next-page
| {{_ 'next'}}
| ▶️
template(name="cronJobs")
.cron-jobs
.jobs-header
h2
| ⏰
| {{_ 'cron-jobs'}}
.jobs-controls
button.btn.btn-success.js-refresh-jobs
| 🔄
| {{_ 'refresh'}}
.jobs-list
table.table.table-striped
thead
tr
th {{_ 'job-name'}}
th {{_ 'schedule'}}
th {{_ 'status'}}
th {{_ 'last-run'}}
th {{_ 'next-run'}}
th {{_ 'actions'}}
tbody
each cronJobs
tr
td {{name}}
td {{schedule}}
td
span.status-badge(class="status-{{status}}") {{status}}
td {{formatDate lastRun}}
td {{formatDate nextRun}}
td
.btn-group
if isRunning
button.btn.btn-sm.btn-warning.js-pause-job(data-job="{{name}}")
| ⏸️
else
button.btn.btn-sm.btn-success.js-start-job(data-job="{{name}}")
| ▶️
button.btn.btn-sm.btn-danger.js-stop-job(data-job="{{name}}")
| ⏹️
button.btn.btn-sm.btn-danger.js-remove-job(data-job="{{name}}")
| 🗑️
template(name="cronAddJob")
.cron-add-job
.add-job-header
h2
|
| {{_ 'add-cron-job'}}
.add-job-form
form.js-add-cron-job-form
.form-group
label(for="job-name") {{_ 'job-name'}}
input.form-control#job-name(type="text" name="name" required)
.form-group
label(for="job-description") {{_ 'job-description'}}
textarea.form-control#job-description(name="description" rows="3")
.form-group
label(for="job-schedule") {{_ 'schedule'}}
select.form-control#job-schedule(name="schedule")
option(value="every 1 minute") {{_ 'every-1-minute'}}
option(value="every 5 minutes") {{_ 'every-5-minutes'}}
option(value="every 10 minutes") {{_ 'every-10-minutes'}}
option(value="every 30 minutes") {{_ 'every-30-minutes'}}
option(value="every 1 hour") {{_ 'every-1-hour'}}
option(value="every 6 hours") {{_ 'every-6-hours'}}
option(value="every 1 day") {{_ 'every-1-day'}}
option(value="once") {{_ 'run-once'}}
.form-group
label(for="job-weight") {{_ 'weight'}}
input.form-control#job-weight(type="number" name="weight" value="1" min="1" max="10")
.form-actions
button.btn.btn-primary(type="submit")
|
| {{_ 'add-job'}}
button.btn.btn-default.js-cancel-add-job
| ❌
| {{_ 'cancel'}}

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