Compare commits

...

296 commits
v8.01 ... main

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

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

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

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

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

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

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

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

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

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

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

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

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

Fixes #6008.

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

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

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

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

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

Fixes #5994
2025-11-05 18:44:48 +02:00
Lauri Ojansivu
91a0aa7387 Updated ChangeLog. 2025-11-05 17:08:52 +02:00
Lauri Ojansivu
fbd6b920ef Updated ChangeLog. 2025-11-05 17:08:10 +02:00
Lauri Ojansivu
1b25d1d572 Moved migrations from opening board to right sidebar / Migrations.
Thanks to xet7 !
2025-11-05 17:06:26 +02:00
Lauri Ojansivu
e93e72234c Updated ChangeLog. 2025-11-05 16:38:10 +02:00
Lauri Ojansivu
15d9b0ae3a Updated ChangeLog. 2025-11-05 16:38:03 +02:00
Lauri Ojansivu
550d87ac6c Fix 8.16: Switching Board View fails with 403 error.
Thanks to xet7 !
2025-11-05 16:35:29 +02:00
Lauri Ojansivu
f8e576e890 Try to fix Snap.
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
Thanks to xet7 !
2025-11-02 22:23:16 +02:00
Lauri Ojansivu
fb8ef4d978 Try to fix Snap.
Thanks to xet7 !
2025-11-02 21:36:17 +02:00
Lauri Ojansivu
5127e87898 Try to fix Snap.
Thanks to xet7 !
2025-11-02 21:33:06 +02:00
Lauri Ojansivu
3f2d4444e4 Try to fix Snap. Part 2.
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
Thanks to xet7 !
2025-11-02 16:14:45 +02:00
Lauri Ojansivu
9c7badb0eb Merge branch 'main' of github.com:wekan/wekan 2025-11-02 16:04:16 +02:00
Lauri Ojansivu
9d9f77a731 Try to fix Snap.
Thanks to xet7 !
2025-11-02 16:02:53 +02:00
Lauri Ojansivu
c400ce74b1 v8.16 2025-11-02 12:09:27 +02:00
Lauri Ojansivu
c2e20ee4a3 Updated ChangeLog.
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
2025-11-02 11:43:33 +02:00
Lauri Ojansivu
ccd9034339 Fix SECURITY ISSUE 5: Attachment API uses bearer value as userId and DoS (Low).
Thanks to Siam Thanat Hack (STH) and xet7 !
2025-11-02 11:42:07 +02:00
Lauri Ojansivu
0a1a075f31 Fix SECURITY ISSUE 4: Members can forge others’ votes (Low). Bonus: Similar fixes to planning poker too done by xet7.
Thanks to Siam Thanat Hack (STH) and xet7 !
2025-11-02 11:12:41 +02:00
Lauri Ojansivu
4aaeec9515 Updated ChangeLog. 2025-11-02 10:17:33 +02:00
Lauri Ojansivu
ea310d7508 Fix SECURITY ISSUE 3: Unauthenticated (or any) user can update board sort.
Thanks to Siam Thanat Hack (STH) !
2025-11-02 10:13:45 +02:00
Lauri Ojansivu
0a2e6a0c38 Updated ChangeLog. 2025-11-02 09:20:28 +02:00
Lauri Ojansivu
f26d582018 Fix SECURITY ISSUE 2: Access to boards of any Orgs/Teams, and avatar permissions.
Thanks to Siam Thanat Hack (STH) !
2025-11-02 09:11:50 +02:00
Lauri Ojansivu
e9a727301d Fix SECURITY ISSUE 1: File Attachments enables stored XSS (High).
Thanks to Siam Thanat Hack (STH) !
2025-11-02 08:36:29 +02:00
Lauri Ojansivu
d64d2f9c42 Updated translations. 2025-11-02 07:30:24 +02:00
Lauri Ojansivu
5c0d122e84 Updated funding 2025-11-02 06:15:08 +02:00
Lauri Ojansivu
5079c853a7 Updated translations.
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
2025-10-29 02:58:00 +02:00
Lauri Ojansivu
b039ba12a2
Merge pull request #5984 from wekan/dependabot/github_actions/actions/download-artifact-6
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
Bump actions/download-artifact from 5 to 6
2025-10-28 06:00:19 +02:00
Lauri Ojansivu
3323ac6ac1
Merge pull request #5983 from wekan/dependabot/github_actions/actions/upload-artifact-5
Bump actions/upload-artifact from 4 to 5
2025-10-28 05:59:59 +02:00
dependabot[bot]
3204311ac1
Bump actions/download-artifact from 5 to 6
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 23:12:09 +00:00
dependabot[bot]
0fc2ad97cd
Bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 20:32:10 +00:00
Lauri Ojansivu
30620d0ca4 Some migrations and mobile fixes.
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
Thanks to xet7 !
2025-10-25 21:09:07 +03:00
Lauri Ojansivu
bccc22c5fe Updated ChangeLog. 2025-10-25 19:25:04 +03:00
Lauri Ojansivu
ecf2418347 Fix changing swimlane color to not reload webpage.
Thanks to xet7 !
2025-10-25 19:23:35 +03:00
Lauri Ojansivu
0c99cb3103 Updated ChangeLog. 2025-10-25 19:19:35 +03:00
Lauri Ojansivu
034dc08269 Disabled migrations that happen when opening board. Defaulting to per-swimlane lists and drag drop list to same or different swimlane.
Thanks to xet7 !
2025-10-25 19:17:09 +03:00
Lauri Ojansivu
d1a51b42f6 Updated translations.
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
2025-10-24 18:43:21 +03:00
Lauri Ojansivu
92bfbb2d0c Updated ChangeLog.
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
2025-10-23 05:54:25 +03:00
Lauri Ojansivu
91b846e2cd List menu / More / Delete duplicate lists that do not have any cards.
Thanks to xet7 !
2025-10-23 05:50:43 +03:00
Lauri Ojansivu
7fe7fb4c15 v8.15 2025-10-23 04:41:34 +03:00
Lauri Ojansivu
0cebd8aa4d Fix drag lists did not work. Part 2.
Thanks to xet7 !
2025-10-23 04:35:33 +03:00
Lauri Ojansivu
8662c96d1c Fix drag lists did not work.
Thanks to xet7 !
2025-10-23 04:33:34 +03:00
Lauri Ojansivu
0cbc9402f3 v8.14 2025-10-23 04:09:14 +03:00
Lauri Ojansivu
940df02456 Updated translations. 2025-10-23 04:08:49 +03:00
Lauri Ojansivu
b4b598f542 Fix board reloading page every second.
Thanks to xet7 !
2025-10-23 04:03:52 +03:00
Lauri Ojansivu
ef19c35b5a v8.12 2025-10-23 03:29:23 +03:00
Lauri Ojansivu
fc98120269 Updated translations. 2025-10-23 03:24:28 +03:00
Lauri Ojansivu
b8a3d6deaf Updated ChangeLog. 2025-10-23 03:17:13 +03:00
Lauri Ojansivu
45537ede87 Fix UI issues of Right Sidebar / Subtasks Settings and Card Settings.
Thanks to xet7 !

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fixes #5946,
fixes #5436,
fixes #2936,
fixes #1926,
fixes #300,
fixes #142
2025-10-19 10:57:36 +03:00
Lauri Ojansivu
cea414b589 Updated translations.
Some checks failed
Docker / build (push) Has been cancelled
Docker Image CI / build (push) Has been cancelled
Release Charts / release (push) Has been cancelled
Test suite / Meteor tests (push) Has been cancelled
Test suite / Coverage report (push) Has been cancelled
2025-10-17 21:55:49 +03:00
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 63776 additions and 14849 deletions

1
.github/FUNDING.yml vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -52,8 +52,8 @@ ongoworks:speakingurl
raix:handlebar-helpers raix:handlebar-helpers
http@2.0.0! # force new http package http@2.0.0! # force new http package
# Datepicker # Datepicker (disabled - using native HTML inputs)
wekan-bootstrap-datepicker # wekan-bootstrap-datepicker
# UI components # UI components
ostrio:i18n ostrio:i18n
@ -93,4 +93,4 @@ ejson@1.1.3
logging@1.3.3 logging@1.3.3
wekan-fullcalendar wekan-fullcalendar
momentjs:moment@2.29.3 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-lockout@1.0.0
wekan-accounts-oidc@1.0.10 wekan-accounts-oidc@1.0.10
wekan-accounts-sandstorm@0.8.0 wekan-accounts-sandstorm@0.8.0
wekan-bootstrap-datepicker@1.10.0
wekan-fontawesome@6.4.2
wekan-fullcalendar@3.10.5 wekan-fullcalendar@3.10.5
wekan-ldap@0.0.2 wekan-ldap@0.0.2
wekan-markdown@1.0.9 wekan-markdown@1.0.9

View file

@ -19,6 +19,355 @@ Fixing other platforms In Progress.
[Upgrade WeKan](https://wekan.fi/upgrade/) [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 # v8.01 2025-10-11 WeKan ® release
This release adds the following new features: 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). - [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. 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 1](https://github.com/wekan/wekan/commit/3e9481c5bd2c02ba501bd0a6ef1d1e6ce82bb1d9),
[Part 2](https://github.com/wekan/wekan/commit/cdd7d69c660d0b6ac06b7b75d4f59985b8a9322a). [Part 2](https://github.com/wekan/wekan/commit/cdd7d69c660d0b6ac06b7b75d4f59985b8a9322a).
Thanks to xet7. 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. # 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 #rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
#mv /home/wekan/app_build/bundle /build #mv /home/wekan/app_build/bundle /build
wget "https://github.com/wekan/wekan/releases/download/v8.01/wekan-8.01-amd64.zip" wget "https://github.com/wekan/wekan/releases/download/v8.17/wekan-8.17-amd64.zip"
unzip wekan-8.01-amd64.zip unzip wekan-8.17-amd64.zip
rm wekan-8.01-amd64.zip rm wekan-8.17-amd64.zip
mv /home/wekan/app/bundle /build mv /home/wekan/app/bundle /build
# Put back the original tar # 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 ## Responsible Security Disclosure
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.
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? ## 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 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 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? ## 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 - 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. - 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. - 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 ## XSS
@ -172,6 +175,57 @@ Meteor.startup(() => {
- https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312 - https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
- https://wekan.github.io/hall-of-fame/filebleed/ - 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 ## Brute force login protection
- https://github.com/wekan/wekan/commit/23e5e1e3bd081699ce39ce5887db7e612616014d - 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. - 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. 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 We welcome all fixes to improve security by email to security@wekan.fi
## 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).

View file

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

View file

@ -4,3 +4,61 @@ if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/pwa-service-worker.js'); 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.) */ /* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
.board-wrapper.mobile-view { .board-wrapper.mobile-view {
width: 100% !important; width: 100vw !important;
min-width: 100% !important; max-width: 100vw !important;
min-width: 100vw !important;
left: 0 !important; left: 0 !important;
right: 0 !important; right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
} }
.board-wrapper.mobile-view .board-canvas { .board-wrapper.mobile-view .board-canvas {
width: 100% !important; width: 100vw !important;
min-width: 100% !important; max-width: 100vw !important;
min-width: 100vw !important;
left: 0 !important; left: 0 !important;
right: 0 !important; right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
} }
.board-wrapper.mobile-view .board-canvas.mobile-view .swimlane { .board-wrapper.mobile-view .board-canvas.mobile-view .swimlane {
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: flex; display: block !important;
flex-direction: column; flex-direction: column;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-x: hidden; overflow-x: hidden !important;
overflow-y: auto; overflow-y: auto;
width: 100%; width: 100vw !important;
min-width: 100%; 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 { .board-wrapper {
width: 100% !important; width: 100vw !important;
min-width: 100% !important; max-width: 100vw !important;
min-width: 100vw !important;
left: 0 !important; left: 0 !important;
right: 0 !important; right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
} }
.board-wrapper .board-canvas { .board-wrapper .board-canvas {
width: 100% !important; width: 100vw !important;
min-width: 100% !important; max-width: 100vw !important;
min-width: 100vw !important;
left: 0 !important; left: 0 !important;
right: 0 !important; right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
} }
.board-wrapper .board-canvas .swimlane { .board-wrapper .board-canvas .swimlane {
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: flex; display: block !important;
flex-direction: column; flex-direction: column;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-x: hidden; overflow-x: hidden !important;
overflow-y: auto; overflow-y: auto;
width: 100%; width: 100vw !important;
min-width: 100%; max-width: 100vw !important;
min-width: 100vw !important;
} }
} }
.calendar-event-green { .calendar-event-green {
@ -496,3 +511,10 @@
font-size: 25px; font-size: 25px;
cursor: pointer; 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") template(name="board")
if isBoardReady.get
if isMigrating.get
+migrationProgress
else if isConverting.get
+boardConversionProgress
else if isBoardReady.get
if currentBoard if currentBoard
if onlyShowCurrentCard if onlyShowCurrentCard
+cardDetails(currentCard) +cardDetails(currentCard)
@ -16,6 +21,10 @@ template(name="boardBody")
if notDisplayThisBoard if notDisplayThisBoard
| {{_ 'tableVisibilityMode-allowPrivateOnly'}} | {{_ 'tableVisibilityMode-allowPrivateOnly'}}
else 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-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
.board-canvas.js-swimlanes( .board-canvas.js-swimlanes(
class="{{#if hasSwimlanes}}dragscroll{{/if}}" class="{{#if hasSwimlanes}}dragscroll{{/if}}"
@ -34,15 +43,19 @@ template(name="boardBody")
each currentBoard.swimlanes each currentBoard.swimlanes
+swimlane(this) +swimlane(this)
else else
a.js-empty-board-add-swimlane(title="{{_ 'add-swimlane'}}") // Fallback: If no swimlanes exist, show lists instead of empty message
h1.big-message.quiet +listsGroup(currentBoard)
| {{_ 'add-swimlane'}} +
else if isViewLists else if isViewLists
+listsGroup(currentBoard) +listsGroup(currentBoard)
else if isViewCalendar else if isViewCalendar
+calendarView +calendarView
else else
+listsGroup(currentBoard) // Default view - show swimlanes if they exist, otherwise show lists
if hasSwimlanes
each currentBoard.swimlanes
+swimlane(this)
else
+listsGroup(currentBoard)
+sidebar +sidebar
template(name="calendarView") template(name="calendarView")

View file

@ -1,6 +1,12 @@
import { ReactiveCache } from '/imports/reactiveCache'; import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n'; import { TAPi18n } from '/imports/i18n';
import dragscroll from '@wekanteam/dragscroll'; 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 subManager = new SubsManager();
const { calculateIndex } = Utils; const { calculateIndex } = Utils;
@ -9,6 +15,11 @@ const swimlaneWhileSortingHeight = 150;
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
onCreated() { onCreated() {
this.isBoardReady = new ReactiveVar(false); 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: // 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 // https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
@ -17,22 +28,512 @@ BlazeComponent.extendComponent({
this.autorun(() => { this.autorun(() => {
const currentBoardId = Session.get('currentBoard'); const currentBoardId = Session.get('currentBoard');
if (!currentBoardId) return; if (!currentBoardId) return;
const handle = subManager.subscribe('board', currentBoardId, false); const handle = subManager.subscribe('board', currentBoardId, false);
Tracker.nonreactive(() => {
Tracker.autorun(() => { // Use a separate autorun for subscription ready state to avoid reactive loops
this.isBoardReady.set(handle.ready()); 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() { onlyShowCurrentCard() {
return Utils.isMiniScreen() && Utils.getCurrentCardId(true); const isMiniScreen = Utils.isMiniScreen();
const currentCardId = Utils.getCurrentCardId(true);
return isMiniScreen && currentCardId;
}, },
goHome() { goHome() {
FlowRouter.go('home'); FlowRouter.go('home');
}, },
isConverting() {
return this.isConverting.get();
},
isMigrating() {
return this.isMigrating.get();
},
isBoardReady() {
return this.isBoardReady.get();
},
currentBoard() {
return Utils.getCurrentBoard();
},
}).register('board'); }).register('board');
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
@ -43,36 +544,51 @@ BlazeComponent.extendComponent({
this._isDragging = false; this._isDragging = false;
// Used to set the overlay // Used to set the overlay
this.mouseHasEnterCardDetails = false; this.mouseHasEnterCardDetails = false;
this._sortFieldsFixed = new Set(); // Track which boards have had sort fields fixed
// fix swimlanes sort field if there are null values // fix swimlanes sort field if there are null values
const currentBoardData = Utils.getCurrentBoard(); const currentBoardData = Utils.getCurrentBoard();
const nullSortSwimlanes = currentBoardData.nullSortSwimlanes(); if (currentBoardData && Swimlanes) {
if (nullSortSwimlanes.length > 0) { const boardId = currentBoardData._id;
const swimlanes = currentBoardData.swimlanes(); // Only fix sort fields once per board to prevent reactive loops
let count = 0; if (!this._sortFieldsFixed.has(`swimlanes-${boardId}`)) {
swimlanes.forEach(s => { const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
Swimlanes.update(s._id, { if (nullSortSwimlanes.length > 0) {
$set: { const swimlanes = currentBoardData.swimlanes();
sort: count, let count = 0;
}, swimlanes.forEach(s => {
}); Swimlanes.update(s._id, {
count += 1; $set: {
}); sort: count,
},
});
count += 1;
});
}
this._sortFieldsFixed.add(`swimlanes-${boardId}`);
}
} }
// fix lists sort field if there are null values // fix lists sort field if there are null values
const nullSortLists = currentBoardData.nullSortLists(); if (currentBoardData && Lists) {
if (nullSortLists.length > 0) { const boardId = currentBoardData._id;
const lists = currentBoardData.lists(); // Only fix sort fields once per board to prevent reactive loops
let count = 0; if (!this._sortFieldsFixed.has(`lists-${boardId}`)) {
lists.forEach(l => { const nullSortLists = currentBoardData.nullSortLists();
Lists.update(l._id, { if (nullSortLists.length > 0) {
$set: { const lists = currentBoardData.lists();
sort: count, let count = 0;
}, lists.forEach(l => {
}); Lists.update(l._id, {
count += 1; $set: {
}); sort: count,
},
});
count += 1;
});
}
this._sortFieldsFixed.add(`lists-${boardId}`);
}
} }
}, },
onRendered() { 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) { const popupObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) { mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) { 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); setTimeout(function() { focusFirstInteractive(node); }, 10);
} }
}); });
@ -380,22 +901,20 @@ BlazeComponent.extendComponent({
// Always reset dragscroll on view switch // Always reset dragscroll on view switch
dragscroll.reset(); dragscroll.reset();
if (Utils.isTouchScreenOrShowDesktopDragHandles()) { if ($swimlanesDom.data('uiSortable') || $swimlanesDom.data('sortable')) {
$swimlanesDom.sortable({ if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
handle: '.js-swimlane-header-handle', $swimlanesDom.sortable('option', 'handle', '.js-swimlane-header-handle');
}); } else {
} else { $swimlanesDom.sortable('option', 'handle', '.swimlane-header');
$swimlanesDom.sortable({ }
handle: '.swimlane-header',
});
}
// Disable drag-dropping if the current user is not a board member // Disable drag-dropping if the current user is not a board member
$swimlanesDom.sortable( $swimlanesDom.sortable(
'option', 'option',
'disabled', 'disabled',
!ReactiveCache.getCurrentUser()?.isBoardAdmin(), !ReactiveCache.getCurrentUser()?.isBoardAdmin(),
); );
}
}); });
// If there is no data in the board (ie, no lists) we autofocus the list // If there is no data in the board (ie, no lists) we autofocus the list
@ -412,51 +931,122 @@ BlazeComponent.extendComponent({
notDisplayThisBoard() { notDisplayThisBoard() {
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly'); let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
let currentBoard = Utils.getCurrentBoard(); let currentBoard = Utils.getCurrentBoard();
if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard.permission == 'public') { return allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard && currentBoard.permission == 'public';
return true;
}
return false;
}, },
isViewSwimlanes() { isViewSwimlanes() {
const currentUser = ReactiveCache.getCurrentUser(); const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) { if (currentUser) {
return (currentUser.profile || {}).boardView === 'board-view-swimlanes'; boardView = (currentUser.profile || {}).boardView;
} else { } else {
return ( boardView = window.localStorage.getItem('boardView');
window.localStorage.getItem('boardView') === 'board-view-swimlanes'
);
} }
},
// If no board view is set, default to swimlanes
hasSwimlanes() { if (!boardView) {
return Utils.getCurrentBoard().swimlanes().length > 0; boardView = 'board-view-swimlanes';
}
return boardView === 'board-view-swimlanes';
}, },
isViewLists() { isViewLists() {
const currentUser = ReactiveCache.getCurrentUser(); const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) { if (currentUser) {
return (currentUser.profile || {}).boardView === 'board-view-lists'; boardView = (currentUser.profile || {}).boardView;
} else { } else {
return window.localStorage.getItem('boardView') === 'board-view-lists'; boardView = window.localStorage.getItem('boardView');
} }
return boardView === 'board-view-lists';
}, },
isViewCalendar() { isViewCalendar() {
const currentUser = ReactiveCache.getCurrentUser(); const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) { if (currentUser) {
return (currentUser.profile || {}).boardView === 'board-view-cal'; boardView = (currentUser.profile || {}).boardView;
} else { } 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() { isVerticalScrollbars() {
const user = ReactiveCache.getCurrentUser(); const user = ReactiveCache.getCurrentUser();
return user && user.isVerticalScrollbars(); 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() { openNewListForm() {
if (this.isViewSwimlanes()) { if (this.isViewSwimlanes()) {
// The form had been removed in 416b17062e57f215206e93a85b02ef9eb1ab4902 // The form had been removed in 416b17062e57f215206e93a85b02ef9eb1ab4902
@ -480,6 +1070,31 @@ BlazeComponent.extendComponent({
} }
}, },
'click .js-empty-board-add-swimlane': Popup.open('swimlaneAdd'), '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]; const firstSwimlane = currentBoard.swimlanes()[0];
Meteor.call('createCardWithDueDate', currentBoard._id, firstList._id, myTitle, startDate.toDate(), firstSwimlane._id, function(error, result) { Meteor.call('createCardWithDueDate', currentBoard._id, firstList._id, myTitle, startDate.toDate(), firstSwimlane._id, function(error, result) {
if (error) { if (error) {
console.log(error); if (process.env.DEBUG === 'true') {
console.log(error);
}
} else { } else {
console.log("Card Created", result); if (process.env.DEBUG === 'true') {
console.log("Card Created", result);
}
} }
}); });
closeModal(); closeModal();

View file

@ -505,73 +505,73 @@
flex-wrap: nowrap !important; flex-wrap: nowrap !important;
align-items: stretch !important; align-items: stretch !important;
justify-content: flex-start !important; justify-content: flex-start !important;
width: 100% !important; width: 100vw !important;
max-width: 100% !important; max-width: 100vw !important;
min-width: 100% !important; min-width: 100vw !important;
overflow-x: hidden !important; overflow-x: hidden !important;
overflow-y: auto !important; overflow-y: auto !important;
} }
.mobile-mode .swimlane { .mobile-mode .swimlane {
display: block !important; display: block !important;
width: 100% !important; width: 100vw !important;
max-width: 100% !important; max-width: 100vw !important;
min-width: 100% !important; min-width: 100vw !important;
margin: 0 0 2rem 0 !important; margin: 0 0 2rem 0 !important;
padding: 0 !important; padding: 0 !important;
float: none !important; float: none !important;
clear: both !important; clear: both !important;
} }
.mobile-mode .swimlane .swimlane-header { .mobile-mode .swimlane .swimlane-header {
display: block !important; display: block !important;
width: 100% !important; width: 100vw !important;
max-width: 100% !important; max-width: 100vw !important;
min-width: 100% !important; min-width: 100vw !important;
margin: 0 0 1rem 0 !important; margin: 0 0 1rem 0 !important;
padding: 1rem !important; padding: 1rem !important;
font-size: clamp(18px, 2.5vw, 32px) !important; font-size: clamp(18px, 2.5vw, 32px) !important;
font-weight: bold !important; font-weight: bold !important;
border-bottom: 2px solid #ccc !important; border-bottom: 2px solid #ccc !important;
} }
.mobile-mode .swimlane .lists { .mobile-mode .swimlane .lists {
display: block !important; display: block !important;
width: 100% !important; width: 100vw !important;
max-width: 100% !important; max-width: 100vw !important;
min-width: 100% !important; min-width: 100vw !important;
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 !important;
flex-direction: column !important; flex-direction: column !important;
flex-wrap: nowrap !important; flex-wrap: nowrap !important;
align-items: stretch !important; align-items: stretch !important;
justify-content: flex-start !important; justify-content: flex-start !important;
} }
.mobile-mode .list { .mobile-mode .list {
display: block !important; display: block !important;
width: 100% !important; width: 100vw !important;
max-width: 100% !important; max-width: 100vw !important;
min-width: 100% !important; min-width: 100vw !important;
margin: 0 0 2rem 0 !important; margin: 0 0 2rem 0 !important;
padding: 0 !important; padding: 0 !important;
float: none !important; float: none !important;
clear: both !important; clear: both !important;
border-left: none !important; border-left: none !important;
border-right: none !important; border-right: none !important;
border-top: none !important; border-top: none !important;
border-bottom: 2px solid #ccc !important; border-bottom: 2px solid #ccc !important;
flex: none !important; flex: none !important;
flex-basis: auto !important; flex-basis: auto !important;
flex-grow: 0 !important; flex-grow: 0 !important;
flex-shrink: 0 !important; flex-shrink: 0 !important;
position: static !important; position: static !important;
left: auto !important; left: auto !important;
right: auto !important; right: auto !important;
top: auto !important; top: auto !important;
bottom: auto !important; bottom: auto !important;
transform: none !important; transform: none !important;
} }
.mobile-mode .list:first-child { .mobile-mode .list:first-child {
margin-left: 0 !important; margin-left: 0 !important;
@ -667,9 +667,9 @@
flex-wrap: nowrap !important; flex-wrap: nowrap !important;
align-items: stretch !important; align-items: stretch !important;
justify-content: flex-start !important; justify-content: flex-start !important;
width: 100% !important; width: 100vw !important;
max-width: 100% !important; max-width: 100vw !important;
min-width: 100% !important; min-width: 100vw !important;
overflow-x: hidden !important; overflow-x: hidden !important;
overflow-y: auto !important; overflow-y: auto !important;
} }

View file

@ -14,41 +14,41 @@ template(name="boardHeaderBar")
with currentBoard with currentBoard
if currentUser.isBoardAdmin if currentUser.isBoardAdmin
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title) 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}}" a.board-header-btn(
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}}") class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}") title="{{_ currentBoard.permission}}")
if showStarCounter | {{#if currentBoard.isPublic}}🌐{{else}}🔒{{/if}}
span span {{_ currentBoard.permission}}
= currentBoard.stars
a.board-header-btn( a.board-header-btn.js-watch-board(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}" title="{{_ watchLevel }}")
title="{{_ currentBoard.permission}}") if $eq watchLevel "watching"
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}") | 👁️
span {{_ currentBoard.permission}} if $eq watchLevel "tracking"
| 🔔
a.board-header-btn.js-watch-board( if $eq watchLevel "muted"
title="{{_ watchLevel }}") | 🔕
if $eq watchLevel "watching" span {{_ watchLevel}}
i.fa.fa-eye a.board-header-btn.js-star-board(title="{{_ 'star-board'}}")
if $eq watchLevel "tracking" if isStarred
i.fa.fa-bell | ⭐
if $eq watchLevel "muted" else
i.fa.fa-bell-slash | ☆
span {{_ watchLevel}} if showStarCounter
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}") span.board-star-counter {{currentBoard.stars}}
i.fa.fa-sort a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}} | {{sortCardsIcon}}
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
if isSortActive if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}") a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin | ❌
else else
a.board-header-btn.js-log-in( a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}") title="{{_ 'log-in'}}")
i.fa.fa-sign-in | 🚪
span {{_ 'log-in'}} span {{_ 'log-in'}}
.board-header-btns.center .board-header-btns.center
@ -59,40 +59,41 @@ template(name="boardHeaderBar")
if currentUser if currentUser
with currentBoard with currentBoard
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title) 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}}" a.board-header-btn(
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}") class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}") title="{{_ currentBoard.permission}}")
| {{#if currentBoard.isPublic}}🌐{{else}}🔒{{/if}}
a.board-header-btn( a.board-header-btn.js-watch-board(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}" title="{{_ watchLevel }}")
title="{{_ currentBoard.permission}}") if $eq watchLevel "watching"
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}") | 👁️
if $eq watchLevel "tracking"
a.board-header-btn.js-watch-board( | 🔔
title="{{_ watchLevel }}") if $eq watchLevel "muted"
if $eq watchLevel "watching" | 🔕
i.fa.fa-eye a.board-header-btn.js-star-board(title="{{_ 'star-board'}}")
if $eq watchLevel "tracking" if isStarred
i.fa.fa-bell | ⭐
if $eq watchLevel "muted" else
i.fa.fa-bell-slash | ☆
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}") a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort | {{sortCardsIcon}}
if isSortActive if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}") a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin | ❌
else else
a.board-header-btn.js-log-in( a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}") title="{{_ 'log-in'}}")
i.fa.fa-sign-in | 🚪
if isSandstorm if isSandstorm
if currentUser if currentUser
a.board-header-btn.js-open-archived-board a.board-header-btn.js-open-archived-board
i.fa.fa-archive | 📦
//if showSort //if showSort
// a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}") // 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( a.board-header-btn.js-open-filter-view(
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}" title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
class="{{#if Filter.isActive}}emphasis{{/if}}") class="{{#if Filter.isActive}}emphasis{{/if}}")
i.fa.fa-filter | 🔽
if Filter.isActive if Filter.isActive
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}") 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'}}") a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
i.fa.fa-search | 🔍
unless currentBoard.isTemplatesBoard unless currentBoard.isTemplatesBoard
a.board-header-btn.js-toggle-board-view( a.board-header-btn.js-toggle-board-view(
title="{{_ 'board-view'}}") title="{{_ 'board-view'}}")
i.fa.fa-caret-down | ▼
if $eq boardView 'board-view-swimlanes' if $eq boardView 'board-view-swimlanes'
i.fa.fa-th-large | 🏊
if $eq boardView 'board-view-lists' if $eq boardView 'board-view-lists'
i.fa.fa-trello | 📋
if $eq boardView 'board-view-cal' if $eq boardView 'board-view-cal'
i.fa.fa-calendar | 📅
if canModifyBoard if canModifyBoard
a.board-header-btn.js-multiselection-activate( a.board-header-btn.js-multiselection-activate(
title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}" title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if MultiSelection.isActive}}emphasis{{/if}}") class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
i.fa.fa-check-square-o | ☑️
if MultiSelection.isActive if MultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}") a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin | ❌
.separator .separator
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}") a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
i.fa.fa-navicon | ☰
template(name="boardVisibilityList") template(name="boardVisibilityList")
ul.pop-over-list ul.pop-over-list
li li
with "private" with "private"
a.js-select-visibility a.js-select-visibility
i.fa.fa-lock.colorful | 🔒
| {{_ 'private'}} | {{_ 'private'}}
if visibilityCheck if visibilityCheck
i.fa.fa-check | ✅
span.sub-name {{_ 'private-desc'}} span.sub-name {{_ 'private-desc'}}
if notAllowPrivateVisibilityOnly if notAllowPrivateVisibilityOnly
li li
with "public" with "public"
a.js-select-visibility a.js-select-visibility
i.fa.fa-globe.colorful | 🌐
| {{_ 'public'}} | {{_ 'public'}}
if visibilityCheck if visibilityCheck
i.fa.fa-check | ✅
span.sub-name {{_ 'public-desc'}} span.sub-name {{_ 'public-desc'}}
template(name="boardChangeVisibilityPopup") template(name="boardChangeVisibilityPopup")
@ -162,26 +163,26 @@ template(name="boardChangeWatchPopup")
li li
with "watching" with "watching"
a.js-select-watch a.js-select-watch
i.fa.fa-eye.colorful | 👁️
| {{_ 'watching'}} | {{_ 'watching'}}
if watchCheck if watchCheck
i.fa.fa-check | ✅
span.sub-name {{_ 'watching-info'}} span.sub-name {{_ 'watching-info'}}
li li
with "tracking" with "tracking"
a.js-select-watch a.js-select-watch
i.fa.fa-bell.colorful | 🔔
| {{_ 'tracking'}} | {{_ 'tracking'}}
if watchCheck if watchCheck
i.fa.fa-check | ✅
span.sub-name {{_ 'tracking-info'}} span.sub-name {{_ 'tracking-info'}}
li li
with "muted" with "muted"
a.js-select-watch a.js-select-watch
i.fa.fa-bell-slash.colorful | 🔕
| {{_ 'muted'}} | {{_ 'muted'}}
if watchCheck if watchCheck
i.fa.fa-check | ✅
span.sub-name {{_ 'muted-info'}} span.sub-name {{_ 'muted-info'}}
template(name="boardChangeViewPopup") template(name="boardChangeViewPopup")
@ -189,24 +190,24 @@ template(name="boardChangeViewPopup")
li li
with "board-view-swimlanes" with "board-view-swimlanes"
a.js-open-swimlanes-view a.js-open-swimlanes-view
i.fa.fa-th-large.colorful | 🏊
| {{_ 'board-view-swimlanes'}} | {{_ 'board-view-swimlanes'}}
if $eq Utils.boardView "board-view-swimlanes" if $eq Utils.boardView "board-view-swimlanes"
i.fa.fa-check | ✅
li li
with "board-view-lists" with "board-view-lists"
a.js-open-lists-view a.js-open-lists-view
i.fa.fa-trello.colorful | 📋
| {{_ 'board-view-lists'}} | {{_ 'board-view-lists'}}
if $eq Utils.boardView "board-view-lists" if $eq Utils.boardView "board-view-lists"
i.fa.fa-check | ✅
li li
with "board-view-cal" with "board-view-cal"
a.js-open-cal-view a.js-open-cal-view
i.fa.fa-calendar.colorful | 📅
| {{_ 'board-view-cal'}} | {{_ 'board-view-cal'}}
if $eq Utils.boardView "board-view-cal" if $eq Utils.boardView "board-view-cal"
i.fa.fa-check | ✅
template(name="createBoard") template(name="createBoard")
form form
@ -218,11 +219,70 @@ template(name="createBoard")
else else
p.quiet p.quiet
if $eq visibility.get 'public' if $eq visibility.get 'public'
span.fa.fa-globe.colorful span 🌐
= " " = " "
| {{{_ 'board-public-info'}}} | {{{_ 'board-public-info'}}}
else 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'}}} | {{{_ 'board-private-info'}}}
a.js-change-visibility {{_ 'change'}}. a.js-change-visibility {{_ 'change'}}.
@ -246,10 +306,10 @@ template(name="createBoard")
// li // li
// a.js-sort-by(name="{{value.name}}") // a.js-sort-by(name="{{value.name}}")
// if $eq sortby value.name // if $eq sortby value.name
// i(class="fa {{Direction}}") // | {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
// | {{_ value.label }}{{_ value.shortLabel}} // | {{_ value.label }}{{_ value.shortLabel}}
// if $eq sortby value.name // if $eq sortby value.name
// i(class="fa fa-check") // | ✅
template(name="boardChangeTitlePopup") template(name="boardChangeTitlePopup")
form form
@ -269,14 +329,22 @@ template(name="boardCreateRulePopup")
template(name="cardsSortPopup") template(name="cardsSortPopup")
ul.pop-over-list ul.pop-over-list
li li
a.js-sort-due {{_ 'due-date'}} a.js-sort-due
| 📅
| {{_ 'due-date'}}
hr hr
li li
a.js-sort-title {{_ 'title-alphabetically'}} a.js-sort-title
| 🔤
| {{_ 'title-alphabetically'}}
hr hr
li li
a.js-sort-created-desc {{_ 'created-at-newest-first'}} a.js-sort-created-desc
| ⬇️
| {{_ 'created-at-newest-first'}}
hr hr
li 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-edit-board-title': Popup.open('boardChangeTitle'),
'click .js-star-board'() { '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-open-board-menu': Popup.open('boardMenu'),
'click .js-change-visibility': Popup.open('boardChangeVisibility'), 'click .js-change-visibility': Popup.open('boardChangeVisibility'),
@ -82,10 +85,37 @@ BlazeComponent.extendComponent({
}, },
'click .js-toggle-board-view': Popup.open('boardChangeView'), 'click .js-toggle-board-view': Popup.open('boardChangeView'),
'click .js-toggle-sidebar'() { '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'() { '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'), 'click .js-sort-cards': Popup.open('cardsSort'),
/* /*
@ -102,14 +132,22 @@ BlazeComponent.extendComponent({
*/ */
'click .js-filter-reset'(event) { 'click .js-filter-reset'(event) {
event.stopPropagation(); event.stopPropagation();
Sidebar.setView(); if (Sidebar) {
Sidebar.setView();
} else {
console.warn('Sidebar not available for setView');
}
Filter.reset(); Filter.reset();
}, },
'click .js-sort-reset'() { 'click .js-sort-reset'() {
Session.set('sortBy', ''); Session.set('sortBy', '');
}, },
'click .js-open-search-view'() { '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'() { 'click .js-multiselection-activate'() {
const currentCard = Utils.getCurrentCardId(); const currentCard = Utils.getCurrentCardId();
@ -128,6 +166,7 @@ BlazeComponent.extendComponent({
}, },
]; ];
}, },
}).register('boardHeaderBar'); }).register('boardHeaderBar');
Template.boardHeaderBar.helpers({ Template.boardHeaderBar.helpers({
@ -137,6 +176,23 @@ Template.boardHeaderBar.helpers({
isSortActive() { isSortActive() {
return Session.get('sortBy') ? true : false; 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({ Template.boardChangeViewPopup.events({
@ -203,6 +259,7 @@ const CreateBoard = BlazeComponent.extendComponent({
title: title, title: title,
permission: 'private', permission: 'private',
type: 'template-container', 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()); Utils.goBoardId(this.boardId.get());
} else { } else {
@ -246,6 +312,7 @@ const CreateBoard = BlazeComponent.extendComponent({
Boards.insert({ Boards.insert({
title, title,
permission: visibility, permission: visibility,
migrationVersion: 1, // Latest version - no migration needed
}), }),
); );
@ -254,6 +321,15 @@ const CreateBoard = BlazeComponent.extendComponent({
boardId: this.boardId.get(), 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()); Utils.goBoardId(this.boardId.get());
} }
}, },
@ -275,6 +351,13 @@ const CreateBoard = BlazeComponent.extendComponent({
}, },
}).register('createBoardPopup'); }).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 { (class HeaderBarCreateBoard extends CreateBoard {
onSubmit(event) { onSubmit(event) {
super.onSubmit(event); super.onSubmit(event);

View file

@ -8,6 +8,273 @@
padding: 1vh 0; 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 { .zoom-controls {
display: flex; display: flex;
align-items: center; align-items: center;
@ -103,26 +370,38 @@
transform: rotate(4deg); transform: rotate(4deg);
display: block !important; display: block !important;
} }
.board-list li.starred .fa-star, .board-list li.starred .is-star-active,
.board-list li.starred .fa-star-o { .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; opacity: 1;
} }
.board-list .board-list-item { .board-list .board-list-item {
overflow: hidden; overflow: hidden;
background-color: #999; background-color: inherit; /* Inherit board color from parent li.js-board */
color: #f6f6f6; color: #f6f6f6;
min-height: 100px; min-height: 100px;
font-size: 16px; font-size: 16px;
line-height: 22px; line-height: 22px;
border-radius: 3px; border-radius: 0; /* No border-radius - parent .js-board has it */
display: block; display: block;
font-weight: 700; font-weight: 700;
padding: 8px; padding: 36px 8px 32px 8px; /* Top padding for drag handle, bottom for checkbox */
margin: 8px; margin: 0; /* No margin - moved to parent .js-board */
position: relative; position: relative;
text-decoration: none; text-decoration: none;
word-wrap: break-word; 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 { .board-list .board-list-item.template-container {
border: 4px solid #fff; border: 4px solid #fff;
} }
@ -150,13 +429,20 @@
.board-list .js-add-board .label { .board-list .js-add-board .label {
font-weight: normal; font-weight: normal;
line-height: 56px; 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 { .board-list .js-add-board .label:hover {
background-color: #939393; background-color: #808080; /* Even darker on hover */
} }
.board-list .fa-star, .board-list .is-star-active,
.board-list .fa-star-o { .board-list .is-not-star-active {
bottom: 0; top: 0;
font-size: 14px; font-size: 14px;
height: 18px; height: 18px;
line-height: 18px; line-height: 18px;
@ -164,7 +450,6 @@
padding: 9px 9px; padding: 9px 9px;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0;
transition-duration: 0.15s; transition-duration: 0.15s;
transition-property: color, font-size, background; transition-property: color, font-size, background;
} }
@ -212,32 +497,121 @@
transition-duration: 0.15s; transition-duration: 0.15s;
transition-property: color, font-size, background; 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-clone,
.board-list li:hover a:hover .fa-archive, .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; 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-clone,
.board-list li:hover a .fa-archive, .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; color: #fff;
opacity: 0.75; 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-clone:hover,
.board-list li:hover a .fa-archive: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; font-size: 18px;
opacity: 1; opacity: 1;
} }
.board-list li:hover a .fa-star.is-star-active, .board-list li:hover a .is-star-active,
.board-list li:hover a .fa-clone.is-star-active, .board-list li:hover a .fa-clone,
.board-list li:hover a .fa-archive.is-star-active, .board-list li:hover a .fa-archive,
.board-list li:hover a .fa-star-o.is-star-active { .board-list li:hover a .is-not-star-active {
opacity: 1; 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 { .board-backgrounds-list .board-background-select {
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
@ -361,6 +735,18 @@
min-height: 100vh; /* Force content to be tall enough to scroll */ 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 { .board-list.mobile-view::after {
content: ''; content: '';
display: block; display: block;
@ -371,7 +757,8 @@
screen and (max-device-width: 800px), screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-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: 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 { .board-list {
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
@ -457,7 +844,8 @@
screen and (max-device-width: 800px), screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-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: 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 { .wrapper {
font-size: 2em !important; /* 2x bigger base font size for All Boards page */ font-size: 2em !important; /* 2x bigger base font size for All Boards page */
} }
@ -725,9 +1113,62 @@
#resetBtn { #resetBtn {
display: inline; 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 { .js-board {
display: block; 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 { .minicard-members {
padding: 6px 0 6px 8px; padding: 6px 0 6px 8px;
width: 100%; width: 100%;
@ -757,7 +1198,8 @@
screen and (max-device-width: 800px), screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-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: 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 { .wrapper {
overflow: hidden; overflow: hidden;
height: 100vh; height: 100vh;
@ -824,5 +1266,17 @@
#content { #content {
overflow: hidden; 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 .wrapper
.board-list-header .board-list-header
ul.AllBoardTeamsOrgs .boards-layout
li.AllBoardTeams // Left menu
if userHasTeams .boards-left-menu
select.js-AllBoardTeams#jsAllBoardTeams("multiple") ul.menu
option(value="-1") {{_ 'teams'}} : li(class="menu-item {{#if isSelectedMenu 'starred'}}active{{/if}}")
each teamsDatas a.js-select-menu(data-type="starred")
option(value="{{teamId}}") {{_ teamDisplayName}} 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 // Existing filter by orgs/teams (kept)
if userHasOrgs ul.AllBoardTeamsOrgs
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple") li.AllBoardTeams
option(value="-1") {{_ 'organizations'}} : if userHasTeams
each orgsDatas select.js-AllBoardTeams#jsAllBoardTeams("multiple")
option(value="{{orgId}}") {{orgDisplayName}} option(value="-1") {{_ 'teams'}} :
each teamsDatas
option(value="{{teamId}}") {{_ teamDisplayName}}
//li.AllBoardTemplates li.AllBoardOrgs
// if userHasTemplates if userHasOrgs
// select.js-AllBoardTemplates#jsAllBoardTemplates("multiple") select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
// option(value="-1") {{_ 'templates'}} : option(value="-1") {{_ 'organizations'}} :
// each templatesDatas each orgsDatas
// option(value="{{templateId}}") {{_ templateDisplayName}} option(value="{{orgId}}") {{orgDisplayName}}
li.AllBoardBtns li.AllBoardBtns
div.AllBoardButtonsContainer div.AllBoardButtonsContainer
if userHasOrgsOrTeams if userHasOrgsOrTeams
i.fa.fa-filter span 🔍
input#filterBtn(type="button" value="{{_ 'filter'}}") input#filterBtn(type="button" value="{{_ 'filter'}}")
input#resetBtn(type="button" value="{{_ 'filter-clear'}}") button#resetBtn.filter-reset-btn
span.reset-icon ❌
span {{_ 'filter-clear'}}
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}}") // Right boards grid
li.js-add-board .boards-right-grid
a.board-list-item.label(title="{{_ 'add-board'}}") .boards-path-header
| {{_ 'add-board'}} .path-left
each boards span.path-icon {{currentMenuPath.icon}}
li(class="{{_id}}" class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board span.path-text {{currentMenuPath.text}}
if isInvited if BoardMultiSelection.isActive
.board-list-item span.multiselection-hint 📌 {{_ 'multi-selection-active'}}
span.details .path-right
span.board-list-item-name= title if canModifyBoards
i.fa.js-star-board( if hasBoardsSelected
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}" button.js-archive-selected-boards.board-header-btn
title="{{_ 'star-board-title'}}") span 📦
p.board-list-item-desc {{_ 'just-invited'}} span {{_ 'archive-board'}}
button.js-accept-invite.primary {{_ 'accept'}} button.js-duplicate-selected-boards.board-header-btn
button.js-decline-invite {{_ 'decline'}} span 📋
else span {{_ 'duplicate-board'}}
if $eq type "template-container" a.board-header-btn.js-multiselection-activate(
a.js-open-board.template-container.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}") title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
span.details class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
span.board-list-item-name(title="{{_ 'template-container'}}") | ☑️
+viewer if BoardMultiSelection.isActive
= title a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
i.fa.js-star-board( | ✖
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}" ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
title="{{_ 'star-board-title'}}") li.js-add-board
p.board-list-item-desc if isSelectedMenu 'templates'
+viewer a.board-list-item.label(title="{{_ 'add-template-container'}}")
= description | {{_ 'add-template-container'}}
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'}}")
else else
a.js-open-board.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}") a.board-list-item.label(title="{{_ 'add-board'}}")
span.details | {{_ 'add-board'}}
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}") each boards
+viewer li.js-board(class="{{_id}} {{#if isStarred}}starred{{/if}} {{colorClass}} {{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}", draggable="true")
= title if isInvited
unless currentSetting.hideBoardMemberList .board-list-item
if allowsBoardMemberList if BoardMultiSelection.isActive
.minicard-members .materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
each member in boardMembers _id class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
a.name span.details
+userAvatar(userId=member noRemove=true) span.board-list-item-name= title
unless currentSetting.hideCardCounterList span.js-star-board(
if allowsCardCounterList class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
.minicard-lists.flex.flex-wrap title="{{_ 'star-board-title'}}")
each list in boardLists _id | {{#if isStarred}}⭐{{else}}☆{{/if}}
.item p.board-list-item-desc {{_ 'just-invited'}}
| {{ list }} button.js-accept-invite.primary {{_ 'accept'}}
i.fa.js-star-board( button.js-decline-invite {{_ 'decline'}}
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}" else
title="{{_ 'star-board-title'}}") if $eq type "template-container"
p.board-list-item-desc .template-container.board-list-item
+viewer if BoardMultiSelection.isActive
= description .materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
if hasSpentTimeCards class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
i.fa.js-has-spenttime-cards( span.board-handle(title="{{_ 'drag-board'}}") ↕️
class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}" a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}") span.details
if isTouchScreenOrShowDesktopDragHandles span.board-list-item-name(title="{{_ 'template-container'}}")
i.fa.board-handle( +viewer
class="fa-arrows" = title
title="{{_ 'drag-board'}}") p.board-list-item-desc
else +viewer
if isSandstorm = description
i.fa.js-clone-board( if hasSpentTimeCards
class="fa-clone" span.js-has-spenttime-cards(
title="{{_ 'duplicate-board'}}") class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
i.fa.js-archive-board( title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
class="fa-archive" | ⏱️
title="{{_ 'archive-board'}}") span.js-star-board(
else if isAdministrable class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
i.fa.js-clone-board( title="{{_ 'star-board-title'}}")
class="fa-clone" | {{#if isStarred}}⭐{{else}}☆{{/if}}
title="{{_ 'duplicate-board'}}") else
i.fa.js-archive-board( .board-list-item
class="fa-archive" if BoardMultiSelection.isActive
title="{{_ 'archive-board'}}") .materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
else if currentUser.isAdmin class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
i.fa.js-clone-board( span.board-handle(title="{{_ 'drag-board'}}") ↕️
class="fa-clone" a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
title="{{_ 'duplicate-board'}}") span.details
i.fa.js-archive-board( span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
class="fa-archive" +viewer
title="{{_ 'archive-board'}}") = 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") template(name="boardListHeaderBar")
h1 {{_ title }} h1 {{_ title }}
@ -154,3 +166,25 @@ template(name="boardListHeaderBar")
// a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}") // a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
// i.fa.fa-clone // i.fa.fa-clone
// span {{_ 'templates'}} // 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 Utils.isMiniScreen() && Session.get('currentBoard'); */
return true; return true;
}, },
BoardMultiSelection() {
return BoardMultiSelection;
},
}) })
Template.boardListHeaderBar.events({ Template.boardListHeaderBar.events({
@ -45,6 +48,9 @@ BlazeComponent.extendComponent({
onCreated() { onCreated() {
Meteor.subscribe('setting'); Meteor.subscribe('setting');
Meteor.subscribe('tableVisibilityModeSettings'); Meteor.subscribe('tableVisibilityModeSettings');
this.selectedMenu = new ReactiveVar('starred');
this.selectedWorkspaceIdVar = new ReactiveVar(null);
this.workspacesTreeVar = new ReactiveVar([]);
let currUser = ReactiveCache.getCurrentUser(); let currUser = ReactiveCache.getCurrentUser();
let userLanguage; let userLanguage;
if (currUser && currUser.profile) { if (currUser && currUser.profile) {
@ -53,9 +59,72 @@ BlazeComponent.extendComponent({
if (userLanguage) { if (userLanguage) {
TAPi18n.setLanguage(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() { 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 itemsSelector = '.js-board:not(.placeholder)';
const $boards = this.$('.js-boards'); const $boards = this.$('.js-boards');
@ -73,27 +142,20 @@ BlazeComponent.extendComponent({
EscapeActions.executeUpTo('popup-close'); EscapeActions.executeUpTo('popup-close');
}, },
stop(evt, ui) { 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 prevBoardDom = ui.item.prev('.js-board').get(0);
const nextBoardBom = ui.item.next('.js-board').get(0); const nextBoardDom = ui.item.next('.js-board').get(0);
const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardBom, 1); const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardDom, 1);
const boardDomElement = ui.item.get(0); const boardDomElement = ui.item.get(0);
const board = Blaze.getData(boardDomElement); 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'); $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(() => { this.autorun(() => {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) { if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$boards.sortable({ $boards.sortable({
@ -101,6 +163,7 @@ BlazeComponent.extendComponent({
}); });
} }
}); });
*/
}, },
userHasTeams() { userHasTeams() {
if (ReactiveCache.getCurrentUser()?.teams?.length > 0) if (ReactiveCache.getCurrentUser()?.teams?.length > 0)
@ -132,6 +195,41 @@ BlazeComponent.extendComponent({
const ret = this.userHasOrgs() || this.userHasTeams(); const ret = this.userHasOrgs() || this.userHasTeams();
return ret; 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() { boards() {
let query = { let query = {
// { type: 'board' }, // { type: 'board' },
@ -184,10 +282,33 @@ BlazeComponent.extendComponent({
}; };
} }
const ret = ReactiveCache.getBoards(query, { const boards = ReactiveCache.getBoards(query, {});
sort: { sort: 1 /* boards default sorting */ }, const currentUser = ReactiveCache.getCurrentUser();
}); let list = boards;
return ret; // 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) { boardLists(boardId) {
/* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214 /* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
@ -235,11 +356,65 @@ BlazeComponent.extendComponent({
events() { events() {
return [ return [
{ {
'click .js-add-board': Popup.open('createBoard'), 'click .js-select-menu'(evt) {
'click .js-star-board'(evt) { const type = evt.currentTarget.getAttribute('data-type');
const boardId = this.currentData()._id; this.selectedWorkspaceIdVar.set(null);
ReactiveCache.getCurrentUser().toggleBoardStar(boardId); 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(); 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) { 'click .js-clone-board'(evt) {
if (confirm(TAPi18n.__('duplicate-board-confirm'))) { 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) { 'click #resetBtn'(event) {
let allBoards = document.getElementsByClassName("js-board"); let allBoards = document.getElementsByClassName("js-board");
let currBoard; 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'); }).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; 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-overlay.hidden
#viewer-top-bar #viewer-top-bar
span#attachment-name span#attachment-name
a#viewer-close.fa.fa-times-thin a#viewer-close
#viewer-container #viewer-container
i.fa.fa-chevron-left.attachment-arrow#prev-attachment | ◀️
#viewer-content #viewer-content
img#image-viewer.hidden img#image-viewer.hidden
video#video-viewer.hidden(controls="true") video#video-viewer.hidden(controls="true")
@ -45,7 +45,7 @@ template(name="attachmentViewer")
object#pdf-viewer.hidden(type="application/pdf") object#pdf-viewer.hidden(type="application/pdf")
span.pdf-preview-error {{_ 'preview-pdf-not-supported' }} span.pdf-preview-error {{_ 'preview-pdf-not-supported' }}
object#txt-viewer.hidden(type="text/plain") object#txt-viewer.hidden(type="text/plain")
i.fa.fa-chevron-right.attachment-arrow#next-attachment | ▶️
template(name="attachmentGallery") template(name="attachmentGallery")
@ -53,11 +53,11 @@ template(name="attachmentGallery")
if canModifyCard if canModifyCard
a.attachment-item.add-attachment.js-add-attachment a.attachment-item.add-attachment.js-add-attachment
i.fa.fa-plus.icon |
each attachments 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 }}") .attachment-thumbnail-container.open-preview(data-attachment-id="{{_id}}" data-card-id="{{ meta.cardId }}")
if link if link
if(isImage) if(isImage)
@ -86,25 +86,32 @@ template(name="attachmentGallery")
= name = name
span.file-size ({{fileSize size}}) span.file-size ({{fileSize size}})
.attachment-actions .attachment-actions
a.js-download(href="{{link}}?download=true", download="{{name}}") a.js-download(href="{{link}}?download=true", download="{{name}}", title="{{_ 'download'}}")
i.fa.fa-download.icon(title="{{_ 'download'}}") | ⬇️
if currentUser.isBoardMember if currentUser.isBoardMember
unless currentUser.isCommentOnly unless currentUser.isCommentOnly
unless currentUser.isWorker unless currentUser.isWorker
a.js-rename a.js-rename(title="{{_ 'rename'}}")
i.fa.fa-pencil-square-o.icon(title="{{_ 'rename'}}") | ✏️
a.js-confirm-delete a.js-confirm-delete(title="{{_ '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-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") template(name="attachmentActionsPopup")
ul.pop-over-list ul.pop-over-list
li li
if isImage if isImage
a(class="{{#if isCover}}js-remove-cover{{else}}js-add-cover{{/if}}") a(class="{{#if isCover}}js-remove-cover{{else}}js-add-cover{{/if}}")
i.fa.fa-book | 📖
i.fa.fa-picture-o | 🖼️
if isCover if isCover
| {{_ 'remove-cover'}} | {{_ 'remove-cover'}}
else else
@ -112,7 +119,7 @@ template(name="attachmentActionsPopup")
if currentUser.isBoardAdmin if currentUser.isBoardAdmin
if isImage if isImage
a(class="{{#if isBackgroundImage}}js-remove-background-image{{else}}js-add-background-image{{/if}}") a(class="{{#if isBackgroundImage}}js-remove-background-image{{else}}js-add-background-image{{/if}}")
i.fa.fa-picture-o | 🖼️
if isBackgroundImage if isBackgroundImage
| {{_ 'remove-background-image'}} | {{_ 'remove-background-image'}}
else else
@ -120,19 +127,19 @@ template(name="attachmentActionsPopup")
if $neq versions.original.storage "fs" if $neq versions.original.storage "fs"
a.js-move-storage-fs a.js-move-storage-fs
i.fa.fa-arrow-right | ▶️
| {{_ 'attachment-move-storage-fs'}} | {{_ 'attachment-move-storage-fs'}}
if $neq versions.original.storage "gridfs" if $neq versions.original.storage "gridfs"
if versions.original.storage if versions.original.storage
a.js-move-storage-gridfs a.js-move-storage-gridfs
i.fa.fa-arrow-right | ▶️
| {{_ 'attachment-move-storage-gridfs'}} | {{_ 'attachment-move-storage-gridfs'}}
if $neq versions.original.storage "s3" if $neq versions.original.storage "s3"
if versions.original.storage if versions.original.storage
a.js-move-storage-s3 a.js-move-storage-s3
i.fa.fa-arrow-right | ▶️
| {{_ 'attachment-move-storage-s3'}} | {{_ 'attachment-move-storage-s3'}}
template(name="attachmentRenamePopup") template(name="attachmentRenamePopup")

View file

@ -3,6 +3,7 @@ import { ObjectID } from 'bson';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify'; import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
import uploadProgressManager from '../../lib/uploadProgressManager'; import uploadProgressManager from '../../lib/uploadProgressManager';
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
const filesize = require('filesize'); const filesize = require('filesize');
const prettyMilliseconds = require('pretty-ms'); const prettyMilliseconds = require('pretty-ms');
@ -342,7 +343,7 @@ export function handleFileUpload(card, files) {
} }
// Check if user can modify the card // Check if user can modify the card
if (!card.canModifyCard()) { if (!Utils.canModifyCard()) {
if (process.env.DEBUG === 'true') { if (process.env.DEBUG === 'true') {
console.warn('User does not have permission to modify this card'); console.warn('User does not have permission to modify this card');
} }
@ -576,3 +577,20 @@ BlazeComponent.extendComponent({
] ]
} }
}).register('attachmentRenamePopup'); }).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 span.full-name
= name = name
if hasCustomField if hasCustomField
i.fa.fa-check | ✅
hr hr
a.quiet-button.full.js-settings a.quiet-button.full.js-settings
i.fa.fa-cog | ⚙️
span {{_ 'settings'}} span {{_ 'settings'}}
template(name="cardCustomField") template(name="cardCustomField")
@ -22,7 +22,7 @@ template(name="cardCustomField-text")
= value = value
.edit-controls.clearfix .edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}} button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form a.js-close-inlined-form
else else
a.js-open-inlined-form a.js-open-inlined-form
if value if value
@ -41,7 +41,7 @@ template(name="cardCustomField-number")
input(type="number" value=data.value) input(type="number" value=data.value)
.edit-controls.clearfix .edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}} button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form a.js-close-inlined-form
else else
a.js-open-inlined-form a.js-open-inlined-form
if value if value
@ -66,7 +66,7 @@ template(name="cardCustomField-currency")
input(type="text" value=data.value autofocus) input(type="text" value=data.value autofocus)
.edit-controls.clearfix .edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}} button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form a.js-close-inlined-form
else else
a.js-open-inlined-form a.js-open-inlined-form
if value if value
@ -113,7 +113,7 @@ template(name="cardCustomField-dropdown")
= name = name
.edit-controls.clearfix .edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}} button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form a.js-close-inlined-form
else else
a.js-open-inlined-form a.js-open-inlined-form
if value 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) input.js-card-customfield-stringtemplate-item.last(type="text" value="" placeholder="{{_ 'custom-field-stringtemplate-item-placeholder'}}" autofocus)
.edit-controls.clearfix .edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}} button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form a.js-close-inlined-form
else else
a.js-open-inlined-form a.js-open-inlined-form
if value if value

View file

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

View file

@ -8,57 +8,134 @@
.card-date.is-active { .card-date.is-active {
background-color: #b3b3b3; background-color: #b3b3b3;
} }
.card-date.current, /* Date status colors - red = overdue, amber = due soon, no shade = not due */
.card-date.almost-due, .card-date.overdue {
.card-date.due, background-color: #ff4444; /* Red for overdue */
.card-date.long-overdue {
color: #fff; 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 { .card-date.current {
background-color: #5ba639; background-color: #5ba639; /* Green for current/active */
color: #fff;
} }
.card-date.current:hover, .card-date.current:hover,
.card-date.current.is-active { .card-date.current.is-active {
background-color: #46802c; 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.completed:hover,
.card-date.almost-due.is-active { .card-date.completed.is-active {
background-color: #bc9f07; 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.completed-early:hover,
.card-date.due.is-active { .card-date.completed-early.is-active {
background-color: #c73200; 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.completed-late:hover,
.card-date.long-overdue.is-active { .card-date.completed-late.is-active {
background-color: #fd3e24; 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 { .card-date.end-date time::before {
content: "\f253"; content: "🏁"; /* Finish flag - represents end/completion */
} }
.card-date.due-date time::before { .card-date.due-date time::before {
content: "\f090"; content: "⏰"; /* Alarm clock - represents due/deadline */
} }
.card-date.start-date time::before { .card-date.start-date time::before {
content: "\f251"; content: "🚀"; /* Rocket - represents start/launch */
} }
.card-date.received-date time::before { .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 { .card-date time::before {
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit; font-size: inherit;
-webkit-font-smoothing: antialiased;
margin-right: 0.3em; margin-right: 0.3em;
display: inline-block;
} }
.customfield-date { .customfield-date {
display: block; display: block;

View file

@ -21,3 +21,132 @@ template(name="dateCustomField")
if showWeekOfYear if showWeekOfYear
b b
| {{showWeek}} | {{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 { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker'; 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 // editCardReceivedDatePopup
(class extends DatePicker { (class extends DatePicker {
onCreated() { onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm')); super.onCreated(formatDateTime(now()));
this.data().getReceived() && this.data().getReceived() &&
this.date.set(moment(this.data().getReceived())); this.date.set(new Date(this.data().getReceived()));
} }
_storeDate(date) { _storeDate(date) {
this.card.setReceived(moment(date).format('YYYY-MM-DD HH:mm')); this.card.setReceived(formatDateTime(date));
} }
_deleteDate() { _deleteDate() {
@ -22,22 +43,28 @@ import { DatePicker } from '/client/lib/datepicker';
// editCardStartDatePopup // editCardStartDatePopup
(class extends DatePicker { (class extends DatePicker {
onCreated() { onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm')); super.onCreated(formatDateTime(now()));
this.data().getStart() && this.date.set(moment(this.data().getStart())); this.data().getStart() && this.date.set(new Date(this.data().getStart()));
} }
onRendered() { onRendered() {
super.onRendered(); super.onRendered();
if (moment.isDate(this.card.getReceived())) { // DatePicker base class handles initialization with native HTML inputs
this.$('.js-datepicker').datepicker( const self = this;
'setStartDate', this.$('.js-calendar-date').on('change', function(evt) {
this.card.getReceived(), 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) { _storeDate(date) {
this.card.setStart(moment(date).format('YYYY-MM-DD HH:mm')); this.card.setStart(formatDateTime(date));
} }
_deleteDate() { _deleteDate() {
@ -49,18 +76,16 @@ import { DatePicker } from '/client/lib/datepicker';
(class extends DatePicker { (class extends DatePicker {
onCreated() { onCreated() {
super.onCreated('1970-01-01 17:00:00'); 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() { onRendered() {
super.onRendered(); super.onRendered();
if (moment.isDate(this.card.getStart())) { // DatePicker base class handles initialization with native HTML inputs
this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
}
} }
_storeDate(date) { _storeDate(date) {
this.card.setDue(moment(date).format('YYYY-MM-DD HH:mm')); this.card.setDue(formatDateTime(date));
} }
_deleteDate() { _deleteDate() {
@ -71,19 +96,17 @@ import { DatePicker } from '/client/lib/datepicker';
// editCardEndDatePopup // editCardEndDatePopup
(class extends DatePicker { (class extends DatePicker {
onCreated() { onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm')); super.onCreated(formatDateTime(now()));
this.data().getEnd() && this.date.set(moment(this.data().getEnd())); this.data().getEnd() && this.date.set(new Date(this.data().getEnd()));
} }
onRendered() { onRendered() {
super.onRendered(); super.onRendered();
if (moment.isDate(this.card.getStart())) { // DatePicker base class handles initialization with native HTML inputs
this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
}
} }
_storeDate(date) { _storeDate(date) {
this.card.setEnd(moment(date).format('YYYY-MM-DD HH:mm')); this.card.setEnd(formatDateTime(date));
} }
_deleteDate() { _deleteDate() {
@ -100,27 +123,29 @@ const CardDate = BlazeComponent.extendComponent({
onCreated() { onCreated() {
const self = this; const self = this;
self.date = ReactiveVar(); self.date = ReactiveVar();
self.now = ReactiveVar(moment()); self.now = ReactiveVar(now());
window.setInterval(() => { window.setInterval(() => {
self.now.set(moment()); self.now.set(now());
}, 60000); }, 60000);
}, },
showWeek() { showWeek() {
return this.date.get().week().toString(); return getISOWeek(this.date.get()).toString();
}, },
showWeekOfYear() { 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() { showDate() {
// this will start working once mquandalle:moment const currentUser = ReactiveCache.getCurrentUser();
// is updated to at least moment.js 2.10.5 const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
// until then, the date is displayed in the "L" format return formatDateByUserPreference(this.date.get(), dateFormat, true);
return this.date.get().calendar(null, {
sameElse: 'llll',
});
}, },
showISODate() { showISODate() {
@ -133,7 +158,7 @@ class CardReceivedDate extends CardDate {
super.onCreated(); super.onCreated();
const self = this; const self = this;
self.autorun(() => { 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 endAt = this.data().getEnd();
const startAt = this.data().getStart(); const startAt = this.data().getStart();
const theDate = this.date.get(); 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 ( if (
(startAt && theDate.isAfter(startAt)) || (startAt && isAfter(theDate, startAt)) ||
(endAt && theDate.isAfter(endAt)) || (endAt && isAfter(theDate, endAt)) ||
(dueAt && theDate.isAfter(dueAt)) (dueAt && isAfter(theDate, dueAt))
) ) {
classes += 'long-overdue'; classes += 'overdue';
else classes += 'current'; } else {
classes += 'not-due';
}
return classes; return classes;
} }
showTitle() { showTitle() {
return `${TAPi18n.__('card-received-on')} ${this.date const currentUser = ReactiveCache.getCurrentUser();
.get() const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
.format('LLLL')}`; const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
} }
events() { events() {
@ -173,26 +203,35 @@ class CardStartDate extends CardDate {
super.onCreated(); super.onCreated();
const self = this; const self = this;
self.autorun(() => { self.autorun(() => {
self.date.set(moment(self.data().getStart())); self.date.set(new Date(self.data().getStart()));
}); });
} }
classes() { classes() {
let classes = 'start-date' + ' '; let classes = 'start-date ';
const dueAt = this.data().getDue(); const dueAt = this.data().getDue();
const endAt = this.data().getEnd(); const endAt = this.data().getEnd();
const theDate = this.date.get(); const theDate = this.date.get();
const now = this.now.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))) // Start date logic: if start date is after due or end dates, it's overdue
classes += 'long-overdue'; if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
else if (theDate.isAfter(now)) classes += ''; classes += 'overdue';
else classes += 'current'; } 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; return classes;
} }
showTitle() { 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() { events() {
@ -208,27 +247,48 @@ class CardDueDate extends CardDate {
super.onCreated(); super.onCreated();
const self = this; const self = this;
self.autorun(() => { self.autorun(() => {
self.date.set(moment(self.data().getDue())); self.date.set(new Date(self.data().getDue()));
}); });
} }
classes() { classes() {
let classes = 'due-date' + ' '; let classes = 'due-date ';
const endAt = this.data().getEnd(); const endAt = this.data().getEnd();
const theDate = this.date.get(); const theDate = this.date.get();
const now = this.now.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's an end date and it's before the due date, task is completed early
// if there is an end date, don't need to flag the due date if (endAt && isBefore(endAt, theDate)) {
else if (endAt) classes += ''; classes += 'completed-early';
else if (now.diff(theDate, 'days') >= 2) classes += 'long-overdue'; }
else if (now.diff(theDate, 'minute') >= 0) classes += 'due'; // If there's an end date, don't show due date status since task is completed
else if (now.diff(theDate, 'days') >= -1) classes += 'almost-due'; 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; return classes;
} }
showTitle() { 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() { events() {
@ -244,22 +304,33 @@ class CardEndDate extends CardDate {
super.onCreated(); super.onCreated();
const self = this; const self = this;
self.autorun(() => { self.autorun(() => {
self.date.set(moment(self.data().getEnd())); self.date.set(new Date(self.data().getEnd()));
}); });
} }
classes() { classes() {
let classes = 'end-date' + ' '; let classes = 'end-date ';
const dueAt = this.data().getDue(); const dueAt = this.data().getDue();
const theDate = this.date.get(); const theDate = this.date.get();
if (!dueAt) classes += '';
else if (theDate.isBefore(dueAt)) classes += 'current'; if (!dueAt) {
else if (theDate.isAfter(dueAt)) classes += 'due'; // 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; return classes;
} }
showTitle() { showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`; return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`;
} }
events() { events() {
@ -279,16 +350,21 @@ class CardCustomFieldDate extends CardDate {
super.onCreated(); super.onCreated();
const self = this; const self = this;
self.autorun(() => { self.autorun(() => {
self.date.set(moment(self.data().value)); self.date.set(new Date(self.data().value));
}); });
} }
showWeek() { showWeek() {
return this.date.get().week().toString(); return getISOWeek(this.date.get()).toString();
} }
showWeekOfYear() { 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() { showDate() {
@ -301,7 +377,10 @@ class CardCustomFieldDate extends CardDate {
} }
showTitle() { 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() { classes() {
@ -315,32 +394,62 @@ class CardCustomFieldDate extends CardDate {
CardCustomFieldDate.register('cardCustomFieldDate'); CardCustomFieldDate.register('cardCustomFieldDate');
(class extends CardReceivedDate { (class extends CardReceivedDate {
template() {
return 'minicardReceivedDate';
}
showDate() { 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')); }.register('minicardReceivedDate'));
(class extends CardStartDate { (class extends CardStartDate {
template() {
return 'minicardStartDate';
}
showDate() { 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')); }.register('minicardStartDate'));
(class extends CardDueDate { (class extends CardDueDate {
template() {
return 'minicardDueDate';
}
showDate() { 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')); }.register('minicardDueDate'));
(class extends CardEndDate { (class extends CardEndDate {
template() {
return 'minicardEndDate';
}
showDate() { 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')); }.register('minicardEndDate'));
(class extends CardCustomFieldDate { (class extends CardCustomFieldDate {
template() {
return 'minicardCustomFieldDate';
}
showDate() { 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')); }.register('minicardCustomFieldDate'));
@ -349,7 +458,7 @@ class VoteEndDate extends CardDate {
super.onCreated(); super.onCreated();
const self = this; const self = this;
self.autorun(() => { self.autorun(() => {
self.date.set(moment(self.data().getVoteEnd())); self.date.set(new Date(self.data().getVoteEnd()));
}); });
} }
classes() { classes() {
@ -357,10 +466,12 @@ class VoteEndDate extends CardDate {
return classes; return classes;
} }
showDate() { 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() { showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`; return `${TAPi18n.__('card-end-on')} ${this.date.get().toLocaleString()}`;
} }
events() { events() {
@ -376,7 +487,7 @@ class PokerEndDate extends CardDate {
super.onCreated(); super.onCreated();
const self = this; const self = this;
self.autorun(() => { self.autorun(() => {
self.date.set(moment(self.data().getPokerEnd())); self.date.set(new Date(self.data().getPokerEnd()));
}); });
} }
classes() { classes() {
@ -384,10 +495,12 @@ class PokerEndDate extends CardDate {
return classes; return classes;
} }
showDate() { 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() { showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`; return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`;
} }
events() { 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 { .assignee {
border-radius: 0.4vw; border-radius: 3px;
display: block; display: block;
position: relative; position: relative;
float: left; float: left;
height: 4vw; height: 30px;
width: 4vw; width: 30px;
margin: 0.4vh; margin: .3vh;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
z-index: 1; z-index: 1;
@ -34,11 +62,11 @@
background-color: #b3b3b3; background-color: #b3b3b3;
border: 1px solid #fff; border: 1px solid #fff;
border-radius: 50%; border-radius: 50%;
height: 1vw; height: 7px;
width: 1vw; width: 7px;
position: absolute; position: absolute;
right: -0.1vw; right: -1px;
bottom: -0.1vw; bottom: -1px;
border: 1px solid #fff; border: 1px solid #fff;
z-index: 15; z-index: 15;
} }

View file

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

View file

@ -1,7 +1,26 @@
import { ReactiveCache } from '/imports/reactiveCache'; import { ReactiveCache } from '/imports/reactiveCache';
import moment from 'moment/min/moment-with-locales';
import { TAPi18n } from '/imports/i18n'; import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker'; 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 Cards from '/models/cards';
import Boards from '/models/boards'; import Boards from '/models/boards';
import Checklists from '/models/checklists'; import Checklists from '/models/checklists';
@ -287,6 +306,10 @@ BlazeComponent.extendComponent({
const $tooltip = this.$('.card-details-header .copied-tooltip'); const $tooltip = this.$('.card-details-header .copied-tooltip');
Utils.showCopied(promise, $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'), 'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
'submit .js-card-description'(event) { 'submit .js-card-description'(event) {
event.preventDefault(); event.preventDefault();
@ -407,56 +430,57 @@ BlazeComponent.extendComponent({
) { ) {
newState = forIt; 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) { 'click .js-poker'(e) {
let newState = null; let newState = null;
if ($(e.target).hasClass('js-poker-vote-one')) { if ($(e.target).hasClass('js-poker-vote-one')) {
newState = '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')) { if ($(e.target).hasClass('js-poker-vote-two')) {
newState = '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')) { if ($(e.target).hasClass('js-poker-vote-three')) {
newState = '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')) { if ($(e.target).hasClass('js-poker-vote-five')) {
newState = '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')) { if ($(e.target).hasClass('js-poker-vote-eight')) {
newState = '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')) { if ($(e.target).hasClass('js-poker-vote-thirteen')) {
newState = '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')) { if ($(e.target).hasClass('js-poker-vote-twenty')) {
newState = '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')) { if ($(e.target).hasClass('js-poker-vote-forty')) {
newState = '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')) { if ($(e.target).hasClass('js-poker-vote-one-hundred')) {
newState = 'oneHundred'; newState = 'oneHundred';
this.data().setPoker(Meteor.userId(), newState); Meteor.call('cards.pokerVote', this.data()._id, newState);
} }
if ($(e.target).hasClass('js-poker-vote-unsure')) { if ($(e.target).hasClass('js-poker-vote-unsure')) {
newState = 'unsure'; newState = 'unsure';
this.data().setPoker(Meteor.userId(), newState); Meteor.call('cards.pokerVote', this.data()._id, newState);
} }
}, },
'click .js-poker-finish'(e) { 'click .js-poker-finish'(e) {
if ($(e.target).hasClass('js-poker-finish')) { if ($(e.target).hasClass('js-poker-finish')) {
e.preventDefault(); e.preventDefault();
const now = moment().format('YYYY-MM-DD HH:mm'); const now = new Date();
this.data().setPokerEnd(now); Meteor.call('cards.setPokerEnd', this.data()._id, now);
} }
}, },
@ -464,9 +488,9 @@ BlazeComponent.extendComponent({
if ($(e.target).hasClass('js-poker-replay')) { if ($(e.target).hasClass('js-poker-replay')) {
e.preventDefault(); e.preventDefault();
this.currentCard = this.currentData(); this.currentCard = this.currentData();
this.currentCard.replayPoker(); Meteor.call('cards.replayPoker', this.currentCard._id);
this.data().unsetPokerEnd(); Meteor.call('cards.unsetPokerEnd', this.currentCard._id);
this.data().unsetPokerEstimation(); Meteor.call('cards.unsetPokerEstimation', this.currentCard._id);
} }
}, },
'click .js-poker-estimation'(event) { 'click .js-poker-estimation'(event) {
@ -477,63 +501,66 @@ BlazeComponent.extendComponent({
this.find('#pokerEstimation').value = ''; this.find('#pokerEstimation').value = '';
if (ruleTitle) { if (ruleTitle) {
this.data().setPokerEstimation(parseInt(ruleTitle, 10)); Meteor.call('cards.setPokerEstimation', this.data()._id, parseInt(ruleTitle, 10));
} else { } else {
this.data().setPokerEstimation(''); Meteor.call('cards.unsetPokerEstimation', this.data()._id);
} }
} }
}, },
// Drag and drop file upload handlers // Drag and drop file upload handlers
'dragover .js-card-details'(event) { 'dragover .js-card-details'(event) {
event.preventDefault(); // Only prevent default for file drags to avoid interfering with other drag operations
event.stopPropagation(); const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
}
}, },
'dragenter .js-card-details'(event) { 'dragenter .js-card-details'(event) {
event.preventDefault(); const dataTransfer = event.originalEvent.dataTransfer;
event.stopPropagation(); if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
const card = this.data(); event.preventDefault();
const board = card.board(); event.stopPropagation();
// Only allow drag-and-drop if user can modify card and board allows attachments const card = this.data();
if (card.canModifyCard() && board && board.allowsAttachments) { const board = card.board();
// Check if the drag contains files // Only allow drag-and-drop if user can modify card and board allows attachments
const dataTransfer = event.originalEvent.dataTransfer; if (Utils.canModifyCard() && board && board.allowsAttachments) {
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
$(event.currentTarget).addClass('is-dragging-over'); $(event.currentTarget).addClass('is-dragging-over');
} }
} }
}, },
'dragleave .js-card-details'(event) { 'dragleave .js-card-details'(event) {
event.preventDefault(); const dataTransfer = event.originalEvent.dataTransfer;
event.stopPropagation(); if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
$(event.currentTarget).removeClass('is-dragging-over'); event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
}
}, },
'drop .js-card-details'(event) { '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; const dataTransfer = event.originalEvent.dataTransfer;
if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) { if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
return; event.preventDefault();
} event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
// Check if the drop contains files (not just text/HTML) const card = this.data();
if (!dataTransfer.types.includes('Files')) { const board = card.board();
return;
}
const files = dataTransfer.files; // Check permissions
if (files && files.length > 0) { if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
handleFileUpload(card, files); 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(); let ret = !!Utils.getPopupCardId();
return ret; return ret;
}, },
isDateFormat(format) {
const currentUser = ReactiveCache.getCurrentUser();
if (!currentUser) return format === 'YYYY-MM-DD';
return currentUser.getDateFormat() === format;
},
// Upload progress helpers // Upload progress helpers
hasActiveUploads() { hasActiveUploads() {
return uploadProgressManager.hasActiveUploads(this._id); return uploadProgressManager.hasActiveUploads(this._id);
@ -1074,20 +1106,15 @@ BlazeComponent.extendComponent({
'is-checked', 'is-checked',
); );
const endString = this.currentCard.getVoteEnd(); const endString = this.currentCard.getVoteEnd();
Meteor.call('cards.setVoteQuestion', this.currentCard._id, voteQuestion, publicVote, allowNonBoardMembers);
this.currentCard.setVoteQuestion(
voteQuestion,
publicVote,
allowNonBoardMembers,
);
if (endString) { if (endString) {
this.currentCard.setVoteEnd(endString); Meteor.call('cards.setVoteEnd', this.currentCard._id, endString);
} }
Popup.back(); Popup.back();
}, },
'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => { 'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => {
event.preventDefault(); event.preventDefault();
this.currentCard.unsetVote(); Meteor.call('cards.unsetVote', this.currentCard._id);
Popup.back(); Popup.back();
}), }),
'click a.js-toggle-vote-public'(event) { 'click a.js-toggle-vote-public'(event) {
@ -1106,8 +1133,8 @@ BlazeComponent.extendComponent({
// editVoteEndDatePopup // editVoteEndDatePopup
(class extends DatePicker { (class extends DatePicker {
onCreated() { onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm')); super.onCreated(formatDateTime(now()));
this.data().getVoteEnd() && this.date.set(moment(this.data().getVoteEnd())); this.data().getVoteEnd() && this.date.set(new Date(this.data().getVoteEnd()));
} }
events() { events() {
return [ return [
@ -1118,12 +1145,12 @@ BlazeComponent.extendComponent({
// if no time was given, init with 12:00 // if no time was given, init with 12:00
const time = const time =
evt.target.time.value || 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 dateString = `${evt.target.date.value} ${time}`;
/* /*
const newDate = moment(dateString, 'L LT', true); const newDate = parseDate(dateString, ['L LT'], true);
if (newDate.isValid()) { if (newDate.isValid()) {
// if active vote - store it // if active vote - store it
if (this.currentData().getVoteQuestion()) { if (this.currentData().getVoteQuestion()) {
@ -1137,28 +1164,27 @@ BlazeComponent.extendComponent({
*/ */
// Try to parse different date formats of all languages. // Try to parse different date formats using native Date parsing
// This code is same for vote and planning poker. const formats = [
const usaDate = moment(dateString, 'L LT', true); 'YYYY-MM-DD HH:mm',
const euroAmDate = moment(dateString, 'DD.MM.YYYY LT', true); 'MM/DD/YYYY HH:mm',
const euro24hDate = moment(dateString, 'DD.MM.YYYY HH.mm', true); 'DD.MM.YYYY HH:mm',
const eurodotDate = moment(dateString, 'DD.MM.YYYY HH:mm', true); 'DD/MM/YYYY HH:mm',
const minusDate = moment(dateString, 'YYYY-MM-DD HH:mm', true); 'DD-MM-YYYY HH:mm'
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); let parsedDate = null;
const hrvatskiDate = moment(dateString, 'DD. MM. YYYY H:mm', true); for (const format of formats) {
const latviaDate = moment(dateString, 'YYYY.MM.DD. H:mm', true); parsedDate = parseDate(dateString, [format], true);
const nederlandsDate = moment(dateString, 'DD-MM-YYYY HH:mm', true); if (parsedDate) break;
// 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 ? // Fallback to native Date parsing
// Also some other languages that have non-ascii characters in dates if (!parsedDate) {
// do not work. parsedDate = new Date(dateString);
const greekDate = moment(dateString, 'DD/MM/YYYY h:mm A', true); }
const macedonianDate = moment(dateString, 'D.MM.YYYY H:mm', true);
if (usaDate.isValid()) { if (isValidDate(parsedDate)) {
// if active poker - store it // if active poker - store it
if (this.currentData().getPokerQuestion()) { if (this.currentData().getPokerQuestion()) {
this._storeDate(usaDate.toDate()); this._storeDate(usaDate.toDate());
@ -1287,10 +1313,10 @@ BlazeComponent.extendComponent({
]; ];
} }
_storeDate(newDate) { _storeDate(newDate) {
this.card.setVoteEnd(newDate); Meteor.call('cards.setVoteEnd', this.card._id, newDate);
} }
_deleteDate() { _deleteDate() {
this.card.unsetVoteEnd(); Meteor.call('cards.unsetVoteEnd', this.card._id);
} }
}.register('editVoteEndDatePopup')); }.register('editVoteEndDatePopup'));
@ -1312,17 +1338,14 @@ BlazeComponent.extendComponent({
); );
const endString = this.currentCard.getPokerEnd(); const endString = this.currentCard.getPokerEnd();
this.currentCard.setPokerQuestion( Meteor.call('cards.setPokerQuestion', this.currentCard._id, pokerQuestion, allowNonBoardMembers);
pokerQuestion,
allowNonBoardMembers,
);
if (endString) { if (endString) {
this.currentCard.setPokerEnd(endString); Meteor.call('cards.setPokerEnd', this.currentCard._id, new Date(endString));
} }
Popup.back(); Popup.back();
}, },
'click .js-remove-poker': Popup.afterConfirm('deletePoker', (event) => { 'click .js-remove-poker': Popup.afterConfirm('deletePoker', (event) => {
this.currentCard.unsetPoker(); Meteor.call('cards.unsetPoker', this.currentCard._id);
Popup.back(); Popup.back();
}), }),
'click a.js-toggle-poker-allow-non-members'(event) { 'click a.js-toggle-poker-allow-non-members'(event) {
@ -1337,9 +1360,9 @@ BlazeComponent.extendComponent({
// editPokerEndDatePopup // editPokerEndDatePopup
(class extends DatePicker { (class extends DatePicker {
onCreated() { onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm')); super.onCreated(formatDateTime(now()));
this.data().getPokerEnd() && 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'); return moment.localeData().longDateFormat('LT');
} }
const newDate = moment(dateString, dateformat() + ' ' + timeformat(), true); const newDate = parseDate(dateString, [dateformat() + ' ' + timeformat()], true);
*/ */
events() { events() {
@ -1369,7 +1392,7 @@ BlazeComponent.extendComponent({
// if no time was given, init with 12:00 // if no time was given, init with 12:00
const time = const time =
evt.target.time.value || 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 dateString = `${evt.target.date.value} ${time}`;
@ -1380,7 +1403,7 @@ BlazeComponent.extendComponent({
Maybe client/components/lib/datepicker.jade could have hidden input field for Maybe client/components/lib/datepicker.jade could have hidden input field for
datepicker format that could be used to detect date format? 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 (newDate.isValid()) {
// if active poker - store it // if active poker - store it
@ -1393,28 +1416,27 @@ BlazeComponent.extendComponent({
} }
*/ */
// Try to parse different date formats of all languages. // Try to parse different date formats using native Date parsing
// This code is same for vote and planning poker. const formats = [
const usaDate = moment(dateString, 'L LT', true); 'YYYY-MM-DD HH:mm',
const euroAmDate = moment(dateString, 'DD.MM.YYYY LT', true); 'MM/DD/YYYY HH:mm',
const euro24hDate = moment(dateString, 'DD.MM.YYYY HH.mm', true); 'DD.MM.YYYY HH:mm',
const eurodotDate = moment(dateString, 'DD.MM.YYYY HH:mm', true); 'DD/MM/YYYY HH:mm',
const minusDate = moment(dateString, 'YYYY-MM-DD HH:mm', true); 'DD-MM-YYYY HH:mm'
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); let parsedDate = null;
const hrvatskiDate = moment(dateString, 'DD. MM. YYYY H:mm', true); for (const format of formats) {
const latviaDate = moment(dateString, 'YYYY.MM.DD. H:mm', true); parsedDate = parseDate(dateString, [format], true);
const nederlandsDate = moment(dateString, 'DD-MM-YYYY HH:mm', true); if (parsedDate) break;
// 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 ? // Fallback to native Date parsing
// Also some other languages that have non-ascii characters in dates if (!parsedDate) {
// do not work. parsedDate = new Date(dateString);
const greekDate = moment(dateString, 'DD/MM/YYYY h:mm A', true); }
const macedonianDate = moment(dateString, 'D.MM.YYYY H:mm', true);
if (usaDate.isValid()) { if (isValidDate(parsedDate)) {
// if active poker - store it // if active poker - store it
if (this.currentData().getPokerQuestion()) { if (this.currentData().getPokerQuestion()) {
this._storeDate(usaDate.toDate()); this._storeDate(usaDate.toDate());
@ -1544,10 +1566,10 @@ BlazeComponent.extendComponent({
]; ];
} }
_storeDate(newDate) { _storeDate(newDate) {
this.card.setPokerEnd(newDate); Meteor.call('cards.setPokerEnd', this.card._id, newDate);
} }
_deleteDate() { _deleteDate() {
this.card.unsetPokerEnd(); Meteor.call('cards.unsetPokerEnd', this.card._id);
} }
}.register('editPokerEndDatePopup')); }.register('editPokerEndDatePopup'));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -125,8 +125,19 @@ Template.createLabelPopup.events({
.$('#labelName') .$('#labelName')
.val() .val()
.trim(); .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(); Popup.back();
}, },
}); });
@ -144,8 +155,19 @@ Template.editLabelPopup.events({
.$('#labelName') .$('#labelName')
.val() .val()
.trim(); .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(); Popup.back();
}, },
}); });

View file

@ -99,8 +99,8 @@
float: none; float: none;
} }
.minicard .minicard-labels .minicard-label { .minicard .minicard-labels .minicard-label {
width: 1.5vw; width: clamp(12px, 1.5vw, 16px);
height: 1.5vw; height: clamp(12px, 1.5vw, 16px);
border-radius: 0.3vw; border-radius: 0.3vw;
margin-right: 0.4vw; margin-right: 0.4vw;
margin-bottom: 0.4vh; margin-bottom: 0.4vh;
@ -130,8 +130,8 @@
margin-right: 0.5vw; margin-right: 0.5vw;
} }
.minicard .handle { .minicard .handle {
width: 2.5vw; width: clamp(20px, 2.5vw, 28px);
height: 2.5vw; height: clamp(20px, 2.5vw, 28px);
position: absolute; position: absolute;
right: 0.7vw; right: 0.7vw;
top: 0.7vh; top: 0.7vh;
@ -168,6 +168,148 @@
.minicard .date { .minicard .date {
margin-right: 0.4vw; 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 { .minicard .badges {
float: left; float: left;
margin-top: 1vh; margin-top: 1vh;
@ -220,8 +362,8 @@
.minicard .minicard-creator .member { .minicard .minicard-creator .member {
float: right; float: right;
border-radius: 50%; border-radius: 50%;
height: 3.5vw; height: clamp(24px, 3.5vw, 32px);
width: 3.5vw; width: clamp(24px, 3.5vw, 32px);
margin-bottom: 0.5vh; margin-bottom: 0.5vh;
} }
.minicard .minicard-members .assignee, .minicard .minicard-members .assignee,
@ -229,8 +371,8 @@
.minicard .minicard-creator .assignee { .minicard .minicard-creator .assignee {
float: right; float: right;
border-radius: 50%; border-radius: 50%;
height: 3.5vw; height: clamp(24px, 3.5vw, 32px);
width: 3.5vw; width: clamp(24px, 3.5vw, 32px);
} }
.minicard .minicard-members + .badges, .minicard .minicard-members + .badges,
.minicard .minicard-assignees + .badges, .minicard .minicard-assignees + .badges,

View file

@ -4,19 +4,14 @@ template(name="minicard")
class="{{#if isLinkedBoard}}linked-board{{/if}}" class="{{#if isLinkedBoard}}linked-board{{/if}}"
class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}") class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}")
if canModifyCard if canModifyCard
if isTouchScreenOrShowDesktopDragHandles a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") ☰
a.fa.fa-navicon.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") if canMoveCard
.handle .handle
.fa.fa-arrows | ↕️
else
a.fa.fa-navicon.minicard-details-menu.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
.dates .dates
if getReceived if getReceived
unless getStart .date
unless getDue +minicardReceivedDate
unless getEnd
.date
+minicardReceivedDate
if getStart if getStart
.date .date
+minicardStartDate +minicardStartDate
@ -36,7 +31,7 @@ template(name="minicard")
if hasActiveUploads if hasActiveUploads
.minicard-upload-progress .minicard-upload-progress
.upload-progress-header .upload-progress-header
i.fa.fa-upload | 📤
span {{_ 'uploading-files'}} ({{uploadCount}}) span {{_ 'uploading-files'}} ({{uploadCount}})
each uploads each uploads
.upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}") .upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
@ -45,11 +40,11 @@ template(name="minicard")
.upload-progress-fill(style="width: {{progress}}%") .upload-progress-fill(style="width: {{progress}}%")
if $eq status 'error' if $eq status 'error'
.upload-progress-error .upload-progress-error
i.fa.fa-exclamation-triangle | ⚠️
span {{_ 'upload-failed'}} span {{_ 'upload-failed'}}
else if $eq status 'completed' else if $eq status 'completed'
.upload-progress-success .upload-progress-success
i.fa.fa-check | ✅
span {{_ 'upload-completed'}} span {{_ 'upload-completed'}}
.minicard-title .minicard-title
@ -61,12 +56,12 @@ template(name="minicard")
| {{ parentCardName }} | {{ parentCardName }}
if isLinkedBoard if isLinkedBoard
a.js-linked-link a.js-linked-link
span.linked-icon.fa.fa-folder span.linked-icon | 📁
else if isLinkedCard else if isLinkedCard
a.js-linked-link a.js-linked-link
span.linked-icon.fa.fa-id-card span.linked-icon | 🃏
if getArchived if getArchived
span.linked-icon.linked-archived.fa.fa-archive span.linked-icon.linked-archived | 📦
+viewer +viewer
if currentBoard.allowsCardNumber if currentBoard.allowsCardNumber
span.card-number span.card-number
@ -147,7 +142,7 @@ template(name="minicard")
if canModifyCard if canModifyCard
if comments.length if comments.length
.badge(title="{{_ 'card-comments-title' 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 = comments.length
//span.badge-comment.badge-text //span.badge-comment.badge-text
@ -155,36 +150,36 @@ template(name="minicard")
if getDescription if getDescription
unless currentBoard.allowsDescriptionTextOnMinicard unless currentBoard.allowsDescriptionTextOnMinicard
.badge.badge-state-image-only(title=getDescription) .badge.badge-state-image-only(title=getDescription)
span.badge-icon.fa.fa-align-left span.badge-icon 📝
if getVoteQuestion if getVoteQuestion
.badge.badge-state-image-only(title=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-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 }} span.badge-text {{ voteCountNegative }}
if getPokerQuestion if getPokerQuestion
.badge.badge-state-image-only(title=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 if expiredPoker
span.badge-text {{ getPokerEstimation }} span.badge-text {{ getPokerEstimation }}
if attachments.length if attachments.length
if currentBoard.allowsBadgeAttachmentOnMinicard if currentBoard.allowsBadgeAttachmentOnMinicard
.badge .badge
span.badge-icon.fa.fa-paperclip span.badge-icon 📎
span.badge-text= attachments.length span.badge-text= attachments.length
if checklists.length if checklists.length
.badge(class="{{#if checklistFinished}}is-finished{{/if}}") .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}} span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
if allSubtasks.count if allSubtasks.count
.badge .badge
span.badge-icon.fa.fa-sitemap span.badge-icon 🌐
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}} span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down //{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
if currentBoard.allowsCardSortingByNumber if currentBoard.allowsCardSortingByNumber
if currentBoard.allowsCardSortingByNumberOnMinicard if currentBoard.allowsCardSortingByNumberOnMinicard
.badge .badge
span.badge-icon.fa.fa-sort span.badge-icon 🔢
span.badge-text.check-list-sort {{ sort }} span.badge-text.check-list-sort {{ sort }}
if currentBoard.allowsDescriptionTextOnMinicard if currentBoard.allowsDescriptionTextOnMinicard
if getDescription if getDescription
@ -193,7 +188,7 @@ template(name="minicard")
| {{ getDescription }} | {{ getDescription }}
if shouldShowListOnMinicard if shouldShowListOnMinicard
.minicard-list-name .minicard-list-name
i.fa.fa-list | 📋
| {{ listName }} | {{ listName }}
if $eq 'subtext-with-full-path' currentBoard.presentParentTask if $eq 'subtext-with-full-path' currentBoard.presentParentTask
.parent-subtext .parent-subtext
@ -212,50 +207,50 @@ template(name="minicardDetailsActionsPopup")
if canModifyCard if canModifyCard
li li
a.js-move-card a.js-move-card
i.fa.fa-arrow-right | ➡️
| {{_ 'moveCardPopup-title'}} | {{_ 'moveCardPopup-title'}}
li li
a.js-copy-card a.js-copy-card
i.fa.fa-copy | 📋
| {{_ 'copyCardPopup-title'}} | {{_ 'copyCardPopup-title'}}
hr hr
li li
a.js-archive a.js-archive
i.fa.fa-arrow-right | ➡️
i.fa.fa-archive | 📦
| {{_ 'archive-card'}} | {{_ 'archive-card'}}
hr hr
li li
a.js-move-card-to-top a.js-move-card-to-top
i.fa.fa-arrow-up | ⬆️
| {{_ 'moveCardToTop-title'}} | {{_ 'moveCardToTop-title'}}
li li
a.js-move-card-to-bottom a.js-move-card-to-bottom
i.fa.fa-arrow-down | ⬇️
| {{_ 'moveCardToBottom-title'}} | {{_ 'moveCardToBottom-title'}}
hr hr
li li
a.js-add-labels a.js-add-labels
i.fa.fa-tags | 🏷️
| {{_ 'card-edit-labels'}} | {{_ 'card-edit-labels'}}
li li
a.js-due-date a.js-due-date
i.fa.fa-sign-in | 📥
| {{_ 'editCardDueDatePopup-title'}} | {{_ 'editCardDueDatePopup-title'}}
li li
a.js-set-card-color a.js-set-card-color
i.fa.fa-paint-brush | 🎨
| {{_ 'setCardColorPopup-title'}} | {{_ 'setCardColorPopup-title'}}
li li
a.js-link a.js-link
i.fa.fa-link | 🔗
| {{_ 'link-card'}} | {{_ 'link-card'}}
li li
a.js-toggle-watch-card a.js-toggle-watch-card
if isWatching if isWatching
i.fa.fa-eye | 👁️
| {{_ 'unwatch'}} | {{_ 'unwatch'}}
else else
i.fa.fa-eye-slash | 👁️-slash
| {{_ 'watch'}} | {{_ 'watch'}}

View file

@ -111,55 +111,58 @@ BlazeComponent.extendComponent({
'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'), 'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
// Drag and drop file upload handlers // Drag and drop file upload handlers
'dragover .minicard'(event) { 'dragover .minicard'(event) {
event.preventDefault(); // Only prevent default for file drags to avoid interfering with sortable
event.stopPropagation(); const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
}
}, },
'dragenter .minicard'(event) { 'dragenter .minicard'(event) {
event.preventDefault(); const dataTransfer = event.originalEvent.dataTransfer;
event.stopPropagation(); if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
const card = this.data(); event.preventDefault();
const board = card.board(); event.stopPropagation();
// Only allow drag-and-drop if user can modify card and board allows attachments const card = this.data();
if (card.canModifyCard() && board && board.allowsAttachments) { const board = card.board();
// Check if the drag contains files // Only allow drag-and-drop if user can modify card and board allows attachments
const dataTransfer = event.originalEvent.dataTransfer; if (Utils.canModifyCard() && board && board.allowsAttachments) {
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
$(event.currentTarget).addClass('is-dragging-over'); $(event.currentTarget).addClass('is-dragging-over');
} }
} }
}, },
'dragleave .minicard'(event) { 'dragleave .minicard'(event) {
event.preventDefault(); const dataTransfer = event.originalEvent.dataTransfer;
event.stopPropagation(); if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
$(event.currentTarget).removeClass('is-dragging-over'); event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
}
}, },
'drop .minicard'(event) { '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; const dataTransfer = event.originalEvent.dataTransfer;
if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) { if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
return; event.preventDefault();
} event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
// Check if the drop contains files (not just text/HTML) const card = this.data();
if (!dataTransfer.types.includes('Files')) { const board = card.board();
return;
}
const files = dataTransfer.files; // Check permissions
if (files && files.length > 0) { if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
handleFileUpload(card, files); 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: // Show list name if either:
// 1. Board-wide setting is enabled, OR // 1. Board-wide setting is enabled, OR
// 2. This specific card has the setting enabled // 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 .broken-cards-null
| NULL | NULL
if getBoard.archived if getBoard.archived
i.fa.fa-archive | 📦
li.result-card-context.result-card-context-separator li.result-card-context.result-card-context-separator
= ' ' = ' '
| {{_ 'context-separator'}} | {{_ 'context-separator'}}
@ -27,7 +27,7 @@ template(name="resultCard")
.broken-cards-null .broken-cards-null
| NULL | NULL
if getSwimlane.archived if getSwimlane.archived
i.fa.fa-archive | 📦
li.result-card-context.result-card-context-separator li.result-card-context.result-card-context-separator
= ' ' = ' '
| {{_ 'context-separator'}} | {{_ 'context-separator'}}
@ -41,4 +41,4 @@ template(name="resultCard")
.broken-cards-null .broken-cards-null
| NULL | NULL
if getList.archived if getList.archived
i.fa.fa-archive | 📦

View file

@ -1,6 +1,6 @@
template(name="subtasks") template(name="subtasks")
h3.card-details-item-title h3.card-details-item-title
i.fa.fa-sitemap | 🌐
| {{_ 'subtasks'}} | {{_ 'subtasks'}}
if currentUser.isBoardAdmin if currentUser.isBoardAdmin
if toggleDeleteDialog.get if toggleDeleteDialog.get
@ -16,7 +16,7 @@ template(name="subtasks")
+addSubtaskItemForm +addSubtaskItemForm
else else
a.js-open-inlined-form(title="{{_ 'add-subtask'}}") a.js-open-inlined-form(title="{{_ 'add-subtask'}}")
i.fa.fa-plus |
template(name="subtaskDetail") template(name="subtaskDetail")
.js-subtasks.subtask .js-subtasks.subtask
@ -26,7 +26,7 @@ template(name="subtaskDetail")
.subtask-title .subtask-title
span span
if canModifyCard 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 if canModifyCard
h2.title.js-open-inlined-form.is-editable h2.title.js-open-inlined-form.is-editable
+viewer +viewer
@ -40,7 +40,7 @@ template(name="addSubtaskItemForm")
textarea.js-add-subtask-item(rows='1' autofocus dir="auto") textarea.js-add-subtask-item(rows='1' autofocus dir="auto")
.edit-controls.clearfix .edit-controls.clearfix
button.primary.confirm.js-submit-add-subtask-item-form(type="submit") {{_ 'save'}} 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") template(name="editSubtaskItemForm")
textarea.js-edit-subtask-item(rows='1' autofocus dir="auto") textarea.js-edit-subtask-item(rows='1' autofocus dir="auto")
@ -50,7 +50,7 @@ template(name="editSubtaskItemForm")
= subtask.title = subtask.title
.edit-controls.clearfix .edit-controls.clearfix
button.primary.confirm.js-submit-edit-subtask-item-form(type="submit") {{_ 'save'}} 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 }} span(title=createdAt) {{ moment createdAt }}
if canModifyCard if canModifyCard
if currentUser.isBoardAdmin if currentUser.isBoardAdmin
@ -68,7 +68,7 @@ template(name="subtasksItems")
+addSubtaskItemForm +addSubtaskItemForm
else else
a.add-subtask-item.js-open-inlined-form a.add-subtask-item.js-open-inlined-form
i.fa.fa-plus |
| {{_ 'add-subtask-item'}}... | {{_ 'add-subtask-item'}}...
template(name='subtaskItemDetail') template(name='subtaskItemDetail')
@ -92,10 +92,10 @@ template(name="subtaskActionsPopup")
ul.pop-over-list ul.pop-over-list
li li
a.js-view-subtask(title="{{ subtask.title }}") a.js-view-subtask(title="{{ subtask.title }}")
i.fa.fa-eye | 👁️
| {{_ "view-it"}} | {{_ "view-it"}}
if currentUser.isBoardAdmin if currentUser.isBoardAdmin
a.js-delete-subtask.delete-subtask a.js-delete-subtask.delete-subtask
i.fa.fa-trash | 🗑️
| {{_ "delete"}} ... | {{_ "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 .fields
.left .left
label(for="date") {{_ 'date'}} 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 .right
label(for="time") {{_ 'time'}} label(for="time") {{_ 'time'}}
input.js-time-field#time(type="text" name="time" value=showTime placeholder=timeFormat) input.js-time-field#time(type="time" name="time" value=showTime)
.js-datepicker
if error.get if error.get
.warning {{_ error.get}} .warning {{_ error.get}}
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}} button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}

View file

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

View file

@ -311,6 +311,73 @@ BlazeComponent.extendComponent({
}, },
}).register('importMapMembersAddPopup'); }).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({ 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; padding: 0;
float: left; 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 { [id^="swimlane-"] .list:first-child {
min-width: 2.5vw; min-width: 2.5vw;
} }
@ -37,7 +259,70 @@
} }
.list.list-collapsed { .list.list-collapsed {
flex: none; 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,
.list .list-composer .open-list-composer { .list .list-composer .open-list-composer {
color: #8c8c8c; color: #8c8c8c;
@ -93,9 +378,6 @@
position: relative; position: relative;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
}
.list-header .list-rotated {
} }
.list-header .list-header-watch-icon { .list-header .list-header-watch-icon {
padding-left: 10px; padding-left: 10px;
@ -121,11 +403,152 @@
color: #a6a6a6; color: #a6a6a6;
margin-right: 15px; margin-right: 15px;
} }
.list-header .list-header-uncollapse-left { .list-header .js-collapse {
color: #a6a6a6; 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 { .list-header .js-collapse:hover {
color: #a6a6a6; 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 { .list-header .list-header-collapse {
color: #a6a6a6; color: #a6a6a6;
@ -218,17 +641,22 @@
.mini-list.mobile-view { .mini-list.mobile-view {
flex: 0 0 60px; flex: 0 0 60px;
height: auto; height: auto;
width: 100%; width: 100vw;
min-width: 100%; max-width: 100vw;
border-left: 0px; min-width: 100vw;
border-left: 0px !important;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important;
} }
.list.mobile-view { .list.mobile-view {
display: contents; display: block !important;
flex-basis: auto; flex-basis: auto;
width: 100%; width: 100vw;
min-width: 100%; max-width: 100vw;
border-left: 0px; min-width: 100vw;
border-left: 0px !important;
margin: 0 !important;
padding: 0 !important;
} }
.list.mobile-view:first-child { .list.mobile-view:first-child {
margin-left: 0px; margin-left: 0px;
@ -236,9 +664,11 @@
.list.mobile-view.ui-sortable-helper { .list.mobile-view.ui-sortable-helper {
flex: 0 0 60px; flex: 0 0 60px;
height: 60px; height: 60px;
width: 100%; width: 100vw;
border-left: 0px; max-width: 100vw;
border-left: 0px !important;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important;
} }
.list.mobile-view.ui-sortable-helper .list-header.ui-sortable-handle { .list.mobile-view.ui-sortable-helper .list-header.ui-sortable-handle {
cursor: grabbing; cursor: grabbing;
@ -246,14 +676,17 @@
.list.mobile-view.placeholder { .list.mobile-view.placeholder {
flex: 0 0 60px; flex: 0 0 60px;
height: 60px; height: 60px;
width: 100%; width: 100vw;
border-left: 0px; max-width: 100vw;
border-left: 0px !important;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important;
} }
.list.mobile-view .list-body { .list.mobile-view .list-body {
padding: 15px 19px; padding: 15px 19px;
width: 100%; width: 100vw;
min-width: 100%; max-width: 100vw;
min-width: 100vw;
} }
.list.mobile-view .list-header { .list.mobile-view .list-header {
/*Updated padding values for mobile devices, this should fix text grouping issue*/ /*Updated padding values for mobile devices, this should fix text grouping issue*/
@ -262,8 +695,9 @@
min-height: 30px; min-height: 30px;
margin-top: 10px; margin-top: 10px;
align-items: center; align-items: center;
width: 100%; width: 100vw;
min-width: 100%; max-width: 100vw;
min-width: 100vw;
/* Force grid layout for iPhone */ /* Force grid layout for iPhone */
display: grid !important; display: grid !important;
grid-template-columns: 30px 1fr auto auto !important; grid-template-columns: 30px 1fr auto auto !important;
@ -339,21 +773,27 @@
align-items: initial; 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 { .mini-list {
flex: 0 0 60px; flex: 0 0 60px;
height: auto; height: auto;
width: 100%; width: 100vw;
min-width: 100%; max-width: 100vw;
border-left: 0px; min-width: 100vw;
border-left: 0px !important;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important;
} }
.list { .list {
display: contents; display: block !important;
flex-basis: auto; flex-basis: auto;
width: 100%; width: 100vw;
min-width: 100%; max-width: 100vw;
border-left: 0px; min-width: 100vw;
border-left: 0px !important;
margin: 0 !important;
padding: 0 !important;
} }
.list:first-child { .list:first-child {
margin-left: 0px; margin-left: 0px;
@ -361,9 +801,11 @@
.list.ui-sortable-helper { .list.ui-sortable-helper {
flex: 0 0 60px; flex: 0 0 60px;
height: 60px; height: 60px;
width: 100%; width: 100vw;
border-left: 0px; max-width: 100vw;
border-left: 0px !important;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important;
} }
.list.ui-sortable-helper .list-header.ui-sortable-handle { .list.ui-sortable-helper .list-header.ui-sortable-handle {
cursor: grabbing; cursor: grabbing;
@ -371,14 +813,17 @@
.list.placeholder { .list.placeholder {
flex: 0 0 60px; flex: 0 0 60px;
height: 60px; height: 60px;
width: 100%; width: 100vw;
border-left: 0px; max-width: 100vw;
border-left: 0px !important;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important;
} }
.list-body { .list-body {
padding: 15px 19px; padding: 15px 19px;
width: 100%; width: 100vw;
min-width: 100%; max-width: 100vw;
min-width: 100vw;
} }
.list-header { .list-header {
/*Updated padding values for mobile devices, this should fix text grouping issue*/ /*Updated padding values for mobile devices, this should fix text grouping issue*/
@ -387,8 +832,9 @@
min-height: 30px; min-height: 30px;
margin-top: 10px; margin-top: 10px;
align-items: center; align-items: center;
width: 100%; width: 100vw;
min-width: 100%; max-width: 100vw;
min-width: 100vw;
} }
.list-header .list-header-left-icon { .list-header .list-header-left-icon {
padding: 7px; 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}}") class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}} {{#if isMiniScreen}}mobile-view{{/if}}")
+listHeader +listHeader
+listBody +listBody
.list-resize-handle.js-list-resize-handle.nodragscroll
template(name='miniList') template(name='miniList')
a.mini-list.js-select-list.js-list(id="js-list-{{_id}}" class="{{#if isMiniScreen}}mobile-view{{/if}}") 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() { onRendered() {
const boardComponent = this.parentComponent().parentComponent(); const boardComponent = this.parentComponent().parentComponent();
// Initialize list resize functionality immediately
this.initializeListResize();
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)'; const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
const $cards = this.$('.js-minicards'); const $cards = this.$('.js-minicards');
@ -147,17 +150,13 @@ BlazeComponent.extendComponent({
}); });
this.autorun(() => { this.autorun(() => {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$cards.sortable({
handle: '.handle',
});
} else {
$cards.sortable({
handle: '.minicard',
});
}
if ($cards.data('uiSortable') || $cards.data('sortable')) { if ($cards.data('uiSortable') || $cards.data('sortable')) {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$cards.sortable('option', 'handle', '.handle');
} else {
$cards.sortable('option', 'handle', '.minicard');
}
$cards.sortable( $cards.sortable(
'option', 'option',
'disabled', 'disabled',
@ -198,20 +197,259 @@ BlazeComponent.extendComponent({
listWidth() { listWidth() {
const user = ReactiveCache.getCurrentUser(); const user = ReactiveCache.getCurrentUser();
const list = Template.currentData(); 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() { listConstraint() {
const user = ReactiveCache.getCurrentUser(); const user = ReactiveCache.getCurrentUser();
const list = Template.currentData(); 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() { autoWidth() {
const user = ReactiveCache.getCurrentUser(); const user = ReactiveCache.getCurrentUser();
const list = Template.currentData(); const list = Template.currentData();
if (!user) {
// For non-logged-in users, auto-width is disabled
return false;
}
return user.isAutoWidth(list.boardId); 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'); }).register('list');
Template.miniList.events({ Template.miniList.events({

View file

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

View file

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

View file

@ -7,12 +7,10 @@ template(name="listHeader")
else else
if isMiniScreen if isMiniScreen
if currentList if currentList
a.list-header-left-icon.fa.fa-angle-left.js-unselect-list a.list-header-left-icon.js-unselect-list
| ◀️
else else
if collapsed 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 if showCardsCountForList cards.length
br br
span.cardCount {{cardsCount}} span.cardCount {{cardsCount}}
@ -29,6 +27,10 @@ template(name="listHeader")
if showCardsCountForList cards.length if showCardsCountForList cards.length
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}} span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
else else
if collapsed
a.js-collapse(title="{{_ 'uncollapse'}}")
| ⬅️
| ➡️
div(class="{{#if collapsed}}list-rotated{{/if}}") div(class="{{#if collapsed}}list-rotated{{/if}}")
h2.list-header-name( h2.list-header-name(
title="{{ moment modifiedAt 'LLL' }}" title="{{ moment modifiedAt 'LLL' }}"
@ -45,94 +47,97 @@ template(name="listHeader")
if isMiniScreen if isMiniScreen
if currentList if currentList
if isWatching if isWatching
i.list-header-watch-icon.fa.fa-eye i.list-header-watch-icon | 👁️
div.list-header-menu div.list-header-menu
unless currentUser.isCommentOnly unless currentUser.isCommentOnly
if canSeeAddCard 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.fa.fa-navicon.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
else else
a.list-header-menu-icon.fa.fa-angle-right.js-select-list a.list-header-menu-icon.js-select-list ▶️
a.list-header-handle.handle.fa.fa-arrows.js-list-handle unless currentUser.isWorker
a.list-header-handle.handle.js-list-handle ↕️
else if currentUser.isBoardMember else if currentUser.isBoardMember
if isWatching if isWatching
i.list-header-watch-icon.fa.fa-eye i.list-header-watch-icon | 👁️
unless collapsed unless collapsed
div.list-header-menu div.list-header-menu
unless currentUser.isCommentOnly unless currentUser.isCommentOnly
//if isBoardAdmin //if isBoardAdmin
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}") // a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
if canSeeAddCard 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'}}") 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'}}") a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
if currentUser.isBoardAdmin if currentUser.isBoardMember
if isTouchScreenOrShowDesktopDragHandles unless currentUser.isCommentOnly
a.list-header-handle.handle.fa.fa-arrows.js-list-handle unless currentUser.isWorker
a.list-header-handle.handle.js-list-handle ↕️
template(name="editListTitleForm") template(name="editListTitleForm")
.list-composer .list-composer
input.list-name-input.full-line(type="text" value=title autofocus) input.list-name-input.full-line(type="text" value=title autofocus)
.edit-controls.clearfix .edit-controls.clearfix
button.primary.confirm(type="submit") {{_ 'save'}} button.primary.confirm(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form a.js-close-inlined-form
| ❌
template(name="listActionPopup") template(name="listActionPopup")
ul.pop-over-list ul.pop-over-list
li li
a.js-add-card.list-header-plus-bottom a.js-add-card.list-header-plus-bottom
i.fa.fa-plus |
i.fa.fa-arrow-down | ⬇️
| {{_ 'add-card-to-bottom-of-list'}} | {{_ 'add-card-to-bottom-of-list'}}
hr hr
ul.pop-over-list ul.pop-over-list
li li
a.js-set-list-width a.js-set-list-width
i.fa.fa-arrows-h | ↔️
| {{_ 'set-list-width'}} | {{_ 'set-list-width'}}
ul.pop-over-list ul.pop-over-list
li li
a.js-toggle-watch-list a.js-toggle-watch-list
if isWatching if isWatching
i.fa.fa-eye | 👁️
| {{_ 'unwatch'}} | {{_ 'unwatch'}}
else else
i.fa.fa-eye-slash | 🙈
| {{_ 'watch'}} | {{_ 'watch'}}
unless currentUser.isCommentOnly unless currentUser.isCommentOnly
unless currentUser.isWorker unless currentUser.isWorker
ul.pop-over-list ul.pop-over-list
li li
a.js-set-color-list a.js-set-color-list
i.fa.fa-paint-brush | 🎨
| {{_ 'set-color-list'}} | {{_ 'set-color-list'}}
ul.pop-over-list ul.pop-over-list
if cards.length if cards.length
li li
a.js-select-cards a.js-select-cards
i.fa.fa-check-square | ☑️
| {{_ 'list-select-cards'}} | {{_ 'list-select-cards'}}
if currentUser.isBoardAdmin if currentUser.isBoardAdmin
ul.pop-over-list ul.pop-over-list
li li
a.js-set-wip-limit a.js-set-wip-limit
i.fa.fa-ban | 🚫
| {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}} | {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
unless currentUser.isWorker unless currentUser.isWorker
hr hr
ul.pop-over-list ul.pop-over-list
li li
a.js-close-list a.js-close-list
i.fa.fa-arrow-right | ➡️
i.fa.fa-archive | 📦
| {{_ 'archive-list'}} | {{_ 'archive-list'}}
hr hr
ul.pop-over-list ul.pop-over-list
li li
a.js-more a.js-more
i.fa.fa-link | 🔗
| {{_ 'listMorePopup-title'}} | {{_ 'listMorePopup-title'}}
template(name="boardLists") template(name="boardLists")
@ -149,7 +154,7 @@ template(name="listMorePopup")
span.clearfix span.clearfix
span {{_ 'link-list'}} 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 }}") input.inline-input(type="text" readonly value="{{ rootUrl }}")
| {{_ 'added'}} | {{_ 'added'}}
span.date(title=list.createdAt) {{ moment createdAt 'LLL' }} span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
@ -169,7 +174,7 @@ template(name="setWipLimitPopup")
ul.pop-over-list ul.pop-over-list
li: a.js-enable-wip-limit {{_ 'enable-wip-limit'}} li: a.js-enable-wip-limit {{_ 'enable-wip-limit'}}
if isWipLimitEnabled if isWipLimitEnabled
i.fa.fa-check | ✅
if isWipLimitEnabled if isWipLimitEnabled
p p
input.wip-limit-value(type="number" value="{{ wipLimitValue }}" min="1" max="99") input.wip-limit-value(type="number" value="{{ wipLimitValue }}" min="1" max="99")
@ -197,7 +202,7 @@ template(name="setListWidthPopup")
br br
a.js-auto-width-board( a.js-auto-width-board(
title="{{#if isAutoWidth}}{{_ 'click-to-disable-auto-width'}}{{else}}{{_ 'click-to-enable-auto-width'}}{{/if}}") 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'}} span {{_ 'auto-list-width'}}
template(name="listWidthErrorPopup") template(name="listWidthErrorPopup")
@ -211,6 +216,6 @@ template(name="setListColorPopup")
// note: we use the swimlane palette to have more than just the border // 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}}") span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color) if(isSelected color)
i.fa.fa-check | ✅
button.primary.confirm.js-submit {{_ 'save'}} button.primary.confirm.js-submit {{_ 'save'}}
button.js-remove-color.negate.wide.right {{_ 'unset-color'}} 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") template(name="dueCardsHeaderBar")
if currentUser if currentUser
h1 h1
i.fa.fa-calendar | 📅
| {{_ 'dueCards-title'}} | {{_ 'dueCards-title'}}
.board-header-btns.left .board-header-btns.left
a.board-header-btn.js-due-cards-view-change(title="{{_ 'dueCardsViewChange-title'}}") a.board-header-btn.js-due-cards-view-change(title="{{_ 'dueCardsViewChange-title'}}")
i.fa.fa-caret-down | ▼
if $eq dueCardsView 'me' if $eq dueCardsView 'me'
i.fa.fa-user | 👤
| {{_ 'dueCardsViewChange-choice-me'}} | {{_ 'dueCardsViewChange-choice-me'}}
if $eq dueCardsView 'all' if $eq dueCardsView 'all'
i.fa.fa-users | 👥
| {{_ 'dueCardsViewChange-choice-all'}} | {{_ 'dueCardsViewChange-choice-all'}}
template(name="dueCardsModalTitle") template(name="dueCardsModalTitle")
if currentUser if currentUser
h2 h2
i.fa.fa-keyboard-o | ⌨️
| {{_ 'dueCards-title'}} | {{_ 'dueCards-title'}}
template(name="dueCards") template(name="dueCards")
@ -32,7 +32,16 @@ template(name="dueCards")
span.global-search-error-messages span.global-search-error-messages
= msg = msg
else 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") template(name="dueCardsViewChangePopup")
if currentUser if currentUser
@ -40,18 +49,18 @@ template(name="dueCardsViewChangePopup")
li li
with "dueCardsViewChange-choice-me" with "dueCardsViewChange-choice-me"
a.js-due-cards-view-me a.js-due-cards-view-me
i.fa.fa-user.colorful | 👤
| {{_ 'dueCardsViewChange-choice-me'}} | {{_ 'dueCardsViewChange-choice-me'}}
if $eq Utils.dueCardsView "me" if $eq Utils.dueCardsView "me"
i.fa.fa-check | ✅
hr hr
li li
with "dueCardsViewChange-choice-all" with "dueCardsViewChange-choice-all"
a.js-due-cards-view-all a.js-due-cards-view-all
i.fa.fa-users.colorful | 👥
| {{_ 'dueCardsViewChange-choice-all'}} | {{_ 'dueCardsViewChange-choice-all'}}
span.sub-name span.sub-name
+viewer +viewer
| {{_ 'dueCardsViewChange-choice-all-description' }} | {{_ 'dueCardsViewChange-choice-all-description' }}
if $eq Utils.dueCardsView "all" if $eq Utils.dueCardsView "all"
i.fa.fa-check | ✅

View file

@ -1,13 +1,6 @@
import { ReactiveCache } from '/imports/reactiveCache'; import { ReactiveCache } from '/imports/reactiveCache';
import { CardSearchPagedComponent } from '../../lib/cardSearch'; import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
import { import { TAPi18n } from '/imports/i18n';
OPERATOR_HAS,
OPERATOR_SORT,
OPERATOR_USER,
ORDER_ASCENDING,
PREDICATE_DUE_AT,
} from '../../../config/search-const';
import { QueryParams } from '../../../config/query-classes';
// const subManager = new SubsManager(); // const subManager = new SubsManager();
@ -15,7 +8,7 @@ BlazeComponent.extendComponent({
dueCardsView() { dueCardsView() {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
// console.log('sort:', Utils.dueCardsView()); // console.log('sort:', Utils.dueCardsView());
return Utils.dueCardsView(); return Utils && Utils.dueCardsView ? Utils.dueCardsView() : 'me';
}, },
events() { events() {
@ -31,6 +24,47 @@ Template.dueCards.helpers({
userId() { userId() {
return Meteor.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({ BlazeComponent.extendComponent({
@ -38,12 +72,16 @@ BlazeComponent.extendComponent({
return [ return [
{ {
'click .js-due-cards-view-me'() { 'click .js-due-cards-view-me'() {
Utils.setDueCardsView('me'); if (Utils && Utils.setDueCardsView) {
Utils.setDueCardsView('me');
}
Popup.back(); Popup.back();
}, },
'click .js-due-cards-view-all'() { 'click .js-due-cards-view-all'() {
Utils.setDueCardsView('all'); if (Utils && Utils.setDueCardsView) {
Utils.setDueCardsView('all');
}
Popup.back(); Popup.back();
}, },
}, },
@ -51,61 +89,162 @@ BlazeComponent.extendComponent({
}, },
}).register('dueCardsViewChangePopup'); }).register('dueCardsViewChangePopup');
class DueCardsComponent extends CardSearchPagedComponent { class DueCardsComponent extends BlazeComponent {
onCreated() { onCreated() {
super.onCreated(); super.onCreated();
const queryParams = new QueryParams(); this._cachedCards = null;
queryParams.addPredicate(OPERATOR_HAS, { this._cachedTimestamp = null;
field: PREDICATE_DUE_AT, this.subscriptionHandle = null;
exists: true, this.isLoading = new ReactiveVar(true);
}); this.hasResults = new ReactiveVar(false);
// queryParams[OPERATOR_LIMIT] = 5; this.searching = new ReactiveVar(false);
queryParams.addPredicate(OPERATOR_SORT, {
name: PREDICATE_DUE_AT, // Subscribe to the optimized due cards publication
order: ORDER_ASCENDING, 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') { onDestroyed() {
queryParams.addPredicate(OPERATOR_USER, ReactiveCache.getCurrentUser().username); super.onDestroyed();
if (this.subscriptionHandle) {
this.subscriptionHandle.stop();
} }
this.runGlobalSearch(queryParams);
} }
dueCardsView() { dueCardsView() {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
//console.log('sort:', Utils.dueCardsView()); //console.log('sort:', Utils.dueCardsView());
return Utils.dueCardsView(); return Utils && Utils.dueCardsView ? Utils.dueCardsView() : 'me';
} }
sortByBoard() { sortByBoard() {
return this.dueCardsView() === 'board'; 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() { dueCardsList() {
const results = this.getResults(); // Check if subscription is ready
console.log('results:', results); if (!this.subscriptionHandle || !this.subscriptionHandle.ready()) {
const cards = []; if (process.env.DEBUG === 'true') {
if (results) { console.log('dueCards client: subscription not ready');
results.forEach(card => { }
cards.push(card); 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) => { if (process.env.DEBUG === 'true') {
const x = a.dueAt === null ? new Date('2100-12-31') : a.dueAt; console.log('dueCards client: filtered to', filteredCards.length, 'cards');
const y = b.dueAt === null ? new Date('2100-12-31') : b.dueAt; }
if (x > y) return 1; // Cache the results for 5 seconds to avoid re-filtering on every render
else if (x < y) return -1; 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 return filteredCards;
console.log('cards:', cards);
return cards;
} }
} }

View file

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

View file

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

View file

@ -58,7 +58,7 @@
float: left; float: left;
overflow: hidden; overflow: hidden;
line-height: 28px; line-height: 28px;
margin: 0 2px; margin: 0 12px;
} }
#header #header-main-bar .board-header-btn i.fa { #header #header-main-bar .board-header-btn i.fa {
float: left; float: left;
@ -100,8 +100,9 @@
z-index: 1000; z-index: 1000;
padding: 10px 0px; padding: 10px 0px;
align-items: center; align-items: center;
flex-wrap: wrap; /* Allow wrapping on mobile */ flex-wrap: nowrap; /* Prevent wrapping to keep single row */
min-height: 28px; /* Allow height to grow */ min-height: 28px;
overflow: hidden; /* Prevent content from overflowing */
} }
#header-quick-access .home-icon { #header-quick-access .home-icon {
display: flex; display: flex;
@ -167,13 +168,39 @@
white-space: nowrap; white-space: nowrap;
padding: 10px; padding: 10px;
margin: -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 { #header-quick-access ul.header-quick-access-list li {
display: inline; display: inline-block; /* Keep inline-block for proper spacing */
width: auto; width: auto;
color: #d9d9d9; color: #d9d9d9;
padding: 12px 0px; padding: 12px 0px;
margin: -10px 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 { #header-quick-access ul.header-quick-access-list li a {
padding: 12px 10px; padding: 12px 10px;
@ -220,6 +247,7 @@
margin: 0; margin: 0;
margin-top: 1px; margin-top: 1px;
} }
#header-quick-access #header-user-bar .header-user-bar-name, #header-quick-access #header-user-bar .header-user-bar-name,
#header-quick-access #header-help { #header-quick-access #header-help {
margin: 4px 8px 0 0; margin: 4px 8px 0 0;
@ -314,7 +342,8 @@
} }
/* Make zoom input wider on all mobile screens */ /* 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 { #header-quick-access .zoom-controls .zoom-input {
min-width: 50px !important; /* Wider on mobile */ min-width: 50px !important; /* Wider on mobile */
width: 50px !important; /* Fixed width to show all numbers */ width: 50px !important; /* Fixed width to show all numbers */
@ -424,7 +453,8 @@
margin: 6px 5px 0; margin: 6px 5px 0;
width: 12px; 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 { #header #header-main-bar {
height: 40px; height: 40px;
} }
@ -446,6 +476,8 @@
transition: background-color 0.4s; transition: background-color 0.4s;
width: 100%; width: 100%;
z-index: 30; z-index: 30;
flex-wrap: nowrap !important; /* Force single row on mobile */
overflow: hidden; /* Prevent content overflow */
} }
/* Mobile home icon styling */ /* Mobile home icon styling */
@ -489,11 +521,12 @@
screen and (max-width: 800px) and (orientation: portrait), screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) { screen and (max-width: 800px) and (orientation: landscape) {
#header-quick-access { #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 */ min-height: 48px !important; /* Minimum height for mobile */
flex-wrap: wrap !important; /* Force wrapping */ flex-wrap: nowrap !important; /* Force single row */
align-items: flex-start !important; /* Align to top when wrapping */ align-items: center !important; /* Center align items */
padding: 8px 0px !important; /* Adjust padding for mobile */ padding: 8px 0px !important; /* Adjust padding for mobile */
overflow: hidden !important; /* Prevent content overflow */
} }
#header-quick-access { #header-quick-access {
font-size: 2em !important; /* 2x bigger base font size */ 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 // Home icon - always at left side of logo
span.home-icon.allBoards span.home-icon.allBoards
a(href="{{pathFor 'home'}}") a(href="{{pathFor 'home'}}")
span.fa.fa-home | 🏠
| {{_ 'all-boards'}} | {{_ 'all-boards'}}
// Logo - always visible in desktop mode // Logo - visible; on mobile constrained by CSS
unless currentSetting.hideLogo unless currentSetting.hideLogo
if currentSetting.customTopLeftCornerLogoImageUrl if currentSetting.customTopLeftCornerLogoImageUrl
if currentSetting.customTopLeftCornerLogoLinkUrl if currentSetting.customTopLeftCornerLogoLinkUrl
@ -80,14 +80,16 @@ template(name="header")
.mobile-mode-toggle .mobile-mode-toggle
a.board-header-btn.js-mobile-mode-toggle(title="{{_ 'mobile-desktop-toggle'}}" class="{{#if mobileMode}}mobile-active{{else}}desktop-active{{/if}}") 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.mobile-icon(class="{{#if mobileMode}}active{{/if}}") 📱
i.fa.fa-desktop.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}") i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}") 🖥️
// Notifications
+notifications +notifications
if currentSetting.customHelpLinkUrl if currentSetting.customHelpLinkUrl
#header-help #header-help
a(href="{{currentSetting.customHelpLinkUrl}}", title="{{_ 'help'}}", target="_blank", rel="noopener noreferrer") a(href="{{currentSetting.customHelpLinkUrl}}", title="{{_ 'help'}}", target="_blank", rel="noopener noreferrer")
span.fa.fa-question | ❓
+headerUserBar +headerUserBar
@ -106,15 +108,15 @@ template(name="header")
if hasAnnouncement if hasAnnouncement
.announcement .announcement
p p
i.fa.fa-bullhorn | 📢
+viewer +viewer
| #{announcement} | #{announcement}
i.fa.fa-times-circle.js-close-announcement | ❌
template(name="offlineWarning") template(name="offlineWarning")
.offline-warning .offline-warning
p p
i.fa.fa-warning | ⚠️
| {{_ 'app-is-offline'}} | {{_ 'app-is-offline'}}
a.app-try-reconnect {{_ 'app-try-reconnect'}} a.app-try-reconnect {{_ 'app-try-reconnect'}}

View file

@ -104,6 +104,9 @@ Template.header.events({
const currentMode = Utils.getMobileMode(); const currentMode = Utils.getMobileMode();
Utils.setMobileMode(!currentMode); Utils.setMobileMode(!currentMode);
}, },
'click .js-open-bookmarks'(evt) {
// Already added but ensure single definition -- safe guard
},
'click .js-close-announcement'() { 'click .js-close-announcement'() {
$('.announcement').hide(); $('.announcement').hide();
}, },
@ -124,6 +127,14 @@ Template.header.events({
location.reload(); 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({ Template.offlineWarning.events({

View file

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

View file

@ -52,9 +52,15 @@ input,
select, select,
textarea, textarea,
button { button {
font: clamp(12px, 2.5vw, 16px) Roboto, Poppins, "Helvetica Neue", Arial, Helvetica, sans-serif; font: clamp(14px, 2.5vw, 18px) Roboto, Poppins, "Helvetica Neue", Arial, Helvetica, sans-serif;
line-height: 1.3; line-height: 1.4;
color: #4d4d4d; 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 { html {
font-size: 100%; font-size: 100%;
@ -460,20 +466,291 @@ a:not(.disabled).is-active i.fa {
.no-scrollbars::-webkit-scrollbar { .no-scrollbars::-webkit-scrollbar {
display: none !important; 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 { #content {
margin: 1px 0px 0px 0px; margin: 1px 0px 0px 0px;
height: calc(100% - 0px); height: calc(100% - 0px);
/* Improve touch scrolling */
-webkit-overflow-scrolling: touch;
} }
#content > .wrapper { #content > .wrapper {
margin-top: 0px; margin-top: 0px;
padding: 8px;
} }
.wrapper { .wrapper {
height: calc(100% - 31px); height: calc(100% - 31px);
margin: 0px; margin: 0px;
padding: 8px;
} }
.panel-default { .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 { .inline-input {

View file

@ -2,7 +2,7 @@ template(name="main")
html(lang="{{TAPi18n.getLanguage}}") html(lang="{{TAPi18n.getLanguage}}")
head head
title 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") meta(http-equiv="X-UA-Compatible" content="IE=edge")
//- XXX We should use pathFor in the following `href` to support the case //- 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 where the application is deployed with a path prefix, but it seems to be
@ -77,19 +77,21 @@ template(name="defaultLayout")
| {{{afterBodyStart}}} | {{{afterBodyStart}}}
+Template.dynamic(template=content) +Template.dynamic(template=content)
| {{{beforeBodyEnd}}} | {{{beforeBodyEnd}}}
+migrationProgress
+boardConversionProgress
if (Modal.isOpen) if (Modal.isOpen)
#modal #modal
.overlay .overlay
if (Modal.isWide) if (Modal.isWide)
.modal-content-wide.modal-container .modal-content-wide.modal-container
a.modal-close-btn.js-close-modal a.modal-close-btn.js-close-modal
i.fa.fa-times-thin | ❌
+Template.dynamic(template=Modal.getHeaderName) +Template.dynamic(template=Modal.getHeaderName)
+Template.dynamic(template=Modal.getTemplateName) +Template.dynamic(template=Modal.getTemplateName)
else else
.modal-content.modal-container .modal-content.modal-container
a.modal-close-btn.js-close-modal a.modal-close-btn.js-close-modal
i.fa.fa-times-thin | ❌
+Template.dynamic(template=Modal.getHeaderName) +Template.dynamic(template=Modal.getHeaderName)
+Template.dynamic(template=Modal.getTemplateName) +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'))) { if (loginInput && loginInput.name && (loginInput.name.toLowerCase().includes('user') || loginInput.name.toLowerCase().includes('email'))) {
loginInput.setAttribute('autocomplete', 'username 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 h1
//a.back-btn(href="{{pathFor 'home'}}") //a.back-btn(href="{{pathFor 'home'}}")
// i.fa.fa-chevron-left // i.fa.fa-chevron-left
i.fa.fa-list | 📋
| {{_ 'my-cards'}} | {{_ 'my-cards'}}
.board-header-btns.left .board-header-btns.left
a.board-header-btn.js-my-cards-view-change(title="{{_ 'myCardsViewChange-title'}}") a.board-header-btn.js-my-cards-view-change(title="{{_ 'myCardsViewChange-title'}}")
i.fa.fa-caret-down | ▼
if $eq myCardsView 'boards' if $eq myCardsView 'boards'
i.fa.fa-trello | 📋
| {{_ 'myCardsViewChange-choice-boards'}} | {{_ 'myCardsViewChange-choice-boards'}}
if $eq myCardsView 'table' if $eq myCardsView 'table'
i.fa.fa-table | 📊
| {{_ 'myCardsViewChange-choice-table'}} | {{_ 'myCardsViewChange-choice-table'}}
template(name="myCardsModalTitle") template(name="myCardsModalTitle")
if currentUser if currentUser
h2 h2
i.fa.fa-keyboard-o | ⌨️
| {{_ 'my-cards'}} | {{_ 'my-cards'}}
template(name="myCards") template(name="myCards")
@ -102,15 +102,15 @@ template(name="myCardsViewChangePopup")
li li
with "myCardsViewChange-choice-boards" with "myCardsViewChange-choice-boards"
a.js-my-cards-view-boards a.js-my-cards-view-boards
i.fa.fa-trello.colorful | 📋
| {{_ 'myCardsViewChange-choice-boards'}} | {{_ 'myCardsViewChange-choice-boards'}}
if $eq Utils.myCardsView "boards" if $eq Utils.myCardsView "boards"
i.fa.fa-check | ✅
hr hr
li li
with "myCardsViewChange-choice-table" with "myCardsViewChange-choice-table"
a.js-my-cards-view-table a.js-my-cards-view-table
i.fa.fa-table.colorful | 📊
| {{_ 'myCardsViewChange-choice-table'}} | {{_ 'myCardsViewChange-choice-table'}}
if $eq Utils.myCardsView "table" if $eq Utils.myCardsView "table"
i.fa.fa-check | ✅

View file

@ -5,7 +5,8 @@
border-bottom-color: #c2c2c2; border-bottom-color: #c2c2c2;
box-shadow: 0 0.2vh 0.8vh rgba(0,0,0,0.3); box-shadow: 0 0.2vh 0.8vh rgba(0,0,0,0.3);
position: absolute; position: absolute;
width: min(300px, 40vw); /* Wider default to fit full color palette */
width: min(380px, 55vw);
z-index: 99999; z-index: 99999;
margin-top: 0.7vh; margin-top: 0.7vh;
} }
@ -72,23 +73,321 @@
} }
.pop-over .content-wrapper { .pop-over .content-wrapper {
width: 100%; 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 { .pop-over .content-container {
width: 5000px; width: 100%;
max-height: 70vh; max-height: calc(70vh + 20px);
transition: transform 0.2s; 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 { .pop-over .content-container .content {
width: min(280px, 37vw); /* Match wider popover, leave padding */
width: 100%;
padding: 0 1.3vw 1.3vh; 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 { .pop-over .content-container .content.no-height {
height: 2.5vh; height: 0;
} overflow: hidden;
.pop-over .quiet { padding: 0;
/* padding: 6px 6px 4px;*/ margin: 0;
visibility: hidden;
} }
.pop-over.search-over { .pop-over.search-over {
background: #f0f0f0; background: #f0f0f0;
@ -104,7 +403,7 @@
.pop-over .at-form .at-error, .pop-over .at-form .at-error,
.pop-over .at-form .at-result { .pop-over .at-form .at-result {
padding: 8px 12px; padding: 8px 12px;
margin: -8px -10px 10px; margin: 0 0 10px 0;
} }
.pop-over .at-form .at-error { .pop-over .at-form .at-error {
background: #ef9a9a; background: #ef9a9a;
@ -148,7 +447,7 @@
font-weight: 700; font-weight: 700;
padding: 1.5px 10px; padding: 1.5px 10px;
position: relative; position: relative;
margin: 0 -10px; margin: 0;
text-decoration: none; text-decoration: none;
overflow: hidden; overflow: hidden;
line-height: 33px; line-height: 33px;
@ -307,12 +606,12 @@
margin: 48px 0px 0px 0px; margin: 48px 0px 0px 0px;
} }
.pop-over .content-container { .pop-over .content-container {
width: 1000%; width: 100%;
height: 100%; height: 100%;
max-height: 100%; max-height: 100%;
} }
.pop-over .content-container .content { .pop-over .content-container .content {
width: calc(10% - 20px); width: calc(100% - 20px);
height: calc(100% - 20px); height: calc(100% - 20px);
padding: 10px; padding: 10px;
} }
@ -334,21 +633,21 @@
margin: 0px 0px; margin: 0px 0px;
} }
.pop-over .popup-container-depth-1 { .pop-over .popup-container-depth-1 {
transform: translateX(-10%); transform: none !important;
} }
.pop-over .popup-container-depth-2 { .pop-over .popup-container-depth-2 {
transform: translateX(-20%); transform: none !important;
} }
.pop-over .popup-container-depth-3 { .pop-over .popup-container-depth-3 {
transform: translateX(-30%); transform: none !important;
} }
.pop-over .popup-container-depth-4 { .pop-over .popup-container-depth-4 {
transform: translateX(-40%); transform: none !important;
} }
.pop-over .popup-container-depth-5 { .pop-over .popup-container-depth-5 {
transform: translateX(-50%); transform: none !important;
} }
.pop-over .popup-container-depth-6 { .pop-over .popup-container-depth-6 {
transform: translateX(-60%); transform: none !important;
} }
} }

View file

@ -2,13 +2,13 @@
class="{{#unless title}}miniprofile{{/unless}}" class="{{#unless title}}miniprofile{{/unless}}"
class=currentBoard.colorClass class=currentBoard.colorClass
class="{{#unless title}}no-title{{/unless}}" 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 .header
a.back-btn.js-back-view(class="{{#unless hasPopupParent}}is-hidden{{/unless}}") a.back-btn.js-back-view(class="{{#unless hasPopupParent}}is-hidden{{/unless}}")
i.fa.fa-chevron-left | ◀️
span.header-title= title span.header-title= title
a.close-btn.js-close-pop-over a.close-btn.js-close-pop-over
i.fa.fa-times-thin | ❌
.content-wrapper .content-wrapper
//- //-
We display the all stack of popup content next to each other and move 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') template(name='notifications')
#notifications.board-header-btns.right #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' if $.Session.get 'showNotificationsDrawer'
+notificationsDrawer(unreadNotifications=unreadNotifications) +notificationsDrawer(unreadNotifications=unreadNotifications)

View file

@ -10,7 +10,7 @@ template(name="boardActions")
div.trigger-text div.trigger-text
| {{_'r-its-list'}} | {{_'r-its-list'}}
div.trigger-button.js-add-gen-move-action.js-goto-rules div.trigger-button.js-add-gen-move-action.js-goto-rules
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -38,7 +38,7 @@ template(name="boardActions")
div.trigger-dropdown div.trigger-dropdown
input(id="swimlaneName",type=text,placeholder="{{_'r-name'}}") input(id="swimlaneName",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-spec-move-action.js-goto-rules div.trigger-button.js-add-spec-move-action.js-goto-rules
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -49,7 +49,7 @@ template(name="boardActions")
div.trigger-text div.trigger-text
| {{_'r-card'}} | {{_'r-card'}}
div.trigger-button.js-add-arch-action.js-goto-rules div.trigger-button.js-add-arch-action.js-goto-rules
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -58,7 +58,7 @@ template(name="boardActions")
div.trigger-dropdown div.trigger-dropdown
input(id="swimlane-name",type=text,placeholder="{{_'r-name'}}") input(id="swimlane-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-swimlane-action.js-goto-rules div.trigger-button.js-add-swimlane-action.js-goto-rules
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -75,7 +75,7 @@ template(name="boardActions")
div.trigger-dropdown div.trigger-dropdown
input(id="swimlane-name2",type=text,placeholder="{{_'r-name'}}") input(id="swimlane-name2",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-create-card-action.js-goto-rules div.trigger-button.js-create-card-action.js-goto-rules
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -99,7 +99,7 @@ template(name="boardActions")
div.trigger-dropdown div.trigger-dropdown
input(id="swimlaneName-link",type=text,placeholder="{{_'r-name'}}") input(id="swimlaneName-link",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-link-card-action.js-goto-rules 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 div.trigger-text
| {{_'r-to-current-datetime'}} | {{_'r-to-current-datetime'}}
div.trigger-button.js-set-date-action.js-goto-rules div.trigger-button.js-set-date-action.js-goto-rules
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -30,7 +30,7 @@ template(name="cardActions")
option(value="endAt") {{_'r-df-end-at'}} option(value="endAt") {{_'r-df-end-at'}}
option(value="receivedAt") {{_'r-df-received-at'}} option(value="receivedAt") {{_'r-df-received-at'}}
div.trigger-button.js-remove-datevalue-action.js-goto-rules div.trigger-button.js-remove-datevalue-action.js-goto-rules
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -46,7 +46,7 @@ template(name="cardActions")
option(value="#{_id}") option(value="#{_id}")
= name = name
div.trigger-button.js-add-label-action.js-goto-rules div.trigger-button.js-add-label-action.js-goto-rules
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -59,14 +59,14 @@ template(name="cardActions")
div.trigger-dropdown div.trigger-dropdown
input(id="member-name",type=text,placeholder="{{_'r-name'}}") input(id="member-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-member-action.js-goto-rules div.trigger-button.js-add-member-action.js-goto-rules
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
div.trigger-text div.trigger-text
| {{_'r-remove-all'}} | {{_'r-remove-all'}}
div.trigger-button.js-add-removeall-action.js-goto-rules div.trigger-button.js-add-removeall-action.js-goto-rules
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -77,12 +77,12 @@ template(name="cardActions")
class="card-details-{{cardColorButton}}") class="card-details-{{cardColorButton}}")
| {{_ cardColorButtonText }} | {{_ cardColorButtonText }}
div.trigger-button.js-set-color-action.js-goto-rules div.trigger-button.js-set-color-action.js-goto-rules
i.fa.fa-plus |
template(name="setCardActionsColorPopup") template(name="setCardActionsColorPopup")
form.edit-label form.edit-label
.palette-colors: each colors .palette-colors: each colors
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}") span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color) if(isSelected color)
i.fa.fa-check | ✅
button.primary.confirm.js-submit {{_ 'save'}} button.primary.confirm.js-submit {{_ 'save'}}

View file

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

View file

@ -8,4 +8,4 @@ template(name="mailActions")
input(id="email-subject",type=text,placeholder="{{_'r-subject'}}") input(id="email-subject",type=text,placeholder="{{_'r-subject'}}")
textarea(id="email-msg") textarea(id="email-msg")
div.trigger-button.trigger-button-email.js-mail-action.js-goto-rules 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") template(name="ruleDetails")
.rules .rules
h2 h2
i.fa.fa-magic | ✨
| {{_ 'r-rule-details' }} | {{_ 'r-rule-details' }}
.triggers-content .triggers-content
.triggers-body .triggers-body
@ -20,5 +20,5 @@ template(name="ruleDetails")
= action = action
div.rules-back div.rules-back
button.js-goback button.js-goback
i.fa.fa-chevron-left | ◀️
| {{_ 'back'}} | {{_ 'back'}}

View file

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

View file

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

View file

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

View file

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

View file

@ -10,14 +10,14 @@ template(name="cardTriggers")
div.trigger-text div.trigger-text
| {{_'r-a-card'}} | {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user | 👤
div.user-details.hide-element div.user-details.hide-element
div.trigger-text div.trigger-text
| {{_'r-by'}} | {{_'r-by'}}
div.trigger-dropdown div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}") input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-label-trigger.js-goto-action div.trigger-button.js-add-gen-label-trigger.js-goto-action
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -37,14 +37,14 @@ template(name="cardTriggers")
div.trigger-text div.trigger-text
| {{_'r-a-card'}} | {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user | 👤
div.user-details.hide-element div.user-details.hide-element
div.trigger-text div.trigger-text
| {{_'r-by'}} | {{_'r-by'}}
div.trigger-dropdown div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}") input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-spec-label-trigger.js-goto-action div.trigger-button.js-add-spec-label-trigger.js-goto-action
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -57,14 +57,14 @@ template(name="cardTriggers")
div.trigger-text div.trigger-text
| {{_'r-a-card'}} | {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user | 👤
div.user-details.hide-element div.user-details.hide-element
div.trigger-text div.trigger-text
| {{_'r-by'}} | {{_'r-by'}}
div.trigger-dropdown div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}") input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-member-trigger.js-goto-action div.trigger-button.js-add-gen-member-trigger.js-goto-action
i.fa.fa-plus |
div.trigger-item div.trigger-item
@ -82,14 +82,14 @@ template(name="cardTriggers")
div.trigger-text div.trigger-text
| {{_'r-a-card'}} | {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user | 👤
div.user-details.hide-element div.user-details.hide-element
div.trigger-text div.trigger-text
| {{_'r-by'}} | {{_'r-by'}}
div.trigger-dropdown div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}") input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-spec-member-trigger.js-goto-action div.trigger-button.js-add-spec-member-trigger.js-goto-action
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -104,11 +104,11 @@ template(name="cardTriggers")
div.trigger-text div.trigger-text
| {{_'r-a-card'}} | {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user | 👤
div.user-details.hide-element div.user-details.hide-element
div.trigger-text div.trigger-text
| {{_'r-by'}} | {{_'r-by'}}
div.trigger-dropdown div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}") input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-attachment-trigger.js-goto-action 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 div.trigger-text
| {{_'r-a-card'}} | {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user | 👤
div.user-details.hide-element div.user-details.hide-element
div.trigger-text div.trigger-text
| {{_'r-by'}} | {{_'r-by'}}
div.trigger-dropdown div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}") input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-check-trigger.js-goto-action div.trigger-button.js-add-gen-check-trigger.js-goto-action
i.fa.fa-plus |
div.trigger-item div.trigger-item
@ -35,14 +35,14 @@ template(name="checklistTriggers")
div.trigger-text div.trigger-text
| {{_'r-a-card'}} | {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user | 👤
div.user-details.hide-element div.user-details.hide-element
div.trigger-text div.trigger-text
| {{_'r-by'}} | {{_'r-by'}}
div.trigger-dropdown div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}") input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-spec-check-trigger.js-goto-action div.trigger-button.js-add-spec-check-trigger.js-goto-action
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -53,14 +53,14 @@ template(name="checklistTriggers")
option(value="completed") {{_'r-completed'}} option(value="completed") {{_'r-completed'}}
option(value="uncompleted") {{_'r-made-incomplete'}} option(value="uncompleted") {{_'r-made-incomplete'}}
div.trigger-button.trigger-button-person.js-show-user-field div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user | 👤
div.user-details.hide-element div.user-details.hide-element
div.trigger-text div.trigger-text
| {{_'r-by'}} | {{_'r-by'}}
div.trigger-dropdown div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}") input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-comp-trigger.js-goto-action div.trigger-button.js-add-gen-comp-trigger.js-goto-action
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -75,14 +75,14 @@ template(name="checklistTriggers")
option(value="completed") {{_'r-completed'}} option(value="completed") {{_'r-completed'}}
option(value="uncompleted") {{_'r-made-incomplete'}} option(value="uncompleted") {{_'r-made-incomplete'}}
div.trigger-button.trigger-button-person.js-show-user-field div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user | 👤
div.user-details.hide-element div.user-details.hide-element
div.trigger-text div.trigger-text
| {{_'r-by'}} | {{_'r-by'}}
div.trigger-dropdown div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}") input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-spec-comp-trigger.js-goto-action div.trigger-button.js-add-spec-comp-trigger.js-goto-action
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -93,14 +93,14 @@ template(name="checklistTriggers")
option(value="checked") {{_'r-checked'}} option(value="checked") {{_'r-checked'}}
option(value="unchecked") {{_'r-unchecked'}} option(value="unchecked") {{_'r-unchecked'}}
div.trigger-button.trigger-button-person.js-show-user-field div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user | 👤
div.user-details.hide-element div.user-details.hide-element
div.trigger-text div.trigger-text
| {{_'r-by'}} | {{_'r-by'}}
div.trigger-dropdown div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}") input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-check-item-trigger.js-goto-action div.trigger-button.js-add-gen-check-item-trigger.js-goto-action
i.fa.fa-plus |
div.trigger-item div.trigger-item
div.trigger-content div.trigger-content
@ -115,11 +115,11 @@ template(name="checklistTriggers")
option(value="checked") {{_'r-checked'}} option(value="checked") {{_'r-checked'}}
option(value="unchecked") {{_'r-unchecked'}} option(value="unchecked") {{_'r-unchecked'}}
div.trigger-button.trigger-button-person.js-show-user-field div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user | 👤
div.user-details.hide-element div.user-details.hide-element
div.trigger-text div.trigger-text
| {{_'r-by'}} | {{_'r-by'}}
div.trigger-dropdown div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}") input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-spec-check-item-trigger.js-goto-action 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 ul
li li
a.js-report-broken(data-id="report-broken") a.js-report-broken(data-id="report-broken")
i.fa.fa-chain-broken | 🔗
| {{_ 'broken-cards'}} | {{_ 'broken-cards'}}
li li
a.js-report-files(data-id="report-files") a.js-report-files(data-id="report-files")
i.fa.fa-paperclip | 📎
| {{_ 'filesReportTitle'}} | {{_ 'filesReportTitle'}}
li li
a.js-report-rules(data-id="report-rules") a.js-report-rules(data-id="report-rules")
i.fa.fa-magic | ✨
| {{_ 'rulesReportTitle'}} | {{_ 'rulesReportTitle'}}
li li
a.js-report-boards(data-id="report-boards") a.js-report-boards(data-id="report-boards")
i.fa.fa-magic | ✨
| {{_ 'boardsReportTitle'}} | {{_ 'boardsReportTitle'}}
li li
a.js-report-cards(data-id="report-cards") a.js-report-cards(data-id="report-cards")
i.fa.fa-magic | ✨
| {{_ 'cardsReportTitle'}} | {{_ 'cardsReportTitle'}}
.main-body .main-body

View file

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

View file

@ -1,33 +1,74 @@
template(name="attachmentSettings") template(name="attachmentSettings")
.setting-content.attachment-settings-content ul#attachment-setting.setting-detail
unless currentUser.isAdmin li
| {{_ 'error-notAuthorized'}} h3 {{_ 'attachment-storage-configuration'}}
else .form-group
.content-body label {{_ 'writable-path'}}
.side-menu input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
ul small.form-text.text-muted {{_ 'filesystem-path-description'}}
li
a.js-attachment-storage-settings(data-id="storage-settings") .form-group
i.fa.fa-cog label {{_ 'attachments-path'}}
| {{_ 'attachment-storage-settings'}} input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
li small.form-text.text-muted {{_ 'attachments-path-description'}}
a.js-attachment-migration(data-id="attachment-migration")
i.fa.fa-arrow-right .form-group
| {{_ 'attachment-migration'}} label {{_ 'avatars-path'}}
li input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
a.js-attachment-monitoring(data-id="attachment-monitoring") small.form-text.text-muted {{_ 'avatars-path-description'}}
i.fa.fa-chart-line
| {{_ 'attachment-monitoring'}}
.main-body li
if loading.get h3 {{_ 'mongodb-gridfs-storage'}}
+spinner .form-group
else if showStorageSettings.get label {{_ 'gridfs-enabled'}}
+storageSettings input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
else if showMigration.get small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
+attachmentMigration
else if showMonitoring.get li
+attachmentMonitoring 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") template(name="storageSettings")
.storage-settings .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 ul
li li
a.js-move-attachments(data-id="move-attachments") a.js-move-attachments(data-id="move-attachments")
i.fa.fa-arrow-right | ➡️
| {{_ 'attachment-move'}} | {{_ 'attachment-move'}}
.main-body .main-body
@ -80,17 +80,17 @@ template(name="moveAttachment")
td td
if $neq version.storageName "fs" if $neq version.storageName "fs"
button.js-move-storage-fs button.js-move-storage-fs
i.fa.fa-arrow-right | ➡️
| {{_ 'attachment-move-storage-fs'}} | {{_ 'attachment-move-storage-fs'}}
if $neq version.storageName "gridfs" if $neq version.storageName "gridfs"
if version.storageName if version.storageName
button.js-move-storage-gridfs button.js-move-storage-gridfs
i.fa.fa-arrow-right | ➡️
| {{_ 'attachment-move-storage-gridfs'}} | {{_ 'attachment-move-storage-gridfs'}}
if $neq version.storageName "s3" if $neq version.storageName "s3"
if version.storageName if version.storageName
button.js-move-storage-s3 button.js-move-storage-s3
i.fa.fa-arrow-right | ➡️
| {{_ 'attachment-move-storage-s3'}} | {{_ '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