Compare commits

...
184 Commits
Author SHA1 Message Date
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 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. 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.
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. 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. 2025-10-17 21:55:49 +03:00
Lauri Ojansivu b8942b728f v8.05 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. 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.
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. 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. 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.
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 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. 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. 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. 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
Lauri Ojansivu ea30612013 v8.01 2025-10-11 12:32:38 +03:00
Lauri Ojansivu 4445612bb8 Updated translations. 2025-10-11 12:30:09 +03:00
Lauri Ojansivu da56393078 Updated ChangeLog. 2025-10-11 12:15:25 +03:00
Lauri Ojansivu a86ff1e8d0 Fixed attachments and minicard related bugs that prevented WeKan starting.
Thanks to xet7 !
2025-10-11 12:13:11 +03:00
Lauri Ojansivu 7fce5650b8 Updated ChangeLog. 2025-10-11 11:55:41 +03:00
Lauri Ojansivu b949357e66 Snap MongoDB Server 3-8 support. Part 1.
Thanks to xet7 !
2025-10-11 11:54:14 +03:00
Lauri Ojansivu 8e71b68dbf Updated ChangeLog. 2025-10-11 11:39:39 +03:00
Lauri Ojansivu b77eed221f Fix count of Orgs Teams People at Admin Panel.
Thanks to xet7 !
2025-10-11 11:38:28 +03:00
Lauri Ojansivu 1fa91474d0 Updated ChangeLog. 2025-10-11 11:34:45 +03:00
Lauri Ojansivu fc32a89292 Fixed per-card and per-board settings of showing checkist at minicard.
Thanks to xet7 !
2025-10-11 11:31:57 +03:00
Lauri Ojansivu ea24010298 Updated ChangeLog. 2025-10-11 11:08:27 +03:00
Lauri Ojansivu ae1f80a52c Added attachments API and admin panel attachment management for file storage backends settings. Fixed drag drop upload attachments from file manager to minicard or opened card.
Thanks to xet7 !
2025-10-11 11:05:46 +03:00
Lauri Ojansivu 2f95431c9b Updated ChangeLog. 2025-10-11 10:50:05 +03:00
Lauri Ojansivu d59683eff1 Fixed attachments migrations at Admin Panel to not use too much CPU while migrating attachments.
Thanks to xet7 !
2025-10-11 10:48:12 +03:00
Lauri Ojansivu de77776cd0 Updated ChangeLog. 2025-10-11 10:33:52 +03:00
Lauri Ojansivu 74ccfea570 Add support for MongoDB 3-8, detecting which one is in use.
Thanks to xet7 !
2025-10-11 10:32:20 +03:00
Lauri Ojansivu 1f5a549589 Updated ChangeLog. 2025-10-11 10:17:41 +03:00
Lauri Ojansivu 3ccdc2e307 Made possible to start WeKan immediately without running any database migrations.
Thanks to xet7 !
2025-10-11 10:15:08 +03:00
Lauri Ojansivu 688b725807 v8.00 2025-10-11 07:50:28 +03:00
Lauri Ojansivu ae01ea576c Snap: Migrate MongoDB from 3 to 7 only when "snap set wekan migrate-mongodb='true'". Not automatically.
Thanks to xet7 !
2025-10-11 07:42:51 +03:00
437 changed files with 46549 additions and 11542 deletions

No files matched your search

+3 -3
View File
@@ -52,8 +52,8 @@ ongoworks:speakingurl
raix:handlebar-helpers
http@2.0.0! # force new http package
# Datepicker
wekan-bootstrap-datepicker
# Datepicker (disabled - using native HTML inputs)
# wekan-bootstrap-datepicker
# UI components
ostrio:i18n
@@ -93,4 +93,4 @@ ejson@1.1.3
logging@1.3.3
wekan-fullcalendar
momentjs:moment@2.29.3
wekan-fontawesome
# wekan-fontawesome
-2
View File
@@ -155,8 +155,6 @@ wekan-accounts-cas@0.1.0
wekan-accounts-lockout@1.0.0
wekan-accounts-oidc@1.0.10
wekan-accounts-sandstorm@0.8.0
wekan-bootstrap-datepicker@1.10.0
wekan-fontawesome@6.4.2
wekan-fullcalendar@3.10.5
wekan-ldap@0.0.2
wekan-markdown@1.0.9
+234 -1
View File
@@ -19,6 +19,239 @@ Fixing other platforms In Progress.
[Upgrade WeKan](https://wekan.fi/upgrade/)
WeKan 8.00-8.06 had wrong raw database directory setting /var/snap/wekan/common/wekan and some cards were not visible.
Those are fixed at WeKan 8.07 where database directory is back to /var/snap/wekan/common and all cards are visible.
# v8.09 2025-10-21 WeKan ® release
This release fixes the following bugs:
- [Fix Admin Panel / People editing and layout](https://github.com/wekan/wekan/commit/7a585a3dfb080af51f88669ea5928f715779cee4).
Thanks to xet7.
- [Fix upgrade to 8.08 duplicates lists](https://github.com/wekan/wekan/commit/c3a405222782a4a91eb8725faaa8309f0926dcc4).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.08 2025-10-21 WeKan ® release
This release fixes the following bugs:
- [Fix opening board migration of Shared Lists to Per-Swimlane lists to use ReactiveCache correctly without errors](https://github.com/wekan/wekan/commit/9536e60bd1c77c8a22e89d2eb2968e11da3a28cd).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.07 2025-10-20 WeKan ® release
This release fixes the following bugs:
- [Fix Snap Candidate WeKan 8.00-8.06 commit ae01ea5 database directory from /var/snap/wekan/common/wekan back to 8.07 /var/snap/wekan/common](https://github.com/wekan/wekan/commit/98f141d62f3b6d4371d024c72eae6688d0f4e516).
Thanks to xet7.
- [When opening board, add missing lists](https://github.com/wekan/wekan/commit/80777b46638ed15b8194105751499ada4b066d19).
Thanks to xet7.
- [If Snap Candidate MongoDB raw database files were at SNAP_COMMON/wekan, migrate them back to SNAP_COMMON](https://github.com/wekan/wekan/commit/f2019b1059c8d6f4cd9a46c3db7e004c4928cebb).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.06 2025-10-20 WeKan ® release
This release adds the following new features:
- [At Public Board, drag resize list width and swimlane height. For logged in users, fix adding labels](https://github.com/wekan/wekan/commit/351433524708e9a7ccb4795d9ca31a78904943ea).
Thanks to xet7.
- [When opening board, migrate from Shared Lists to Per-Swimlane Lists](https://github.com/wekan/wekan/commit/1e6252de7f26f3af14a99fb63b5dac27ba0576f3).
Thanks to xet7.
- [Added Date Format setting to Opened Card](https://github.com/wekan/wekan/commit/2dd3916f7ee3df10bd88643cf2c796cb166b3044).
Thanks to xet7.
and fixes the following bugs:
- [Fix add and drag drop attachments to minicards and card](https://github.com/wekan/wekan/commit/b06daff4c7e63453643459f7d8798fde97e3200c).
Thanks to xet7.
- [Fix starred, archive and clone icons](https://github.com/wekan/wekan/pull/5953).
Thanks to helioguardabaxo.
- Fix Due dates to be color coded and have unicode icons.
[Part 1](https://github.com/wekan/wekan/commit/d965faa3174dc81636106e6f81435b2750b0625f),
[Part 2](https://github.com/wekan/wekan/commit/101048339bdd1e45f876aeb1aa5ec32ceda28139).
Thanks to xet7.
- [Fix unable to see My Due Cards](https://github.com/wekan/wekan/commit/66b444e2b0c9b2ed5f98cd1ff0cd9222b2d0c624).
Thanks to xet7.
- Fix drag drop lists.
[Part 1](https://github.com/wekan/wekan/commit/324f3f7794aace800022a24deb5fd5fb36ebd384),
[Part 2](https://github.com/wekan/wekan/commit/ff516ec696ef499f11b04b30053eeb9d3f96d8d1).
Thanks to xet7.
- [Removed extra pipe characters](https://github.com/wekan/wekan/commit/caa6e615ff3c3681bf2b470a625eb39c6009b825).
Thanks to xet7.
- [Fix syntax error at migrations](https://github.com/wekan/wekan/commit/eb6b42c4c9f99894fd93e62c9b3fceda3429c96c).
Thanks to xet7.
- [Fix opened card attachments button text to be at tooltip, not at opened card](https://github.com/wekan/wekan/commit/1e53125499ef563ca3c65f786ac3525e5f50274c).
Thanks to xet7.
- [Fix Broken Hyperlinks in Markdown to HTML conversion](https://github.com/wekan/wekan/commit/973a49526fdf22c143468d3d9db64269b1defa7d).
Thanks to xet7.
- [Fix migrations](https://github.com/wekan/wekan/commit/0acbf30b0346f49c0ee8f5161fb00b4eca8e1a0c).
Thanks to xet7.
- [Fix card popup to use HTML date, not anymore JQuery date](https://github.com/wekan/wekan/commit/2d44881619d78e8ef4c5060d17e9035f5babd778).
Thanks to xet7.
- [Fix Bug: Scale of Minicard icons is linked to horizontal screensize](https://github.com/wekan/wekan/commit/b6b0c5fe6d7dbd37926c662f96f2e3653cabd867).
Thanks to xet7.
- [Fix Bug Member settings drops to the second line and overlaps when many boards are starred as favourites](https://github.com/wekan/wekan/commit/46d46e313cbb8d9c3e4a976ec27b5141c266050f).
Thanks to xet7.
- [Some mobile view fixes](https://github.com/wekan/wekan/commit/c4af4d03acc02f3e54e91f2a65bce2f88742b1a6).
Thanks to xet7.
- [Have all iPhone use mobile view by default, while still having possibility to use mobile/desktop switch button for desktop mode](https://github.com/wekan/wekan/commit/5df4efd7ba06e618e454f068df05885306283bb1).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.05 2025-10-17 WeKan ® release
This release fixes the following bugs:
- [Show original positions of swimlanes, lists and cards](https://github.com/wekan/wekan/commit/2543df94252c2789fb484ae52b9a6ff298252ceb).
Thanks to xet7.
- Fix popups issues at Edit Avatar, Archive card confirm, etc.
[Part 1](https://github.com/wekan/wekan/commit/87ae085e6d0a56a2083eec819cf7d795d3e51e1a),
[Part 2](https://github.com/wekan/wekan/commit/386aea7c788d6eaf9d486ead4d81453401adf390).
Thanks to xet7.
- [Changed wekan-boostrap-datepicker to HTML datepicker](https://github.com/wekan/wekan/commit/79b94824efedaa9e256de931fd26398eb2838d6a).
Thanks to xet7.
- [Replaced moment.js with Javascript date](https://github.com/wekan/wekan/commit/cb6afe67a7363af89663ba17392dc5f90a15f703).
Thanks to xet7.
- [Convert Font Awesome to Unicode Icons. Part 1. In Progress](https://github.com/wekan/wekan/commit/2947238a021b6952b56e828d49a8c0094520d89a).
Thanks to xet7.
- [Resize height of swimlane by dragging. Font Awesome to Unicode icons](https://github.com/wekan/wekan/commit/09631d6b0c1b8e3bbc3bf45d4bb65449b46f1288).
Thanks to xet7.
- [Removed not needed visible text from mobile desktop switch button](https://github.com/wekan/wekan/commit/62ede481966107405460f6d5b90f292c98bae254).
Thanks to xet7.
- Font Awesome to Unicode icons.
[Part 3](https://github.com/wekan/wekan/commit/3af94c2a9059a399b9f9946c387caff892ace2f9).
[Part 4](https://github.com/wekan/wekan/commit/088bc16072ea0dd02aa2dec6a2e3e9aed00a3cc9).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.04 2025-10-16 WeKan ® release
This release fixes the following bugs:
- [Make sure that all cards are visible](https://github.com/wekan/wekan/commit/6b848b318d62afe9772218febdb09c7426774f60).
Thanks to xet7.
- [Fix wide screen](https://github.com/wekan/wekan/commit/f08c7702eecf23588f7bc023beefb453edd704c6).
Thanks to xet7.
- Fix popups positioning.
[Part 1](https://github.com/wekan/wekan/commit/77eea4d494e5db8e2c0e59732bcea73aa163bc13),
[Part 1](https://github.com/wekan/wekan/commit/00ddec75754bbbccc6fb9b3096495b9609246480).
Thanks to xet7.
- [Remove using fork with MongoDB at Snap](https://github.com/wekan/wekan/commit/690481c138f9629054180310dd172295c7f6d34e).
Thanks to xet7.
- [Use only MongoDB 7 at Snap](https://github.com/wekan/wekan/commit/79e83e33ec1dcec4eea81d5fb4a9f7381c176a12).
Thanks to xet7.
- [Removed extra npm packages](https://github.com/wekan/wekan/commit/dd88483ec7526eee4a97bac5f09e03985be5d923).
Thanks to xet7.
- [Try to fix Broken Hyperlinks in Markdown to HTML conversion](https://github.com/wekan/wekan/commit/bbbd3abf06e45a3fa57c4aa987d87f1873eb11d6).
Thanks to xet7.
- [Disable not working minio and s3 support temporarily](https://github.com/wekan/wekan/commit/4283b5b0e330930fff1fa2bb73c355a4ffb4cda0).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.03 2025-10-14 WeKan ® release
This release fixes the following bugs:
- [Fix Snap MongoDB to not fork at systemd, so it stays running](https://github.com/wekan/wekan/commit/5792a869594b4c79a93db414b95a13d60013193b).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.02 2025-10-14 WeKan ® release
This release adds the following new features:
- [Run database migrations when opening board. Not when upgrading WeKan](https://github.com/wekan/wekan/commit/2b5c56484a4dd559f062ef892fd5248a903b2a10).
Thanks to xet7.
- [Added Cron Manager to Admin Panel for long running jobs, like running migrations when opening board, copying or moving boards swimlanes lists cards etc](https://github.com/wekan/wekan/commit/da68b01502afc9d5d9ea1267bee9fc98bb08b611).
Thanks to xet7.
- [If there is no cron jobs running, run migrations for boards that have not been opened yet](https://github.com/wekan/wekan/commit/317138ab7209a41715336ea8251df45f11a6d173).
Thanks to xet7.
- [Accessibility improvements](https://github.com/wekan/wekan/commit/67b078b8056ec9851caaf6ef855719de1e6d966d).
Thanks to xet7.
- [Change list width by dragging between lists](https://github.com/wekan/wekan/commit/abad8cc4d5dded0f5e1a80892a3b29aa71404a5c).
Thanks to xet7.
and adds the following updates:
- [Updated dependencies](https://github.com/wekan/wekan/commit/5bc03b23ea34816d8e1135cbe9ed5f18a2573854).
Thanks to developers of dependencies.
and fixes the following bugs:
- [Fixes to make board showing correctly](https://github.com/wekan/wekan/commit/bd8c565415998c9aaded821988d591105258b378).
Thanks to xet7.
- [Fix opening sidebar](https://github.com/wekan/wekan/commit/0fd781e80aaf841c26ce59caffc579b9c391330f).
Thanks to xet7.
- [Fix Admin Panel menus "Attachment Settings" and "Cron Settings" and make them translateable](https://github.com/wekan/wekan/commit/033919a2702fa6959b8f8c87f076d3f255ace6ba).
Thanks to xet7.
- Change Admin Panel "Attachment Settings" and "Cron Settings" options to be tabs, not submenu.
[Part 1](https://github.com/wekan/wekan/commit/ae2aa1f5cd2511e80e12a91426eb91bb968dff98),
[Part 2](https://github.com/wekan/wekan/commit/5a6faafa30fefcd5dd0af7cc52b847a54d538065),
[Part 3](https://github.com/wekan/wekan/commit/2148aeea42f69fa367bf8c451d7f1c3a63b52880).
Thanks to xet7.
- [Fixed Error in migrate-lists-to-per-swimlane migration](https://github.com/wekan/wekan/commit/cc99da5357fb1fc00e3b5aece20c57917f88301b).
Thanks to xet7.
- Fix Admin Panel Settings menu to show Attachments and Cron options correctly.
[Part 1](https://github.com/wekan/wekan/e0013b9b631eb16861b1cfdb25386bf8e9099b4e),
[Part 2](https://github.com/wekan/wekan/7bb1e24bda2ed9db0bad0fafcf256680c2c05e8a).
- [Fixed migrations](https://github.com/wekan/wekan/commit/63c314ca185aeda650c01b4a67fcde1067320d22).
Thanks to xet7.
- [Removed not needed console log message](https://github.com/wekan/wekan/commit/0a34ee1b6437dcfd65e31d9bbc9f3ccfa5718ba9).
Thanks to xet7.
- [Updated mobile Bookmarks/Starred boards. Part 1. In Progress](https://github.com/wekan/wekan/commit/da98942cce37363d6062695d3c4cf7e2df796cac).
Thanks to xet7.
- [Fix drag drop reorder swimlanes](https://github.com/wekan/wekan/commit/a4518bbefc99be74f7ccfdbb9fdf902007ca90f3).
Thanks to xet7.
- [Try to fix swimlane hamburger menu popup positioning. In progress](https://github.com/wekan/wekan/commit/d4f13de1d978b271d05e1d67d40e3c1c14761578).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.01 2025-10-11 WeKan ® release
This release adds the following new features:
- [Made possible to start WeKan immediately without running any database migrations](https://github.com/wekan/wekan/commit/3ccdc2e3070978a79bc68941375122504318677d).
Thanks to xet7.
- [Add support for MongoDB 3-8 client, detecting which one is in use](https://github.com/wekan/wekan/commit/74ccfea5703b08b2166593d1bb8e1d981970cec0).
Thanks to xet7.
- [Add support for Snap MongoDB Server 3-8 support. Part 1](https://github.com/wekan/wekan/commit/b949357e665d97604cbecb04a9211319ac4e13c2).
Thanks to xet7.
- [Added attachments API and admin panel attachment management for file storage backends settings. Fixed drag drop upload attachments from file manager to minicard or opened card](https://github.com/wekan/wekan/commit/ae1f80a52cde09689dddb1209708630b4949b7ee).
Thanks to xet7.
and fixes the following bugs:
- [Fixed attachments migrations at Admin Panel to not use too much CPU while migrating attachments](https://github.com/wekan/wekan/commit/d59683eff1267ff87a6aef9ae36c7aebbe10eaa1).
Thanks to xet7.
- [Fixed per-card and per-board settings of showing checkist at minicard](https://github.com/wekan/wekan/commit/fc32a89292e4d314bd3868739f6e01bc2bc17018).
Thanks to xet7.
- [Fix count of Orgs Teams People at Admin Panel](https://github.com/wekan/wekan/commit/b77eed221f9e5fc657234a6de3e11c27f4de3d38).
Thanks to xet7.
- [Fixed attachments and minicard related bugs that prevented WeKan starting](https://github.com/wekan/wekan/commit/a86ff1e8d03d330a7ea0702cf3677859ed552509).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.00 2025-10-11 WeKan ® release
This release adds the following new features:
- [Snap: Migrate MongoDB from 3 to 7 only when "snap set wekan migrate-mongodb='true'". Not automatically](https://github.com/wekan/wekan/commit/ae01ea576c3073acbc181a5b19008ddaea4700e1).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v7.99 2025-10-11 WeKan ® release
This release fixed the following bugs:
@@ -55,7 +288,7 @@ and adds the following new features:
- [Mobile one board per row. Board zoom size percent. Board toggle mobile/desktop mode. In Progress](https://github.com/wekan/wekan/commit/752699d1c2fb8ea9ff0f3ec9ae0b2b776443d826).
Thanks to xet7.
- [Drag any files from file manager to minicard or opened card.
- Drag any files from file manager to minicard or opened card.
[Part 1](https://github.com/wekan/wekan/commit/3e9481c5bd2c02ba501bd0a6ef1d1e6ce82bb1d9),
[Part 2](https://github.com/wekan/wekan/commit/cdd7d69c660d0b6ac06b7b75d4f59985b8a9322a).
Thanks to xet7.
+3 -3
View File
@@ -249,9 +249,9 @@ cd /home/wekan/app
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
#rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
#mv /home/wekan/app_build/bundle /build
wget "https://github.com/wekan/wekan/releases/download/v7.99/wekan-7.99-amd64.zip"
unzip wekan-7.99-amd64.zip
rm wekan-7.99-amd64.zip
wget "https://github.com/wekan/wekan/releases/download/v8.09/wekan-8.09-amd64.zip"
unzip wekan-8.09-amd64.zip
rm wekan-8.09-amd64.zip
mv /home/wekan/app/bundle /build
# Put back the original tar
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v7.99.0"
appVersion: "v8.09.0"
files:
userUploads:
- README.md
+276
View File
@@ -39,6 +39,12 @@ If *nix: chmod +x api.py => ./api.py users
python3 api.py addcustomfieldtoboard AUTHORID BOARDID NAME TYPE SETTINGS SHOWONCARD AUTOMATICALLYONCARD SHOWLABELONMINICARD SHOWSUMATTOPOFLIST # Add Custom Field to Board
python3 api.py editcustomfield BOARDID LISTID CARDID CUSTOMFIELDID NEWCUSTOMFIELDVALUE # Edit Custom Field
python3 api.py listattachments BOARDID # List attachments
python3 api.py uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND] # Upload attachment to card
python3 api.py downloadattachment ATTACHMENTID OUTPUTPATH # Download attachment to local file
python3 api.py attachmentinfo ATTACHMENTID # Get attachment information
python3 api.py listcardattachments BOARDID SWIMLANEID LISTID CARDID # List attachments for specific card
python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move] # Copy or move attachment
python3 api.py deleteattachment ATTACHMENTID # Delete attachment
python3 api.py cardsbyswimlane SWIMLANEID LISTID # Retrieve cards list on a swimlane
python3 api.py getcard BOARDID LISTID CARDID # Get card info
python3 api.py addlabel BOARDID LISTID CARDID LABELID # Add label to a card
@@ -750,3 +756,273 @@ if arguments == 1:
data2 = body.text.replace('}',"}\n")
print(data2)
# ------- LIST OF PUBLIC BOARDS END -----------
# ------- NEW ATTACHMENT API ENDPOINTS START -----------
if sys.argv[1] == 'uploadattachment':
# ------- UPLOAD ATTACHMENT START -----------
if arguments < 5:
print("Usage: python3 api.py uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND]")
print("Storage backends: fs, gridfs, s3")
exit(1)
boardid = sys.argv[2]
swimlaneid = sys.argv[3]
listid = sys.argv[4]
cardid = sys.argv[5]
filepath = sys.argv[6]
storage_backend = sys.argv[7] if arguments > 6 else None
# Read file and convert to base64
try:
with open(filepath, 'rb') as f:
file_data = f.read()
import base64
base64_data = base64.b64encode(file_data).decode('utf-8')
except FileNotFoundError:
print(f"Error: File '{filepath}' not found")
exit(1)
except Exception as e:
print(f"Error reading file: {e}")
exit(1)
# Get file info
import os
filename = os.path.basename(filepath)
import mimetypes
file_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
# Prepare request data
upload_data = {
'boardId': boardid,
'swimlaneId': swimlaneid,
'listId': listid,
'cardId': cardid,
'fileData': base64_data,
'fileName': filename,
'fileType': file_type
}
if storage_backend:
upload_data['storageBackend'] = storage_backend
# Make API call
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
upload_url = wekanurl + 'api/attachment/upload'
try:
response = requests.post(upload_url, headers=headers, json=upload_data)
response.raise_for_status()
result = response.json()
print(f"Upload successful!")
print(f"Attachment ID: {result.get('attachmentId')}")
print(f"File: {result.get('fileName')}")
print(f"Size: {result.get('fileSize')} bytes")
print(f"Storage: {result.get('storageBackend')}")
except requests.exceptions.RequestException as e:
print(f"Upload failed: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}")
# ------- UPLOAD ATTACHMENT END -----------
if sys.argv[1] == 'downloadattachment':
# ------- DOWNLOAD ATTACHMENT START -----------
if arguments < 3:
print("Usage: python3 api.py downloadattachment ATTACHMENTID OUTPUTPATH")
exit(1)
attachmentid = sys.argv[2]
outputpath = sys.argv[3]
# Make API call
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
download_url = wekanurl + f'api/attachment/download/{attachmentid}'
try:
response = requests.get(download_url, headers=headers)
response.raise_for_status()
result = response.json()
if result.get('success'):
# Decode base64 data and save to file
import base64
file_data = base64.b64decode(result.get('base64Data'))
with open(outputpath, 'wb') as f:
f.write(file_data)
print(f"Download successful!")
print(f"File saved to: {outputpath}")
print(f"Original filename: {result.get('fileName')}")
print(f"Size: {result.get('fileSize')} bytes")
print(f"Storage: {result.get('storageBackend')}")
else:
print(f"Download failed: {result.get('message', 'Unknown error')}")
except requests.exceptions.RequestException as e:
print(f"Download failed: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}")
# ------- DOWNLOAD ATTACHMENT END -----------
if sys.argv[1] == 'attachmentinfo':
# ------- ATTACHMENT INFO START -----------
if arguments < 2:
print("Usage: python3 api.py attachmentinfo ATTACHMENTID")
exit(1)
attachmentid = sys.argv[2]
# Make API call
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
info_url = wekanurl + f'api/attachment/info/{attachmentid}'
try:
response = requests.get(info_url, headers=headers)
response.raise_for_status()
result = response.json()
if result.get('success'):
print("=== ATTACHMENT INFO ===")
print(f"Attachment ID: {result.get('attachmentId')}")
print(f"File Name: {result.get('fileName')}")
print(f"File Size: {result.get('fileSize')} bytes")
print(f"File Type: {result.get('fileType')}")
print(f"Storage Backend: {result.get('storageBackend')}")
print(f"Board ID: {result.get('boardId')}")
print(f"Swimlane ID: {result.get('swimlaneId')}")
print(f"List ID: {result.get('listId')}")
print(f"Card ID: {result.get('cardId')}")
print(f"Created At: {result.get('createdAt')}")
print(f"Is Image: {result.get('isImage')}")
print(f"Versions: {len(result.get('versions', []))}")
else:
print(f"Failed to get attachment info: {result.get('message', 'Unknown error')}")
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}")
# ------- ATTACHMENT INFO END -----------
if sys.argv[1] == 'listcardattachments':
# ------- LIST CARD ATTACHMENTS START -----------
if arguments < 5:
print("Usage: python3 api.py listcardattachments BOARDID SWIMLANEID LISTID CARDID")
exit(1)
boardid = sys.argv[2]
swimlaneid = sys.argv[3]
listid = sys.argv[4]
cardid = sys.argv[5]
# Make API call
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
list_url = wekanurl + f'api/attachment/list/{boardid}/{swimlaneid}/{listid}/{cardid}'
try:
response = requests.get(list_url, headers=headers)
response.raise_for_status()
result = response.json()
if result.get('success'):
attachments = result.get('attachments', [])
print(f"=== CARD ATTACHMENTS ({len(attachments)}) ===")
for attachment in attachments:
print(f"ID: {attachment.get('attachmentId')}")
print(f"Name: {attachment.get('fileName')}")
print(f"Size: {attachment.get('fileSize')} bytes")
print(f"Type: {attachment.get('fileType')}")
print(f"Storage: {attachment.get('storageBackend')}")
print(f"Created: {attachment.get('createdAt')}")
print("---")
else:
print(f"Failed to list attachments: {result.get('message', 'Unknown error')}")
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}")
# ------- LIST CARD ATTACHMENTS END -----------
if sys.argv[1] == 'copymoveattachment':
# ------- COPY/MOVE ATTACHMENT START -----------
if arguments < 6:
print("Usage: python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move]")
exit(1)
attachmentid = sys.argv[2]
targetboardid = sys.argv[3]
targetswimlaneid = sys.argv[4]
targetlistid = sys.argv[5]
targetcardid = sys.argv[6]
operation = sys.argv[7] if arguments > 6 else 'copy'
if operation not in ['copy', 'move']:
print("Operation must be 'copy' or 'move'")
exit(1)
# Prepare request data
request_data = {
'attachmentId': attachmentid,
'targetBoardId': targetboardid,
'targetSwimlaneId': targetswimlaneid,
'targetListId': targetlistid,
'targetCardId': targetcardid
}
# Make API call
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
api_url = wekanurl + f'api/attachment/{operation}'
try:
response = requests.post(api_url, headers=headers, json=request_data)
response.raise_for_status()
result = response.json()
if result.get('success'):
print(f"{operation.capitalize()} successful!")
if operation == 'copy':
print(f"Source Attachment ID: {result.get('sourceAttachmentId')}")
print(f"New Attachment ID: {result.get('newAttachmentId')}")
else:
print(f"Attachment ID: {result.get('attachmentId')}")
print(f"Source Board: {result.get('sourceBoardId')}")
print(f"Target Board: {result.get('targetBoardId')}")
print(f"File: {result.get('fileName')}")
print(f"Size: {result.get('fileSize')} bytes")
else:
print(f"{operation.capitalize()} failed: {result.get('message', 'Unknown error')}")
except requests.exceptions.RequestException as e:
print(f"{operation.capitalize()} failed: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}")
# ------- COPY/MOVE ATTACHMENT END -----------
if sys.argv[1] == 'deleteattachment':
# ------- DELETE ATTACHMENT START -----------
if arguments < 2:
print("Usage: python3 api.py deleteattachment ATTACHMENTID")
exit(1)
attachmentid = sys.argv[2]
# Make API call
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
delete_url = wekanurl + f'api/attachment/delete/{attachmentid}'
try:
response = requests.delete(delete_url, headers=headers)
response.raise_for_status()
result = response.json()
if result.get('success'):
print("Delete successful!")
print(f"Attachment ID: {result.get('attachmentId')}")
print(f"File: {result.get('fileName')}")
else:
print(f"Delete failed: {result.get('message', 'Unknown error')}")
except requests.exceptions.RequestException as e:
print(f"Delete failed: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}")
# ------- DELETE ATTACHMENT END -----------
# ------- NEW ATTACHMENT API ENDPOINTS END -----------
+11
View File
@@ -4,3 +4,14 @@ if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/pwa-service-worker.js');
});
}
// Import board converter for on-demand conversion
import '/client/lib/boardConverter';
import '/client/components/boardConversionProgress';
// Import migration manager and progress UI
import '/client/lib/migrationManager';
import '/client/components/migrationProgress';
// Import cron settings
import '/client/components/settings/cronSettings';
@@ -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;
}
}
@@ -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'}}
@@ -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();
});
});
+9 -1
View File
@@ -294,7 +294,8 @@
min-width: 100%;
}
@media screen and (max-width: 800px) {
@media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
.board-wrapper {
width: 100% !important;
min-width: 100% !important;
@@ -496,3 +497,10 @@
font-size: 25px;
cursor: pointer;
}
/* Global file drag over state for board canvas */
.board-canvas.file-drag-over {
background-color: rgba(0, 123, 255, 0.05) !important;
border: 2px dashed #007bff !important;
transition: all 0.2s ease;
}
+18 -5
View File
@@ -1,5 +1,10 @@
template(name="board")
if isBoardReady.get
if isMigrating.get
+migrationProgress
else if isConverting.get
+boardConversionProgress
else if isBoardReady.get
if currentBoard
if onlyShowCurrentCard
+cardDetails(currentCard)
@@ -16,6 +21,10 @@ template(name="boardBody")
if notDisplayThisBoard
| {{_ 'tableVisibilityMode-allowPrivateOnly'}}
else
// Debug information (remove in production)
if debugBoardState
.debug-info(style="position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.8); color: white; padding: 10px; z-index: 9999; font-size: 12px;")
| Board: {{currentBoard.title}} | View: {{boardView}} | HasSwimlanes: {{hasSwimlanes}} | Swimlanes: {{currentBoard.swimlanes.length}}
.board-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
.board-canvas.js-swimlanes(
class="{{#if hasSwimlanes}}dragscroll{{/if}}"
@@ -34,15 +43,19 @@ template(name="boardBody")
each currentBoard.swimlanes
+swimlane(this)
else
a.js-empty-board-add-swimlane(title="{{_ 'add-swimlane'}}")
h1.big-message.quiet
| {{_ 'add-swimlane'}} +
// Fallback: If no swimlanes exist, show lists instead of empty message
+listsGroup(currentBoard)
else if isViewLists
+listsGroup(currentBoard)
else if isViewCalendar
+calendarView
else
+listsGroup(currentBoard)
// Default view - show swimlanes if they exist, otherwise show lists
if hasSwimlanes
each currentBoard.swimlanes
+swimlane(this)
else
+listsGroup(currentBoard)
+sidebar
template(name="calendarView")
+485 -58
View File
@@ -1,6 +1,11 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import dragscroll from '@wekanteam/dragscroll';
import { boardConverter } from '/client/lib/boardConverter';
import { migrationManager } from '/client/lib/migrationManager';
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
import Swimlanes from '/models/swimlanes';
import Lists from '/models/lists';
const subManager = new SubsManager();
const { calculateIndex } = Utils;
@@ -9,6 +14,9 @@ const swimlaneWhileSortingHeight = 150;
BlazeComponent.extendComponent({
onCreated() {
this.isBoardReady = new ReactiveVar(false);
this.isConverting = new ReactiveVar(false);
this.isMigrating = new ReactiveVar(false);
this._swimlaneCreated = new Set(); // Track boards where we've created swimlanes
// The pattern we use to manually handle data loading is described here:
// https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
@@ -17,22 +25,338 @@ BlazeComponent.extendComponent({
this.autorun(() => {
const currentBoardId = Session.get('currentBoard');
if (!currentBoardId) return;
const handle = subManager.subscribe('board', currentBoardId, false);
Tracker.nonreactive(() => {
Tracker.autorun(() => {
this.isBoardReady.set(handle.ready());
if (handle.ready()) {
// Ensure default swimlane exists (only once per board)
this.ensureDefaultSwimlane(currentBoardId);
// Check if board needs conversion
this.checkAndConvertBoard(currentBoardId);
} else {
this.isBoardReady.set(false);
}
});
});
});
},
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) {
const swimlaneId = Swimlanes.insert({
title: 'Default',
boardId: 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;
}
// Check if board needs migration based on migration version
const needsMigration = !board.migrationVersion || board.migrationVersion < 1;
if (needsMigration) {
// Start background migration for old boards
this.isMigrating.set(true);
await this.startBackgroundMigration(boardId);
this.isMigrating.set(false);
}
// Check if board needs conversion (for old structure)
if (boardConverter.isBoardConverted(boardId)) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been converted, skipping conversion`);
}
this.isBoardReady.set(true);
} else {
const needsConversion = boardConverter.needsConversion(boardId);
if (needsConversion) {
this.isConverting.set(true);
const success = await boardConverter.convertBoard(boardId);
this.isConverting.set(false);
if (success) {
this.isBoardReady.set(true);
} else {
console.error('Board conversion failed, setting ready to true anyway');
this.isBoardReady.set(true); // Still show board even if conversion failed
}
} else {
this.isBoardReady.set(true);
}
}
// Convert shared lists to per-swimlane lists if needed
await this.convertSharedListsToPerSwimlane(boardId);
// Fix missing lists migration (for cards with wrong listId references)
await this.fixMissingLists(boardId);
// Start attachment migration in background if needed
this.startAttachmentMigrationIfNeeded(boardId);
} catch (error) {
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
}
},
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
Meteor.call('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) {
// 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}, 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
Meteor.call('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) {
console.log(`Successfully fixed missing lists for board ${boardId}: created ${result.createdLists} lists, updated ${result.updatedCards} cards`);
}
} catch (error) {
console.error('Error fixing missing lists:', error);
}
},
async startAttachmentMigrationIfNeeded(boardId) {
try {
// Check if board has already been migrated
if (attachmentMigrationManager.isBoardMigrated(boardId)) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been migrated, skipping`);
}
return;
}
// Check if there are unconverted attachments
const unconvertedAttachments = attachmentMigrationManager.getUnconvertedAttachments(boardId);
if (unconvertedAttachments.length > 0) {
if (process.env.DEBUG === 'true') {
console.log(`Starting attachment migration for ${unconvertedAttachments.length} attachments in board ${boardId}`);
}
await attachmentMigrationManager.startAttachmentMigration(boardId);
} else {
// No attachments to migrate, mark board as migrated
// This will be handled by the migration manager itself
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has no attachments to migrate`);
}
}
} catch (error) {
console.error('Error starting attachment migration:', error);
}
},
onlyShowCurrentCard() {
return Utils.isMiniScreen() && Utils.getCurrentCardId(true);
const isMiniScreen = Utils.isMiniScreen();
const currentCardId = Utils.getCurrentCardId(true);
return isMiniScreen && currentCardId;
},
goHome() {
FlowRouter.go('home');
},
isConverting() {
return this.isConverting.get();
},
isMigrating() {
return this.isMigrating.get();
},
isBoardReady() {
return this.isBoardReady.get();
},
currentBoard() {
return Utils.getCurrentBoard();
},
}).register('board');
BlazeComponent.extendComponent({
@@ -46,33 +370,37 @@ BlazeComponent.extendComponent({
// fix swimlanes sort field if there are null values
const currentBoardData = Utils.getCurrentBoard();
const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
if (nullSortSwimlanes.length > 0) {
const swimlanes = currentBoardData.swimlanes();
let count = 0;
swimlanes.forEach(s => {
Swimlanes.update(s._id, {
$set: {
sort: count,
},
if (currentBoardData && Swimlanes) {
const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
if (nullSortSwimlanes.length > 0) {
const swimlanes = currentBoardData.swimlanes();
let count = 0;
swimlanes.forEach(s => {
Swimlanes.update(s._id, {
$set: {
sort: count,
},
});
count += 1;
});
count += 1;
});
}
}
// fix lists sort field if there are null values
const nullSortLists = currentBoardData.nullSortLists();
if (nullSortLists.length > 0) {
const lists = currentBoardData.lists();
let count = 0;
lists.forEach(l => {
Lists.update(l._id, {
$set: {
sort: count,
},
if (currentBoardData && Lists) {
const nullSortLists = currentBoardData.nullSortLists();
if (nullSortLists.length > 0) {
const lists = currentBoardData.lists();
let count = 0;
lists.forEach(l => {
Lists.update(l._id, {
$set: {
sort: count,
},
});
count += 1;
});
count += 1;
});
}
}
},
onRendered() {
@@ -98,11 +426,16 @@ BlazeComponent.extendComponent({
}
}
// Observe for new popups/menus and set focus
// Observe for new popups/menus and set focus (but exclude swimlane content)
const popupObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1 && (node.classList.contains('popup') || node.classList.contains('modal') || node.classList.contains('menu'))) {
if (node.nodeType === 1 &&
(node.classList.contains('popup') || node.classList.contains('modal') || node.classList.contains('menu')) &&
!node.closest('.js-swimlanes') &&
!node.closest('.swimlane') &&
!node.closest('.list') &&
!node.closest('.minicard')) {
setTimeout(function() { focusFirstInteractive(node); }, 10);
}
});
@@ -380,22 +713,20 @@ BlazeComponent.extendComponent({
// Always reset dragscroll on view switch
dragscroll.reset();
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$swimlanesDom.sortable({
handle: '.js-swimlane-header-handle',
});
} else {
$swimlanesDom.sortable({
handle: '.swimlane-header',
});
}
if ($swimlanesDom.data('uiSortable') || $swimlanesDom.data('sortable')) {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$swimlanesDom.sortable('option', 'handle', '.js-swimlane-header-handle');
} else {
$swimlanesDom.sortable('option', 'handle', '.swimlane-header');
}
// Disable drag-dropping if the current user is not a board member
$swimlanesDom.sortable(
'option',
'disabled',
!ReactiveCache.getCurrentUser()?.isBoardAdmin(),
);
// Disable drag-dropping if the current user is not a board member
$swimlanesDom.sortable(
'option',
'disabled',
!ReactiveCache.getCurrentUser()?.isBoardAdmin(),
);
}
});
// If there is no data in the board (ie, no lists) we autofocus the list
@@ -412,51 +743,122 @@ BlazeComponent.extendComponent({
notDisplayThisBoard() {
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
let currentBoard = Utils.getCurrentBoard();
if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard.permission == 'public') {
return true;
}
return false;
return allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard && currentBoard.permission == 'public';
},
isViewSwimlanes() {
const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) {
return (currentUser.profile || {}).boardView === 'board-view-swimlanes';
boardView = (currentUser.profile || {}).boardView;
} else {
return (
window.localStorage.getItem('boardView') === 'board-view-swimlanes'
);
boardView = window.localStorage.getItem('boardView');
}
},
hasSwimlanes() {
return Utils.getCurrentBoard().swimlanes().length > 0;
// If no board view is set, default to swimlanes
if (!boardView) {
boardView = 'board-view-swimlanes';
}
return boardView === 'board-view-swimlanes';
},
isViewLists() {
const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) {
return (currentUser.profile || {}).boardView === 'board-view-lists';
boardView = (currentUser.profile || {}).boardView;
} else {
return window.localStorage.getItem('boardView') === 'board-view-lists';
boardView = window.localStorage.getItem('boardView');
}
return boardView === 'board-view-lists';
},
isViewCalendar() {
const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) {
return (currentUser.profile || {}).boardView === 'board-view-cal';
boardView = (currentUser.profile || {}).boardView;
} else {
return window.localStorage.getItem('boardView') === 'board-view-cal';
boardView = window.localStorage.getItem('boardView');
}
return boardView === 'board-view-cal';
},
hasSwimlanes() {
const currentBoard = Utils.getCurrentBoard();
if (!currentBoard) {
if (process.env.DEBUG === 'true') {
console.log('hasSwimlanes: No current board');
}
return false;
}
try {
const swimlanes = currentBoard.swimlanes();
const hasSwimlanes = swimlanes && swimlanes.length > 0;
if (process.env.DEBUG === 'true') {
console.log('hasSwimlanes: Board has', swimlanes ? swimlanes.length : 0, 'swimlanes');
}
return hasSwimlanes;
} catch (error) {
console.error('hasSwimlanes: Error getting swimlanes:', error);
return false;
}
},
isVerticalScrollbars() {
const user = ReactiveCache.getCurrentUser();
return user && user.isVerticalScrollbars();
},
boardView() {
return Utils.boardView();
},
debugBoardState() {
// Enable debug mode by setting ?debug=1 in URL
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('debug') === '1';
},
debugBoardStateData() {
const currentBoard = Utils.getCurrentBoard();
const currentBoardId = Session.get('currentBoard');
const isBoardReady = this.isBoardReady.get();
const isConverting = this.isConverting.get();
const isMigrating = this.isMigrating.get();
const boardView = Utils.boardView();
if (process.env.DEBUG === 'true') {
console.log('=== BOARD DEBUG STATE ===');
console.log('currentBoardId:', currentBoardId);
console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none');
console.log('isBoardReady:', isBoardReady);
console.log('isConverting:', isConverting);
console.log('isMigrating:', isMigrating);
console.log('boardView:', boardView);
console.log('========================');
}
return {
currentBoardId,
hasCurrentBoard: !!currentBoard,
currentBoardTitle: currentBoard ? currentBoard.title : 'none',
isBoardReady,
isConverting,
isMigrating,
boardView
};
},
openNewListForm() {
if (this.isViewSwimlanes()) {
// The form had been removed in 416b17062e57f215206e93a85b02ef9eb1ab4902
@@ -480,6 +882,31 @@ BlazeComponent.extendComponent({
}
},
'click .js-empty-board-add-swimlane': Popup.open('swimlaneAdd'),
// Global drag and drop file upload handlers for better visual feedback
'dragover .board-canvas'(event) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
// Add visual indicator that files can be dropped
$('.board-canvas').addClass('file-drag-over');
}
},
'dragleave .board-canvas'(event) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
// Only remove class if we're leaving the board canvas entirely
if (!event.currentTarget.contains(event.relatedTarget)) {
$('.board-canvas').removeClass('file-drag-over');
}
}
},
'drop .board-canvas'(event) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
$('.board-canvas').removeClass('file-drag-over');
}
},
},
];
},
+120 -83
View File
@@ -14,41 +14,41 @@ template(name="boardHeaderBar")
with currentBoard
if currentUser.isBoardAdmin
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
i.fa.fa-pencil-square-o
| ✏️
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}" aria-label="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}")
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
if showStarCounter
span
= currentBoard.stars
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}" aria-label="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}")
| {{#if isStarred}}⭐{{else}}☆{{/if}}
if showStarCounter
span
= currentBoard.stars
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
span {{_ currentBoard.permission}}
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
| {{#if currentBoard.isPublic}}🌐{{else}}🔒{{/if}}
span {{_ currentBoard.permission}}
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
i.fa.fa-eye
if $eq watchLevel "tracking"
i.fa.fa-bell
if $eq watchLevel "muted"
i.fa.fa-bell-slash
span {{_ watchLevel}}
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
| 👁️
if $eq watchLevel "tracking"
| 🔔
if $eq watchLevel "muted"
| 🔕
span {{_ watchLevel}}
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
| {{sortCardsIcon}}
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin
| ❌
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
| 🚪
span {{_ 'log-in'}}
.board-header-btns.center
@@ -59,40 +59,40 @@ template(name="boardHeaderBar")
if currentUser
with currentBoard
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
i.fa.fa-pencil-square-o
| ✏️
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
| {{#if isStarred}}⭐{{else}}☆{{/if}}
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
| {{#if currentBoard.isPublic}}🌐{{else}}🔒{{/if}}
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
i.fa.fa-eye
if $eq watchLevel "tracking"
i.fa.fa-bell
if $eq watchLevel "muted"
i.fa.fa-bell-slash
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
| 👁️
if $eq watchLevel "tracking"
| 🔔
if $eq watchLevel "muted"
| 🔕
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
| {{sortCardsIcon}}
if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin
| ❌
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
| 🚪
if isSandstorm
if currentUser
a.board-header-btn.js-open-archived-board
i.fa.fa-archive
| 📦
//if showSort
// a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
@@ -102,56 +102,56 @@ template(name="boardHeaderBar")
a.board-header-btn.js-open-filter-view(
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
class="{{#if Filter.isActive}}emphasis{{/if}}")
i.fa.fa-filter
| 🔽
if Filter.isActive
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
| ❌
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
i.fa.fa-search
| 🔍
unless currentBoard.isTemplatesBoard
a.board-header-btn.js-toggle-board-view(
title="{{_ 'board-view'}}")
i.fa.fa-caret-down
| ▼
if $eq boardView 'board-view-swimlanes'
i.fa.fa-th-large
| 🏊
if $eq boardView 'board-view-lists'
i.fa.fa-trello
| 📋
if $eq boardView 'board-view-cal'
i.fa.fa-calendar
| 📅
if canModifyBoard
a.board-header-btn.js-multiselection-activate(
title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
i.fa.fa-check-square-o
if MultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
| ☑️
if MultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
| ❌
.separator
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
i.fa.fa-navicon
| ☰
template(name="boardVisibilityList")
ul.pop-over-list
li
with "private"
a.js-select-visibility
i.fa.fa-lock.colorful
| 🔒
| {{_ 'private'}}
if visibilityCheck
i.fa.fa-check
| ✅
span.sub-name {{_ 'private-desc'}}
if notAllowPrivateVisibilityOnly
li
with "public"
a.js-select-visibility
i.fa.fa-globe.colorful
| 🌐
| {{_ 'public'}}
if visibilityCheck
i.fa.fa-check
| ✅
span.sub-name {{_ 'public-desc'}}
template(name="boardChangeVisibilityPopup")
@@ -162,26 +162,26 @@ template(name="boardChangeWatchPopup")
li
with "watching"
a.js-select-watch
i.fa.fa-eye.colorful
| 👁️
| {{_ 'watching'}}
if watchCheck
i.fa.fa-check
| ✅
span.sub-name {{_ 'watching-info'}}
li
with "tracking"
a.js-select-watch
i.fa.fa-bell.colorful
| 🔔
| {{_ 'tracking'}}
if watchCheck
i.fa.fa-check
| ✅
span.sub-name {{_ 'tracking-info'}}
li
with "muted"
a.js-select-watch
i.fa.fa-bell-slash.colorful
| 🔕
| {{_ 'muted'}}
if watchCheck
i.fa.fa-check
| ✅
span.sub-name {{_ 'muted-info'}}
template(name="boardChangeViewPopup")
@@ -189,24 +189,24 @@ template(name="boardChangeViewPopup")
li
with "board-view-swimlanes"
a.js-open-swimlanes-view
i.fa.fa-th-large.colorful
| 🏊
| {{_ 'board-view-swimlanes'}}
if $eq Utils.boardView "board-view-swimlanes"
i.fa.fa-check
| ✅
li
with "board-view-lists"
a.js-open-lists-view
i.fa.fa-trello.colorful
| 📋
| {{_ 'board-view-lists'}}
if $eq Utils.boardView "board-view-lists"
i.fa.fa-check
| ✅
li
with "board-view-cal"
a.js-open-cal-view
i.fa.fa-calendar.colorful
| 📅
| {{_ 'board-view-cal'}}
if $eq Utils.boardView "board-view-cal"
i.fa.fa-check
| ✅
template(name="createBoard")
form
@@ -218,11 +218,40 @@ template(name="createBoard")
else
p.quiet
if $eq visibility.get 'public'
span.fa.fa-globe.colorful
span 🌐
= " "
| {{{_ 'board-public-info'}}}
else
span.fa.fa-lock.colorful
span 🔒
= " "
| {{{_ 'board-private-info'}}}
a.js-change-visibility {{_ 'change'}}.
a.flex.js-toggle-add-template-container
.materialCheckBox#add-template-container
span {{_ 'add-template-container'}}
input.primary.wide(type="submit" value="{{_ 'create'}}")
span.quiet
| {{_ 'or'}}
a.js-import-board {{_ 'import'}}
span.quiet
| /
a.js-board-template {{_ 'template'}}
template(name="createBoardPopup")
form
label
| {{_ 'title'}}
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
if visibilityMenuIsOpen.get
+boardVisibilityList
else
p.quiet
if $eq visibility.get 'public'
span 🌐
= " "
| {{{_ 'board-public-info'}}}
else
span 🔒
= " "
| {{{_ 'board-private-info'}}}
a.js-change-visibility {{_ 'change'}}.
@@ -246,10 +275,10 @@ template(name="createBoard")
// li
// a.js-sort-by(name="{{value.name}}")
// if $eq sortby value.name
// i(class="fa {{Direction}}")
// | {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
// | {{_ value.label }}{{_ value.shortLabel}}
// if $eq sortby value.name
// i(class="fa fa-check")
// | ✅
template(name="boardChangeTitlePopup")
form
@@ -269,14 +298,22 @@ template(name="boardCreateRulePopup")
template(name="cardsSortPopup")
ul.pop-over-list
li
a.js-sort-due {{_ 'due-date'}}
a.js-sort-due
| 📅
| {{_ 'due-date'}}
hr
li
a.js-sort-title {{_ 'title-alphabetically'}}
a.js-sort-title
| 🔤
| {{_ 'title-alphabetically'}}
hr
li
a.js-sort-created-desc {{_ 'created-at-newest-first'}}
a.js-sort-created-desc
| ⬇️
| {{_ 'created-at-newest-first'}}
hr
li
a.js-sort-created-asc {{_ 'created-at-oldest-first'}}
a.js-sort-created-asc
| ⬆️
| {{_ 'created-at-oldest-first'}}
+51 -4
View File
@@ -82,10 +82,29 @@ BlazeComponent.extendComponent({
},
'click .js-toggle-board-view': Popup.open('boardChangeView'),
'click .js-toggle-sidebar'() {
Sidebar.toggle();
console.log('Hamburger menu clicked');
// Use the same approach as keyboard shortcuts
if (typeof Sidebar !== 'undefined' && Sidebar && typeof Sidebar.toggle === 'function') {
console.log('Using Sidebar.toggle()');
Sidebar.toggle();
} else {
console.warn('Sidebar not available, trying alternative approach');
// Try to trigger the sidebar through the global Blaze helper
if (typeof Blaze !== 'undefined' && Blaze._globalHelpers && Blaze._globalHelpers.Sidebar) {
const sidebar = Blaze._globalHelpers.Sidebar();
if (sidebar && typeof sidebar.toggle === 'function') {
console.log('Using Blaze helper Sidebar.toggle()');
sidebar.toggle();
}
}
}
},
'click .js-open-filter-view'() {
Sidebar.setView('filter');
if (Sidebar) {
Sidebar.setView('filter');
} else {
console.warn('Sidebar not available for setView');
}
},
'click .js-sort-cards': Popup.open('cardsSort'),
/*
@@ -102,14 +121,22 @@ BlazeComponent.extendComponent({
*/
'click .js-filter-reset'(event) {
event.stopPropagation();
Sidebar.setView();
if (Sidebar) {
Sidebar.setView();
} else {
console.warn('Sidebar not available for setView');
}
Filter.reset();
},
'click .js-sort-reset'() {
Session.set('sortBy', '');
},
'click .js-open-search-view'() {
Sidebar.setView('search');
if (Sidebar) {
Sidebar.setView('search');
} else {
console.warn('Sidebar not available for setView');
}
},
'click .js-multiselection-activate'() {
const currentCard = Utils.getCurrentCardId();
@@ -128,6 +155,7 @@ BlazeComponent.extendComponent({
},
];
},
}).register('boardHeaderBar');
Template.boardHeaderBar.helpers({
@@ -137,6 +165,23 @@ Template.boardHeaderBar.helpers({
isSortActive() {
return Session.get('sortBy') ? true : false;
},
sortCardsIcon() {
const sortBy = Session.get('sortBy');
if (!sortBy) {
return '🃏'; // Card icon when nothing is selected
}
// Determine which sort option is active based on sortBy object
if (sortBy.dueAt) {
return '📅'; // Due date icon
} else if (sortBy.title) {
return '🔤'; // Alphabet icon
} else if (sortBy.createdAt) {
return sortBy.createdAt === 1 ? '⬆️' : '⬇️'; // Up/down arrow based on direction
}
return '🃏'; // Default card icon
},
});
Template.boardChangeViewPopup.events({
@@ -203,6 +248,7 @@ const CreateBoard = BlazeComponent.extendComponent({
title: title,
permission: 'private',
type: 'template-container',
migrationVersion: 1, // Latest version - no migration needed
}),
);
@@ -246,6 +292,7 @@ const CreateBoard = BlazeComponent.extendComponent({
Boards.insert({
title,
permission: visibility,
migrationVersion: 1, // Latest version - no migration needed
}),
);
+44 -17
View File
@@ -103,8 +103,8 @@
transform: rotate(4deg);
display: block !important;
}
.board-list li.starred .fa-star,
.board-list li.starred .fa-star-o {
.board-list li.starred .is-star-active,
.board-list li.starred .is-not-star-active {
opacity: 1;
}
.board-list .board-list-item {
@@ -154,8 +154,8 @@
.board-list .js-add-board :hover {
background-color: #939393;
}
.board-list .fa-star,
.board-list .fa-star-o {
.board-list .is-star-active,
.board-list .is-not-star-active {
bottom: 0;
font-size: 14px;
height: 18px;
@@ -212,30 +212,30 @@
transition-duration: 0.15s;
transition-property: color, font-size, background;
}
.board-list li:hover a:hover .fa-star,
.board-list li:hover a:hover .is-star-active,
.board-list li:hover a:hover .fa-clone,
.board-list li:hover a:hover .fa-archive,
.board-list li:hover a:hover .fa-star-o {
.board-list li:hover a:hover .is-not-star-active {
color: #fff;
}
.board-list li:hover a .fa-star,
.board-list li:hover a .is-star-active,
.board-list li:hover a .fa-clone,
.board-list li:hover a .fa-archive,
.board-list li:hover a .fa-star-o {
.board-list li:hover a .is-not-star-active {
color: #fff;
opacity: 0.75;
}
.board-list li:hover a .fa-star:hover,
.board-list li:hover a .is-star-active:hover,
.board-list li:hover a .fa-clone:hover,
.board-list li:hover a .fa-archive:hover,
.board-list li:hover a .fa-star-o:hover {
.board-list li:hover a .is-not-star-active:hover {
font-size: 18px;
opacity: 1;
}
.board-list li:hover a .fa-star.is-star-active,
.board-list li:hover a .fa-clone.is-star-active,
.board-list li:hover a .fa-archive.is-star-active,
.board-list li:hover a .fa-star-o.is-star-active {
.board-list li:hover a .is-star-active,
.board-list li:hover a .fa-clone,
.board-list li:hover a .fa-archive,
.board-list li:hover a .is-not-star-active {
opacity: 1;
}
.board-backgrounds-list .board-background-select {
@@ -361,6 +361,18 @@
min-height: 100vh; /* Force content to be tall enough to scroll */
}
/* Hide archive and clone board buttons in mobile view */
.board-list.mobile-view .js-archive-board,
.board-list.mobile-view .js-clone-board {
display: none !important;
}
/* Change board drag handle to up-down arrow in mobile view */
.board-list.mobile-view .board-handle.fa-arrows::before {
content: "↕️" !important;
font-family: inherit !important;
}
.board-list.mobile-view::after {
content: '';
display: block;
@@ -371,7 +383,8 @@
screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) {
screen and (max-width: 800px) and (orientation: landscape),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
.board-list {
height: 100%;
overflow-y: auto;
@@ -457,7 +470,8 @@
screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) {
screen and (max-width: 800px) and (orientation: landscape),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
.wrapper {
font-size: 2em !important; /* 2x bigger base font size for All Boards page */
}
@@ -757,7 +771,8 @@
screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) {
screen and (max-width: 800px) and (orientation: landscape),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
.wrapper {
overflow: hidden;
height: 100vh;
@@ -824,5 +839,17 @@
#content {
overflow: hidden;
}
/* Hide archive and clone board buttons in mobile view */
.board-list .js-archive-board,
.board-list .js-clone-board {
display: none !important;
}
/* Change board drag handle to up-down arrow in mobile view */
.board-list .board-handle.fa-arrows::before {
content: "↕️" !important;
font-family: inherit !important;
}
}
+33 -30
View File
@@ -64,11 +64,9 @@ template(name="boardList")
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
i.fa.board-handle(
class="fa-arrows"
title="{{_ 'drag-board'}}")
if isSandstorm
i.fa.js-clone-board(
class="fa-clone"
@@ -108,9 +106,10 @@ template(name="boardList")
each list in boardLists _id
.item
| {{ list }}
i.fa.js-star-board(
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/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}}
p.board-list-item-desc
+viewer
= description
@@ -118,32 +117,36 @@ template(name="boardList")
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
i.fa.board-handle(
class="fa-arrows"
title="{{_ 'drag-board'}}")
if isSandstorm
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
a.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
| 📋
a.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'}}")
a.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
| 📋
a.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
| 📦
else if currentUser.isAdmin
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
a.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
| 📋
a.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
| 📦
template(name="boardListHeaderBar")
h1 {{_ title }}
@@ -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;
}
}
@@ -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>
@@ -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;
+33
View File
@@ -336,3 +336,36 @@
margin-top: 10px;
}
}
/* Attachment migration styles */
.attachment-item.migrating {
position: relative;
opacity: 0.7;
}
.attachment-migration-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
border-radius: 4px;
}
.migration-spinner {
font-size: 24px;
color: #007cba;
margin-bottom: 8px;
}
.migration-text {
font-size: 12px;
color: #666;
text-align: center;
}
+25 -18
View File
@@ -34,10 +34,10 @@ template(name="attachmentViewer")
#viewer-overlay.hidden
#viewer-top-bar
span#attachment-name
a#viewer-close.fa.fa-times-thin
a#viewer-close ❌
#viewer-container
i.fa.fa-chevron-left.attachment-arrow#prev-attachment
| ◀️
#viewer-content
img#image-viewer.hidden
video#video-viewer.hidden(controls="true")
@@ -45,7 +45,7 @@ template(name="attachmentViewer")
object#pdf-viewer.hidden(type="application/pdf")
span.pdf-preview-error {{_ 'preview-pdf-not-supported' }}
object#txt-viewer.hidden(type="text/plain")
i.fa.fa-chevron-right.attachment-arrow#next-attachment
| ▶️
template(name="attachmentGallery")
@@ -53,11 +53,11 @@ template(name="attachmentGallery")
if canModifyCard
a.attachment-item.add-attachment.js-add-attachment
i.fa.fa-plus.icon
| ➕
each attachments
.attachment-item
.attachment-item(class="{{#if isAttachmentMigrating _id}}migrating{{/if}}")
.attachment-thumbnail-container.open-preview(data-attachment-id="{{_id}}" data-card-id="{{ meta.cardId }}")
if link
if(isImage)
@@ -86,25 +86,32 @@ template(name="attachmentGallery")
= name
span.file-size ({{fileSize size}})
.attachment-actions
a.js-download(href="{{link}}?download=true", download="{{name}}")
i.fa.fa-download.icon(title="{{_ 'download'}}")
a.js-download(href="{{link}}?download=true", download="{{name}}", title="{{_ 'download'}}")
| ⬇️
if currentUser.isBoardMember
unless currentUser.isCommentOnly
unless currentUser.isWorker
a.js-rename
i.fa.fa-pencil-square-o.icon(title="{{_ 'rename'}}")
a.js-confirm-delete
i.fa.fa-trash.icon(title="{{_ 'delete'}}")
a.fa.fa-navicon.icon.js-open-attachment-menu(data-attachment-link="{{link}}" title="{{_ 'attachmentActionsPopup-title'}}")
a.js-rename(title="{{_ 'rename'}}")
| ✏️
a.js-confirm-delete(title="{{_ 'delete'}}")
| 🗑️
a.js-open-attachment-menu(data-attachment-link="{{link}}", title="{{_ 'attachmentActionsPopup-title'}}")
| ☰
// Migration spinner overlay
if isAttachmentMigrating _id
.attachment-migration-overlay
.migration-spinner
| ⚙️
.migration-text {{_ 'migrating-attachment'}}
template(name="attachmentActionsPopup")
ul.pop-over-list
li
if isImage
a(class="{{#if isCover}}js-remove-cover{{else}}js-add-cover{{/if}}")
i.fa.fa-book
i.fa.fa-picture-o
| 📖
| 🖼️
if isCover
| {{_ 'remove-cover'}}
else
@@ -112,7 +119,7 @@ template(name="attachmentActionsPopup")
if currentUser.isBoardAdmin
if isImage
a(class="{{#if isBackgroundImage}}js-remove-background-image{{else}}js-add-background-image{{/if}}")
i.fa.fa-picture-o
| 🖼️
if isBackgroundImage
| {{_ 'remove-background-image'}}
else
@@ -120,19 +127,19 @@ template(name="attachmentActionsPopup")
if $neq versions.original.storage "fs"
a.js-move-storage-fs
i.fa.fa-arrow-right
| ▶️
| {{_ 'attachment-move-storage-fs'}}
if $neq versions.original.storage "gridfs"
if versions.original.storage
a.js-move-storage-gridfs
i.fa.fa-arrow-right
| ▶️
| {{_ 'attachment-move-storage-gridfs'}}
if $neq versions.original.storage "s3"
if versions.original.storage
a.js-move-storage-s3
i.fa.fa-arrow-right
| ▶️
| {{_ 'attachment-move-storage-s3'}}
template(name="attachmentRenamePopup")
+19 -1
View File
@@ -3,6 +3,7 @@ import { ObjectID } from 'bson';
import DOMPurify from 'dompurify';
import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
import uploadProgressManager from '../../lib/uploadProgressManager';
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
const filesize = require('filesize');
const prettyMilliseconds = require('pretty-ms');
@@ -342,7 +343,7 @@ export function handleFileUpload(card, files) {
}
// Check if user can modify the card
if (!card.canModifyCard()) {
if (!Utils.canModifyCard()) {
if (process.env.DEBUG === 'true') {
console.warn('User does not have permission to modify this card');
}
@@ -576,3 +577,20 @@ BlazeComponent.extendComponent({
]
}
}).register('attachmentRenamePopup');
// Template helpers for attachment migration status
Template.registerHelper('attachmentMigrationStatus', function(attachmentId) {
return attachmentMigrationManager.getAttachmentMigrationStatus(attachmentId);
});
Template.registerHelper('isAttachmentMigrating', function(attachmentId) {
return attachmentMigrationManager.isAttachmentBeingMigrated(attachmentId);
});
Template.registerHelper('attachmentMigrationProgress', function() {
return attachmentMigrationManager.attachmentMigrationProgress.get();
});
Template.registerHelper('attachmentMigrationStatusText', function() {
return attachmentMigrationManager.attachmentMigrationStatus.get();
});
@@ -6,10 +6,10 @@ template(name="cardCustomFieldsPopup")
span.full-name
= name
if hasCustomField
i.fa.fa-check
| ✅
hr
a.quiet-button.full.js-settings
i.fa.fa-cog
| ⚙️
span {{_ 'settings'}}
template(name="cardCustomField")
@@ -22,7 +22,7 @@ template(name="cardCustomField-text")
= value
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
else
a.js-open-inlined-form
if value
@@ -41,7 +41,7 @@ template(name="cardCustomField-number")
input(type="number" value=data.value)
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
else
a.js-open-inlined-form
if value
@@ -66,7 +66,7 @@ template(name="cardCustomField-currency")
input(type="text" value=data.value autofocus)
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
else
a.js-open-inlined-form
if value
@@ -113,7 +113,7 @@ template(name="cardCustomField-dropdown")
= name
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
else
a.js-open-inlined-form
if value
@@ -134,7 +134,7 @@ template(name="cardCustomField-stringtemplate")
input.js-card-customfield-stringtemplate-item.last(type="text" value="" placeholder="{{_ 'custom-field-stringtemplate-item-placeholder'}}" autofocus)
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
else
a.js-open-inlined-form
if value
+39 -16
View File
@@ -1,6 +1,27 @@
import moment from 'moment/min/moment-with-locales';
import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker';
import { ReactiveCache } from '/imports/reactiveCache';
import {
formatDateTime,
formatDate,
formatDateByUserPreference,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar
} from '/imports/lib/dateUtils';
import Cards from '/models/cards';
import { CustomFieldStringTemplate } from '/client/lib/customFields'
@@ -134,31 +155,33 @@ CardCustomField.register('cardCustomField');
super.onCreated();
const self = this;
self.date = ReactiveVar();
self.now = ReactiveVar(moment());
self.now = ReactiveVar(now());
window.setInterval(() => {
self.now.set(moment());
self.now.set(now());
}, 60000);
self.autorun(() => {
self.date.set(moment(self.data().value));
self.date.set(new Date(self.data().value));
});
}
showWeek() {
return this.date.get().week().toString();
return getISOWeek(this.date.get()).toString();
}
showWeekOfYear() {
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
const user = ReactiveCache.getCurrentUser();
if (!user) {
// For non-logged-in users, week of year is not shown
return false;
}
return user.isShowWeekOfYear();
}
showDate() {
// this will start working once mquandalle:moment
// is updated to at least moment.js 2.10.5
// until then, the date is displayed in the "L" format
return this.date.get().calendar(null, {
sameElse: 'llll',
});
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
showISODate() {
@@ -167,8 +190,8 @@ CardCustomField.register('cardCustomField');
classes() {
if (
this.date.get().isBefore(this.now.get(), 'minute') &&
this.now.get().isBefore(this.data().value)
isBefore(this.date.get(), this.now.get(), 'minute') &&
isBefore(this.now.get(), this.data().value, 'minute')
) {
return 'current';
}
@@ -176,7 +199,7 @@ CardCustomField.register('cardCustomField');
}
showTitle() {
return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
return `${TAPi18n.__('card-start-on')} ${this.date.get().toLocaleString()}`;
}
events() {
@@ -195,7 +218,7 @@ CardCustomField.register('cardCustomField');
const self = this;
self.card = Utils.getCurrentCard();
self.customFieldId = this.data()._id;
this.data().value && this.date.set(moment(this.data().value));
this.data().value && this.date.set(new Date(this.data().value));
}
_storeDate(date) {
+50 -6
View File
@@ -42,23 +42,67 @@
.card-date.long-overdue.is-active {
background-color: #fd3e24;
}
/* Date type specific colors */
.card-date.received-date {
background-color: #dbdbdb; /* Light grey for received */
}
.card-date.received-date:hover,
.card-date.received-date.is-active {
background-color: #b3b3b3;
}
.card-date.start-date {
background-color: #90ee90; /* Light green for start */
color: #000; /* Black text for start */
}
.card-date.start-date:hover,
.card-date.start-date.is-active {
background-color: #7dd87d;
}
.card-date.due-date {
background-color: #ffd700; /* Yellow for due */
color: #000; /* Black text for due */
}
.card-date.due-date:hover,
.card-date.due-date.is-active {
background-color: #e6c200;
}
.card-date.end-date {
background-color: #ffb3b3; /* Light red for end */
color: #000; /* Black text for end */
}
.card-date.end-date:hover,
.card-date.end-date.is-active {
background-color: #ff9999;
}
.card-date.end-date time::before {
content: "\f253";
content: "🏁"; /* Finish flag - represents end/completion */
}
.card-date.due-date time::before {
content: "\f090";
content: "⏰"; /* Alarm clock - represents due/deadline */
}
.card-date.start-date time::before {
content: "\f251";
content: "🚀"; /* Rocket - represents start/launch */
}
.card-date.received-date time::before {
content: "\f08b";
content: "📥"; /* Inbox tray - represents received/incoming */
}
/* Generic date badge and custom field date */
.card-date:not(.received-date):not(.start-date):not(.due-date):not(.end-date) time::before {
/*content: "📅"; // Calendar - represents generic date */
}
.card-date time::before {
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
-webkit-font-smoothing: antialiased;
margin-right: 0.3em;
display: inline-block;
}
.customfield-date {
display: block;
+108
View File
@@ -21,3 +21,111 @@ template(name="dateCustomField")
if showWeekOfYear
b
| {{showWeek}}
template(name="minicardReceivedDate")
if canModifyCard
a.js-edit-date.card-date.received-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date.received-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="minicardStartDate")
if canModifyCard
a.js-edit-date.card-date.start-date(title="{{_ 'card-start'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date.start-date(title="{{_ 'card-start'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="minicardDueDate")
if canModifyCard
a.js-edit-date.card-date.due-date(title="{{_ 'card-due'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date.due-date(title="{{_ 'card-due'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="minicardEndDate")
if canModifyCard
a.js-edit-date.card-date.end-date(title="{{_ 'card-end'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date.end-date(title="{{_ 'card-end'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="minicardCustomFieldDate")
a(title="{{_ 'date'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="editCardReceivedDatePopup")
form.edit-card-received-date
.datepicker
.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
.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
.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
.clear-date
a.js-clear-date {{_ 'clear'}}
.datepicker-actions
button.primary.wide.left(type="submit") {{_ 'save'}}
button.js-delete-date.negate.wide.right {{_ 'delete'}}
+133 -70
View File
@@ -1,17 +1,38 @@
import moment from 'moment/min/moment-with-locales';
import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker';
import {
formatDateTime,
formatDate,
formatDateByUserPreference,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar,
diff
} from '/imports/lib/dateUtils';
// editCardReceivedDatePopup
(class extends DatePicker {
onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
super.onCreated(formatDateTime(now()));
this.data().getReceived() &&
this.date.set(moment(this.data().getReceived()));
this.date.set(new Date(this.data().getReceived()));
}
_storeDate(date) {
this.card.setReceived(moment(date).format('YYYY-MM-DD HH:mm'));
this.card.setReceived(formatDateTime(date));
}
_deleteDate() {
@@ -22,22 +43,17 @@ import { DatePicker } from '/client/lib/datepicker';
// editCardStartDatePopup
(class extends DatePicker {
onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
this.data().getStart() && this.date.set(moment(this.data().getStart()));
super.onCreated(formatDateTime(now()));
this.data().getStart() && this.date.set(new Date(this.data().getStart()));
}
onRendered() {
super.onRendered();
if (moment.isDate(this.card.getReceived())) {
this.$('.js-datepicker').datepicker(
'setStartDate',
this.card.getReceived(),
);
}
// DatePicker base class handles initialization with native HTML inputs
}
_storeDate(date) {
this.card.setStart(moment(date).format('YYYY-MM-DD HH:mm'));
this.card.setStart(formatDateTime(date));
}
_deleteDate() {
@@ -49,18 +65,16 @@ import { DatePicker } from '/client/lib/datepicker';
(class extends DatePicker {
onCreated() {
super.onCreated('1970-01-01 17:00:00');
this.data().getDue() && this.date.set(moment(this.data().getDue()));
this.data().getDue() && this.date.set(new Date(this.data().getDue()));
}
onRendered() {
super.onRendered();
if (moment.isDate(this.card.getStart())) {
this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
}
// DatePicker base class handles initialization with native HTML inputs
}
_storeDate(date) {
this.card.setDue(moment(date).format('YYYY-MM-DD HH:mm'));
this.card.setDue(formatDateTime(date));
}
_deleteDate() {
@@ -71,19 +85,17 @@ import { DatePicker } from '/client/lib/datepicker';
// editCardEndDatePopup
(class extends DatePicker {
onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
this.data().getEnd() && this.date.set(moment(this.data().getEnd()));
super.onCreated(formatDateTime(now()));
this.data().getEnd() && this.date.set(new Date(this.data().getEnd()));
}
onRendered() {
super.onRendered();
if (moment.isDate(this.card.getStart())) {
this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
}
// DatePicker base class handles initialization with native HTML inputs
}
_storeDate(date) {
this.card.setEnd(moment(date).format('YYYY-MM-DD HH:mm'));
this.card.setEnd(formatDateTime(date));
}
_deleteDate() {
@@ -100,27 +112,29 @@ const CardDate = BlazeComponent.extendComponent({
onCreated() {
const self = this;
self.date = ReactiveVar();
self.now = ReactiveVar(moment());
self.now = ReactiveVar(now());
window.setInterval(() => {
self.now.set(moment());
self.now.set(now());
}, 60000);
},
showWeek() {
return this.date.get().week().toString();
return getISOWeek(this.date.get()).toString();
},
showWeekOfYear() {
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
const user = ReactiveCache.getCurrentUser();
if (!user) {
// For non-logged-in users, week of year is not shown
return false;
}
return user.isShowWeekOfYear();
},
showDate() {
// this will start working once mquandalle:moment
// is updated to at least moment.js 2.10.5
// until then, the date is displayed in the "L" format
return this.date.get().calendar(null, {
sameElse: 'llll',
});
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
},
showISODate() {
@@ -133,7 +147,7 @@ class CardReceivedDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getReceived()));
self.date.set(new Date(self.data().getReceived()));
});
}
@@ -145,9 +159,9 @@ class CardReceivedDate extends CardDate {
const theDate = this.date.get();
// if dueAt, endAt and startAt exist & are > receivedAt, receivedAt doesn't need to be flagged
if (
(startAt && theDate.isAfter(startAt)) ||
(endAt && theDate.isAfter(endAt)) ||
(dueAt && theDate.isAfter(dueAt))
(startAt && isAfter(theDate, startAt)) ||
(endAt && isAfter(theDate, endAt)) ||
(dueAt && isAfter(theDate, dueAt))
)
classes += 'long-overdue';
else classes += 'current';
@@ -155,9 +169,10 @@ class CardReceivedDate extends CardDate {
}
showTitle() {
return `${TAPi18n.__('card-received-on')} ${this.date
.get()
.format('LLLL')}`;
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
}
events() {
@@ -173,7 +188,7 @@ class CardStartDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getStart()));
self.date.set(new Date(self.data().getStart()));
});
}
@@ -184,15 +199,18 @@ class CardStartDate extends CardDate {
const theDate = this.date.get();
const now = this.now.get();
// if dueAt or endAt exist & are > startAt, startAt doesn't need to be flagged
if ((endAt && theDate.isAfter(endAt)) || (dueAt && theDate.isAfter(dueAt)))
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt)))
classes += 'long-overdue';
else if (theDate.isAfter(now)) classes += '';
else if (isAfter(theDate, now)) classes += '';
else classes += 'current';
return classes;
}
showTitle() {
return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
}
events() {
@@ -208,7 +226,7 @@ class CardDueDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getDue()));
self.date.set(new Date(self.data().getDue()));
});
}
@@ -218,17 +236,20 @@ class CardDueDate extends CardDate {
const theDate = this.date.get();
const now = this.now.get();
// if the due date is after the end date, green - done early
if (endAt && theDate.isAfter(endAt)) classes += 'current';
if (endAt && isAfter(theDate, endAt)) classes += 'current';
// if there is an end date, don't need to flag the due date
else if (endAt) classes += '';
else if (now.diff(theDate, 'days') >= 2) classes += 'long-overdue';
else if (now.diff(theDate, 'minute') >= 0) classes += 'due';
else if (now.diff(theDate, 'days') >= -1) classes += 'almost-due';
else if (diff(now, theDate, 'days') >= 2) classes += 'long-overdue';
else if (diff(now, theDate, 'minute') >= 0) classes += 'due';
else if (diff(now, theDate, 'days') >= -1) classes += 'almost-due';
return classes;
}
showTitle() {
return `${TAPi18n.__('card-due-on')} ${this.date.get().format('LLLL')}`;
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${TAPi18n.__('card-due-on')} ${formattedDate}`;
}
events() {
@@ -244,7 +265,7 @@ class CardEndDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getEnd()));
self.date.set(new Date(self.data().getEnd()));
});
}
@@ -253,13 +274,13 @@ class CardEndDate extends CardDate {
const dueAt = this.data().getDue();
const theDate = this.date.get();
if (!dueAt) classes += '';
else if (theDate.isBefore(dueAt)) classes += 'current';
else if (theDate.isAfter(dueAt)) classes += 'due';
else if (isBefore(theDate, dueAt)) classes += 'current';
else if (isAfter(theDate, dueAt)) classes += 'due';
return classes;
}
showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`;
}
events() {
@@ -279,16 +300,21 @@ class CardCustomFieldDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().value));
self.date.set(new Date(self.data().value));
});
}
showWeek() {
return this.date.get().week().toString();
return getISOWeek(this.date.get()).toString();
}
showWeekOfYear() {
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
const user = ReactiveCache.getCurrentUser();
if (!user) {
// For non-logged-in users, week of year is not shown
return false;
}
return user.isShowWeekOfYear();
}
showDate() {
@@ -301,7 +327,10 @@ class CardCustomFieldDate extends CardDate {
}
showTitle() {
return `${this.date.get().format('LLLL')}`;
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${formattedDate}`;
}
classes() {
@@ -315,32 +344,62 @@ class CardCustomFieldDate extends CardDate {
CardCustomFieldDate.register('cardCustomFieldDate');
(class extends CardReceivedDate {
template() {
return 'minicardReceivedDate';
}
showDate() {
return this.date.get().format('L');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardReceivedDate'));
(class extends CardStartDate {
template() {
return 'minicardStartDate';
}
showDate() {
return this.date.get().format('YYYY-MM-DD HH:mm');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardStartDate'));
(class extends CardDueDate {
template() {
return 'minicardDueDate';
}
showDate() {
return this.date.get().format('YYYY-MM-DD HH:mm');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardDueDate'));
(class extends CardEndDate {
template() {
return 'minicardEndDate';
}
showDate() {
return this.date.get().format('YYYY-MM-DD HH:mm');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardEndDate'));
(class extends CardCustomFieldDate {
template() {
return 'minicardCustomFieldDate';
}
showDate() {
return this.date.get().format('L');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardCustomFieldDate'));
@@ -349,7 +408,7 @@ class VoteEndDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getVoteEnd()));
self.date.set(new Date(self.data().getVoteEnd()));
});
}
classes() {
@@ -357,10 +416,12 @@ class VoteEndDate extends CardDate {
return classes;
}
showDate() {
return this.date.get().format('L LT');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
return `${TAPi18n.__('card-end-on')} ${this.date.get().toLocaleString()}`;
}
events() {
@@ -376,7 +437,7 @@ class PokerEndDate extends CardDate {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getPokerEnd()));
self.date.set(new Date(self.data().getPokerEnd()));
});
}
classes() {
@@ -384,10 +445,12 @@ class PokerEndDate extends CardDate {
return classes;
}
showDate() {
return this.date.get().format('l LT');
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`;
}
events() {
+36 -8
View File
@@ -1,11 +1,39 @@
/* Date Format Selector */
.card-details-item-date-format {
margin-bottom: 10px;
}
.card-details-item-date-format .card-details-item-title {
font-size: 14px;
font-weight: bold;
margin-bottom: 5px;
color: #333;
}
.card-details-item-date-format .js-date-format-selector {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
font-size: 14px;
cursor: pointer;
}
.card-details-item-date-format .js-date-format-selector:focus {
outline: none;
border-color: #007cba;
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2);
}
.assignee {
border-radius: 0.4vw;
border-radius: 3px;
display: block;
position: relative;
float: left;
height: 4vw;
width: 4vw;
margin: 0.4vh;
height: 30px;
width: 30px;
margin: .3vh;
cursor: pointer;
user-select: none;
z-index: 1;
@@ -34,11 +62,11 @@
background-color: #b3b3b3;
border: 1px solid #fff;
border-radius: 50%;
height: 1vw;
width: 1vw;
height: 7px;
width: 7px;
position: absolute;
right: -0.1vw;
bottom: -0.1vw;
right: -1px;
bottom: -1px;
border: 1px solid #fff;
z-index: 15;
}
+112 -87
View File
@@ -12,15 +12,19 @@ template(name="cardDetails")
else
unless isMiniScreen
unless isPopup
a.fa.fa-times-thin.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
| ❌
if canModifyCard
if cardMaximized
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
| 🔽
else
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
a.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
| 🔼
if canModifyCard
a.fa.fa-navicon.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
a.fa.fa-link.card-copy-button.js-copy-link(
a.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
| ☰
a.card-copy-button.js-copy-link(
id="cardURL_copy"
class="fa-link"
title="{{_ 'copy-card-link-to-clipboard'}}"
@@ -29,10 +33,12 @@ template(name="cardDetails")
span.copied-tooltip {{_ 'copied'}}
else
unless isPopup
a.fa.fa-times-thin.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
| ❌
if canModifyCard
a.fa.fa-navicon.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
a.fa.fa-link.card-copy-mobile-button.js-copy-link(
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
| ☰
a.card-copy-mobile-button.js-copy-link(
id="cardURL_copy"
class="fa-link"
title="{{_ 'copy-card-link-to-clipboard'}}"
@@ -47,7 +53,8 @@ template(name="cardDetails")
| ##{getCardNumber}
= getTitle
if isWatching
i.card-details-watch.fa.fa-eye
i.card-details-watch
| 👁️
.card-details-path
each parentList
| &nbsp; &gt; &nbsp;
@@ -69,7 +76,7 @@ template(name="cardDetails")
if hasActiveUploads
.card-details-upload-progress
.upload-progress-header
i.fa.fa-upload
| 📤
span {{_ 'uploading-files'}} ({{uploadCount}})
each uploads
.upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
@@ -78,11 +85,11 @@ template(name="cardDetails")
.upload-progress-fill(style="width: {{progress}}%")
if $eq status 'error'
.upload-progress-error
i.fa.fa-exclamation-triangle
| ⚠️
span {{_ 'upload-failed'}}
else if $eq status 'completed'
.upload-progress-success
i.fa.fa-check
| ✅
span {{_ 'upload-completed'}}
.card-details-left
@@ -91,7 +98,7 @@ template(name="cardDetails")
if currentBoard.allowsLabels
.card-details-item.card-details-item-labels
h3.card-details-item-title
i.fa.fa-tags
| 🏷️
| {{_ 'labels'}}
a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
each labels
@@ -101,15 +108,25 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
i.fa.fa-plus
| ➕
if currentBoard.hasAnyAllowsDate
hr
.card-details-item.card-details-item-date-format
h3.card-details-item-title
| 📅
| {{_ 'date-format'}}
.card-details-item-content
select.js-date-format-selector
option(value="YYYY-MM-DD" selected="{{#if isDateFormat 'YYYY-MM-DD'}}selected{{/if}}") {{_ 'date-format-yyyy-mm-dd'}}
option(value="DD-MM-YYYY" selected="{{#if isDateFormat 'DD-MM-YYYY'}}selected{{/if}}") {{_ 'date-format-dd-mm-yyyy'}}
option(value="MM-DD-YYYY" selected="{{#if isDateFormat 'MM-DD-YYYY'}}selected{{/if}}") {{_ 'date-format-mm-dd-yyyy'}}
if currentBoard.allowsReceivedDate
.card-details-item.card-details-item-received
h3.card-details-item-title
i.fa.fa-sign-out
| 📥
| {{_ 'card-received'}}
if getReceived
+cardReceivedDate
@@ -117,12 +134,12 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-received-date
i.fa.fa-plus
| ➕
if currentBoard.allowsStartDate
.card-details-item.card-details-item-start
h3.card-details-item-title
i.fa.fa-hourglass-start
| 🚀
| {{_ 'card-start'}}
if getStart
+cardStartDate
@@ -130,12 +147,12 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-start-date
i.fa.fa-plus
| ➕
if currentBoard.allowsDueDate
.card-details-item.card-details-item-due
h3.card-details-item-title
i.fa.fa-sign-in
| ⏰
| {{_ 'card-due'}}
if getDue
+cardDueDate
@@ -143,12 +160,12 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-due-date
i.fa.fa-plus
| ➕
if currentBoard.allowsEndDate
.card-details-item.card-details-item-end
h3.card-details-item-title
i.fa.fa-hourglass-end
| 🏁
| {{_ 'card-end'}}
if getEnd
+cardEndDate
@@ -156,7 +173,7 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-end-date
i.fa.fa-plus
| ➕
if currentBoard.hasAnyAllowsUser
hr
@@ -164,7 +181,7 @@ template(name="cardDetails")
if currentBoard.allowsCreator
.card-details-item.card-details-item-creator
h3.card-details-item-title
i.fa.fa-user
| 👤
| {{_ 'creator'}}
+userAvatar(userId=userId noRemove=true)
@@ -174,7 +191,7 @@ template(name="cardDetails")
if currentBoard.allowsMembers
.card-details-item.card-details-item-members
h3.card-details-item-title
i.fa.fa-users
| 👤s
| {{_ 'members'}}
each userId in getMembers
+userAvatar(userId=userId cardId=_id)
@@ -182,30 +199,30 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
i.fa.fa-plus
| ➕
//if assigneeSelected
if currentBoard.allowsAssignee
.card-details-item.card-details-item-assignees
h3.card-details-item-title
i.fa.fa-user
| 👤
| {{_ 'assignee'}}
each userId in getAssignees
+userAvatar(userId=userId cardId=_id assignee=true)
| {{! XXX Hack to hide syntaxic coloration /// }}
if canModifyCard
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
i.fa.fa-plus
| ➕
if currentUser.isWorker
unless assigneeSelected
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
i.fa.fa-plus
| ➕
//.card-details-items
if currentBoard.allowsRequestedBy
.card-details-item.card-details-item-name
h3.card-details-item-title
i.fa.fa-shopping-cart
| 🛒
| {{_ 'requested-by'}}
if canModifyCard
unless currentUser.isWorker
@@ -225,7 +242,7 @@ template(name="cardDetails")
if currentBoard.allowsAssignedBy
.card-details-item.card-details-item-name
h3.card-details-item-title
i.fa.fa-user-plus
| 👤-plus
| {{_ 'assigned-by'}}
if canModifyCard
unless currentUser.isWorker
@@ -248,7 +265,7 @@ template(name="cardDetails")
if currentBoard.allowsCardSortingByNumber
.card-details-item.card-details-sort-order
h3.card-details-item-title
i.fa.fa-sort
| 🔢
| {{_ 'sort'}}
if canModifyCard
+inlinedForm(classNames="js-card-details-sort")
@@ -261,7 +278,7 @@ template(name="cardDetails")
if currentBoard.allowsShowLists
.card-details-item.card-details-show-lists
h3.card-details-item-title
i.fa.fa-list
| 📋
| {{_ 'list'}}
select.js-select-card-details-lists(disabled="{{#unless canModifyCard}}disabled{{/unless}}")
each currentBoard.lists
@@ -287,7 +304,7 @@ template(name="cardDetails")
hr
.card-details-item.card-details-item-customfield
h3.card-details-item-title
i.fa.fa-list-alt
| 📋-alt
= definition.name
+cardCustomField
@@ -298,14 +315,14 @@ template(name="cardDetails")
else
input.toggle-switch(type="checkbox" id="toggleCustomFieldsGridButton")
label.toggle-label(for="toggleCustomFieldsGridButton")
a.fa.fa-plus.js-custom-fields.card-details-item.custom-fields(title="{{_ 'custom-fields'}}")
a.js-custom-fields.card-details-item.custom-fields(title="{{_ 'custom-fields'}}")
if getVoteQuestion
hr
.vote-title
div.flex
h3
i.fa.fa-thumbs-up
| 👍
| {{_ 'vote-question'}}
if getVoteEnd
+voteEndDate
@@ -323,11 +340,11 @@ template(name="cardDetails")
if showVotingButtons
button.card-details-green.js-vote.js-vote-positive(class="{{#if voteState}}voted{{/if}}")
if voteState
i.fa.fa-thumbs-up
| 👍
| {{_ 'vote-for-it'}}
button.card-details-red.js-vote.js-vote-negative(class="{{#if $eq voteState false}}voted{{/if}}")
if $eq voteState false
i.fa.fa-thumbs-down
| 👎
| {{_ 'vote-against'}}
if getPokerQuestion
@@ -335,7 +352,7 @@ template(name="cardDetails")
.poker-title
div.flex
h3
i.fa.fa-thumbs-up
| 👍
| {{_ 'poker-question'}}
if getPokerEnd
+pokerEndDate
@@ -350,52 +367,52 @@ template(name="cardDetails")
.poker-card
span.inner.js-poker.js-poker-vote-one(class="{{#if $eq pokerState 'one'}}poker-voted{{/if}}") {{_ 'poker-one'}}
if $eq pokerState "one"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-two(class="{{#if $eq pokerState 'two'}}poker-voted{{/if}}") {{_ 'poker-two'}}
if $eq pokerState "two"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-three(class="{{#if $eq pokerState 'three'}}poker-voted{{/if}}") {{_ 'poker-three'}}
if $eq pokerState "three"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-five(class="{{#if $eq pokerState 'five'}}poker-voted{{/if}}") {{_ 'poker-five'}}
if $eq pokerState "five"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-eight(class="{{#if $eq pokerState 'eight'}}poker-voted{{/if}}") {{_ 'poker-eight'}}
if $eq pokerState "eight"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-thirteen(class="{{#if $eq pokerState 'thirteen'}}poker-voted{{/if}}") {{_ 'poker-thirteen'}}
if $eq pokerState "thirteen"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-twenty(class="{{#if $eq pokerState 'twenty'}}poker-voted{{/if}}") {{_ 'poker-twenty'}}
if $eq pokerState "twenty"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-forty(class="{{#if $eq pokerState 'forty'}}poker-voted{{/if}}") {{_ 'poker-forty'}}
if $eq pokerState "forty"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-one-hundred(class="{{#if $eq pokerState 'oneHundred'}}poker-voted{{/if}}") {{_ 'poker-oneHundred'}}
if $eq pokerState "oneHundred"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-unsure(class="{{#if $eq pokerState 'unsure'}}poker-voted{{/if}}") {{_ 'poker-unsure'}}
if $eq pokerState "unsure"
i.fa.fa-check
| ✅
if currentUser.isBoardAdmin
button.card-details-blue.js-poker-finish(class="{{#if $eq voteState false}}poker-voted{{/if}}") {{_ 'poker-finish'}}
@@ -525,7 +542,7 @@ template(name="cardDetails")
button.card-details-red.js-poker-replay(class="{{#if $eq voteState false}}voted{{/if}}") {{_ 'poker-replay'}}
div.estimation-add
button.js-poker-estimation
i.fa.fa-plus
| ➕
| {{_ 'set-estimation'}}
input(type=text,autofocus value=getPokerEstimation,id="pokerEstimation")
@@ -535,18 +552,18 @@ template(name="cardDetails")
if currentBoard.allowsDescriptionTitle
hr
h3.card-details-item-title
i.fa.fa-align-left
| 📝
| {{_ 'description'}}
if currentBoard.allowsDescriptionText
+inlinedCardDescription(classNames="card-description js-card-description")
+descriptionForm
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
else
if currentBoard.allowsDescriptionText
a.js-open-inlined-form(title="{{_ 'edit'}}" value=title)
i.fa.fa-pencil-square-o
| ✏️
a.js-open-inlined-form(title="{{_ 'edit'}}" value=title)
if getDescription
+viewer
@@ -576,7 +593,7 @@ template(name="cardDetails")
if currentBoard.allowsAttachments
hr
h3.card-details-item-title
i.fa.fa-paperclip
| 📎
| {{_ 'attachments'}}
if Meteor.settings.public.attachmentsUploadMaxSize
| {{_ 'max-upload-filesize'}} {{Meteor.settings.public.attachmentsUploadMaxSize}}
@@ -592,7 +609,7 @@ template(name="cardDetails")
unless currentUser.isNoComments
.comment-title
h3.card-details-item-title
i.fa.fa-comment-o
| 💬
| {{_ 'comments'}}
if currentBoard.allowsComments
@@ -607,7 +624,7 @@ template(name="cardDetails")
unless currentUser.isNoComments
.activity-title
h3.card-details-item-title
i.fa.fa-history
| 📜
| {{ _ 'activities'}}
if currentUser.isBoardMember
.material-toggle-switch(title="{{_ 'show-activities'}}")
@@ -627,41 +644,41 @@ template(name="cardDetails")
+activities(card=this mode="card")
template(name="editCardTitleForm")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
a(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
textarea.js-edit-card-title(rows='1' autofocus dir="auto")
= getTitle
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-title-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
template(name="editCardRequesterForm")
input.js-edit-card-requester(type='text' autofocus value=getRequestedBy dir="auto")
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-requester-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
template(name="editCardAssignerForm")
input.js-edit-card-assigner(type='text' autofocus value=getAssignedBy dir="auto")
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-assigner-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
template(name="editCardSortOrderForm")
input.js-edit-card-sort(type='text' autofocus value=sort dir="auto")
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-sort-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
template(name="cardDetailsActionsPopup")
ul.pop-over-list
li
a.js-toggle-watch-card
if isWatching
i.fa.fa-eye
| 👁️
| {{_ 'unwatch'}}
else
i.fa.fa-eye-slash
| 👁️-slash
| {{_ 'watch'}}
hr
if canModifyCard
@@ -672,16 +689,16 @@ template(name="cardDetailsActionsPopup")
//li: a.js-attachments {{_ 'card-edit-attachments'}}
li
a.js-start-voting
i.fa.fa-thumbs-up
| 👍
| {{_ 'card-edit-voting'}}
li
a.js-start-planning-poker
i.fa.fa-thumbs-up
| 👍
| {{_ 'card-edit-planning-poker'}}
if currentUser.isBoardAdmin
li
a.js-custom-fields
i.fa.fa-list-alt
| 📋-alt
| {{_ 'card-edit-custom-fields'}}
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
@@ -689,67 +706,75 @@ template(name="cardDetailsActionsPopup")
//li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
li
a.js-spent-time
i.fa.fa-clock-o
| 🕐
| {{_ 'editCardSpentTimePopup-title'}}
li
a.js-set-card-color
i.fa.fa-paint-brush
| 🎨
| {{_ 'setCardColorPopup-title'}}
li
a.js-toggle-show-list-on-minicard
if showListOnMinicard
| 👁️
| {{_ 'hide-list-on-minicard'}}
else
| 👁️-slash
| {{_ 'show-list-on-minicard'}}
hr
ul.pop-over-list
li
a.js-export-card
i.fa.fa-share-alt
| 📤
| {{_ 'export-card'}}
hr
ul.pop-over-list
li
a.js-move-card-to-top
i.fa.fa-arrow-up
| ⬆️
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
i.fa.fa-arrow-down
| ⬇️
| {{_ 'moveCardToBottom-title'}}
hr
ul.pop-over-list
if currentUser.isBoardAdmin
li
a.js-move-card
i.fa.fa-arrow-right
| ➡️
| {{_ 'moveCardPopup-title'}}
unless currentUser.isWorker
li
a.js-copy-card
i.fa.fa-copy
| 📋
| {{_ 'copyCardPopup-title'}}
unless currentUser.isWorker
ul.pop-over-list
li
a.js-copy-checklist-cards
i.fa.fa-copy
i.fa.fa-copy
| 📋
| 📋
| {{_ 'copyManyCardsPopup-title'}}
unless archived
hr
ul.pop-over-list
li
a.js-archive
i.fa.fa-arrow-right
i.fa.fa-archive
| ➡️
| 📦
| {{_ 'archive-card'}}
hr
ul.pop-over-list
li
a.js-more
i.fa.fa-link
| 🔗
| {{_ 'cardMorePopup-title'}}
template(name="exportCardPopup")
ul.pop-over-list
li
a(href="{{exportUrlCardPDF}}",, download="{{exportFilenameCardPDF}}")
i.fa.fa-share-alt
| 📤
| {{_ 'export-card-pdf'}}
template(name="moveCardPopup")
@@ -804,7 +829,7 @@ template(name="cardMembersPopup")
= user.profile.fullname
| (<span class="username">{{ user.username }}</span>)
if isCardMember
i.fa.fa-check
| ✅
template(name="cardAssigneesPopup")
input.card-assignees-filter(type="text" placeholder="{{_ 'search'}}")
@@ -818,7 +843,7 @@ template(name="cardAssigneesPopup")
= user.profile.fullname
| (<span class="username">{{ user.username }}</span>)
if isCardAssignee
i.fa.fa-check
| ✅
if currentUser.isWorker
ul.pop-over-list.js-card-assignee-list
li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
@@ -828,7 +853,7 @@ template(name="cardAssigneesPopup")
= currentUser.profile.fullname
| (<span class="username">{{ currentUser.username }}</span>)
if currentUser.isCardAssignee
i.fa.fa-check
| ✅
template(name="cardAssigneePopup")
.board-assignee-menu
@@ -852,7 +877,7 @@ template(name="cardMorePopup")
span.clearfix
span {{_ 'link-card'}}
= ' '
i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
| {{#if board.isPublic}}🌐{{else}}🔒{{/if}}
input.inline-input(type="text" id="cardURL" readonly value="{{ originRelativeUrl }}" autofocus="autofocus")
button.js-copy-card-link-to-clipboard(class="btn" id="clipboard") {{_ 'copy-card-link-to-clipboard'}}
.copied-tooltip {{_ 'copied'}}
@@ -894,7 +919,7 @@ template(name="setCardColorPopup")
unless $eq color 'white'
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color)
i.fa.fa-check
| ✅
button.primary.confirm.js-submit {{_ 'save'}}
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
@@ -928,12 +953,12 @@ template(name="cardStartVotingPopup")
.materialCheckBox#vote-public(name="vote-public" class="{{#if votePublic}}is-checked{{/if}}")
span {{_ 'vote-public'}}
.check-div.flex
i.fa.fa-hourglass-end
| ⏰
a.js-end-date
span
| {{_ 'card-end'}}
unless getVoteEnd
i.fa.fa-plus
| ➕
if getVoteEnd
+voteEndDate
@@ -974,12 +999,12 @@ template(name="cardStartPlanningPokerPopup")
.materialCheckBox#poker-allow-non-members(name="poker-allow-non-members" class="{{#if pokerAllowNonBoardMembers}}is-checked{{/if}}")
span {{_ 'allowNonBoardMembers'}}
.check-div.flex
i.fa.fa-hourglass-end
| ⏰
a.js-end-date
span
| {{_ 'card-end'}}
unless getPokerEnd
i.fa.fa-plus
| ➕
if getPokerEnd
+pokerEndDate
+129 -90
View File
@@ -1,7 +1,26 @@
import { ReactiveCache } from '/imports/reactiveCache';
import moment from 'moment/min/moment-with-locales';
import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker';
import {
formatDateTime,
formatDate,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar
} from '/imports/lib/dateUtils';
import Cards from '/models/cards';
import Boards from '/models/boards';
import Checklists from '/models/checklists';
@@ -287,6 +306,10 @@ BlazeComponent.extendComponent({
const $tooltip = this.$('.card-details-header .copied-tooltip');
Utils.showCopied(promise, $tooltip);
},
'change .js-date-format-selector'(event) {
const dateFormat = event.target.value;
Meteor.call('changeDateFormat', dateFormat);
},
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
'submit .js-card-description'(event) {
event.preventDefault();
@@ -455,7 +478,7 @@ BlazeComponent.extendComponent({
'click .js-poker-finish'(e) {
if ($(e.target).hasClass('js-poker-finish')) {
e.preventDefault();
const now = moment().format('YYYY-MM-DD HH:mm');
const now = formatDateTime(new Date());
this.data().setPokerEnd(now);
}
},
@@ -485,55 +508,58 @@ BlazeComponent.extendComponent({
},
// Drag and drop file upload handlers
'dragover .js-card-details'(event) {
event.preventDefault();
event.stopPropagation();
// Only prevent default for file drags to avoid interfering with other drag operations
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
}
},
'dragenter .js-card-details'(event) {
event.preventDefault();
event.stopPropagation();
const card = this.data();
const board = card.board();
// Only allow drag-and-drop if user can modify card and board allows attachments
if (card.canModifyCard() && board && board.allowsAttachments) {
// Check if the drag contains files
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
const card = this.data();
const board = card.board();
// Only allow drag-and-drop if user can modify card and board allows attachments
if (Utils.canModifyCard() && board && board.allowsAttachments) {
$(event.currentTarget).addClass('is-dragging-over');
}
}
},
'dragleave .js-card-details'(event) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
}
},
'drop .js-card-details'(event) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
const card = this.data();
const board = card.board();
// Check permissions
if (!card.canModifyCard() || !board || !board.allowsAttachments) {
return;
}
// Check if this is a file drop (not a checklist item reorder)
const dataTransfer = event.originalEvent.dataTransfer;
if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) {
return;
}
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
// Check if the drop contains files (not just text/HTML)
if (!dataTransfer.types.includes('Files')) {
return;
}
const card = this.data();
const board = card.board();
const files = dataTransfer.files;
if (files && files.length > 0) {
handleFileUpload(card, files);
// Check permissions
if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
return;
}
// Check if this is a file drop (not a checklist item reorder)
if (!dataTransfer.files || dataTransfer.files.length === 0) {
return;
}
const files = dataTransfer.files;
if (files && files.length > 0) {
handleFileUpload(card, files);
}
}
},
},
@@ -546,6 +572,11 @@ Template.cardDetails.helpers({
let ret = !!Utils.getPopupCardId();
return ret;
},
isDateFormat(format) {
const currentUser = ReactiveCache.getCurrentUser();
if (!currentUser) return format === 'YYYY-MM-DD';
return currentUser.getDateFormat() === format;
},
// Upload progress helpers
hasActiveUploads() {
return uploadProgressManager.hasActiveUploads(this._id);
@@ -657,6 +688,10 @@ Template.cardDetailsActionsPopup.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
showListOnMinicard() {
return this.showListOnMinicard;
},
});
Template.cardDetailsActionsPopup.events({
@@ -702,6 +737,12 @@ Template.cardDetailsActionsPopup.events({
if (!err && ret) Popup.close();
});
},
'click .js-toggle-show-list-on-minicard'() {
const currentCard = this;
const newValue = !currentCard.showListOnMinicard;
Cards.update(currentCard._id, { $set: { showListOnMinicard: newValue } });
Popup.close();
},
});
BlazeComponent.extendComponent({
@@ -1096,8 +1137,8 @@ BlazeComponent.extendComponent({
// editVoteEndDatePopup
(class extends DatePicker {
onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
this.data().getVoteEnd() && this.date.set(moment(this.data().getVoteEnd()));
super.onCreated(formatDateTime(now()));
this.data().getVoteEnd() && this.date.set(new Date(this.data().getVoteEnd()));
}
events() {
return [
@@ -1108,12 +1149,12 @@ BlazeComponent.extendComponent({
// if no time was given, init with 12:00
const time =
evt.target.time.value ||
moment(new Date().setHours(12, 0, 0)).format('LT');
formatTime(new Date().setHours(12, 0, 0));
const dateString = `${evt.target.date.value} ${time}`;
/*
const newDate = moment(dateString, 'L LT', true);
const newDate = parseDate(dateString, ['L LT'], true);
if (newDate.isValid()) {
// if active vote - store it
if (this.currentData().getVoteQuestion()) {
@@ -1127,28 +1168,27 @@ BlazeComponent.extendComponent({
*/
// Try to parse different date formats of all languages.
// This code is same for vote and planning poker.
const usaDate = moment(dateString, 'L LT', true);
const euroAmDate = moment(dateString, 'DD.MM.YYYY LT', true);
const euro24hDate = moment(dateString, 'DD.MM.YYYY HH.mm', true);
const eurodotDate = moment(dateString, 'DD.MM.YYYY HH:mm', true);
const minusDate = moment(dateString, 'YYYY-MM-DD HH:mm', true);
const slashDate = moment(dateString, 'DD/MM/YYYY HH.mm', true);
const dotDate = moment(dateString, 'DD/MM/YYYY HH:mm', true);
const brezhonegDate = moment(dateString, 'DD/MM/YYYY h[e]mm A', true);
const hrvatskiDate = moment(dateString, 'DD. MM. YYYY H:mm', true);
const latviaDate = moment(dateString, 'YYYY.MM.DD. H:mm', true);
const nederlandsDate = moment(dateString, 'DD-MM-YYYY HH:mm', true);
// greekDate does not work: el Greek Ελληνικά ,
// it has date format DD/MM/YYYY h:mm MM like 20/06/2021 11:15 MM
// where MM is maybe some text like AM/PM ?
// Also some other languages that have non-ascii characters in dates
// do not work.
const greekDate = moment(dateString, 'DD/MM/YYYY h:mm A', true);
const macedonianDate = moment(dateString, 'D.MM.YYYY H:mm', true);
// Try to parse different date formats using native Date parsing
const formats = [
'YYYY-MM-DD HH:mm',
'MM/DD/YYYY HH:mm',
'DD.MM.YYYY HH:mm',
'DD/MM/YYYY HH:mm',
'DD-MM-YYYY HH:mm'
];
let parsedDate = null;
for (const format of formats) {
parsedDate = parseDate(dateString, [format], true);
if (parsedDate) break;
}
// Fallback to native Date parsing
if (!parsedDate) {
parsedDate = new Date(dateString);
}
if (usaDate.isValid()) {
if (isValidDate(parsedDate)) {
// if active poker - store it
if (this.currentData().getPokerQuestion()) {
this._storeDate(usaDate.toDate());
@@ -1327,9 +1367,9 @@ BlazeComponent.extendComponent({
// editPokerEndDatePopup
(class extends DatePicker {
onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
super.onCreated(formatDateTime(now()));
this.data().getPokerEnd() &&
this.date.set(moment(this.data().getPokerEnd()));
this.date.set(new Date(this.data().getPokerEnd()));
}
/*
@@ -1347,7 +1387,7 @@ BlazeComponent.extendComponent({
return moment.localeData().longDateFormat('LT');
}
const newDate = moment(dateString, dateformat() + ' ' + timeformat(), true);
const newDate = parseDate(dateString, [dateformat() + ' ' + timeformat()], true);
*/
events() {
@@ -1359,7 +1399,7 @@ BlazeComponent.extendComponent({
// if no time was given, init with 12:00
const time =
evt.target.time.value ||
moment(new Date().setHours(12, 0, 0)).format('LT');
formatTime(new Date().setHours(12, 0, 0));
const dateString = `${evt.target.date.value} ${time}`;
@@ -1370,7 +1410,7 @@ BlazeComponent.extendComponent({
Maybe client/components/lib/datepicker.jade could have hidden input field for
datepicker format that could be used to detect date format?
const newDate = moment(dateString, dateformat() + ' ' + timeformat(), true);
const newDate = parseDate(dateString, [dateformat() + ' ' + timeformat()], true);
if (newDate.isValid()) {
// if active poker - store it
@@ -1383,28 +1423,27 @@ BlazeComponent.extendComponent({
}
*/
// Try to parse different date formats of all languages.
// This code is same for vote and planning poker.
const usaDate = moment(dateString, 'L LT', true);
const euroAmDate = moment(dateString, 'DD.MM.YYYY LT', true);
const euro24hDate = moment(dateString, 'DD.MM.YYYY HH.mm', true);
const eurodotDate = moment(dateString, 'DD.MM.YYYY HH:mm', true);
const minusDate = moment(dateString, 'YYYY-MM-DD HH:mm', true);
const slashDate = moment(dateString, 'DD/MM/YYYY HH.mm', true);
const dotDate = moment(dateString, 'DD/MM/YYYY HH:mm', true);
const brezhonegDate = moment(dateString, 'DD/MM/YYYY h[e]mm A', true);
const hrvatskiDate = moment(dateString, 'DD. MM. YYYY H:mm', true);
const latviaDate = moment(dateString, 'YYYY.MM.DD. H:mm', true);
const nederlandsDate = moment(dateString, 'DD-MM-YYYY HH:mm', true);
// greekDate does not work: el Greek Ελληνικά ,
// it has date format DD/MM/YYYY h:mm MM like 20/06/2021 11:15 MM
// where MM is maybe some text like AM/PM ?
// Also some other languages that have non-ascii characters in dates
// do not work.
const greekDate = moment(dateString, 'DD/MM/YYYY h:mm A', true);
const macedonianDate = moment(dateString, 'D.MM.YYYY H:mm', true);
// Try to parse different date formats using native Date parsing
const formats = [
'YYYY-MM-DD HH:mm',
'MM/DD/YYYY HH:mm',
'DD.MM.YYYY HH:mm',
'DD/MM/YYYY HH:mm',
'DD-MM-YYYY HH:mm'
];
let parsedDate = null;
for (const format of formats) {
parsedDate = parseDate(dateString, [format], true);
if (parsedDate) break;
}
// Fallback to native Date parsing
if (!parsedDate) {
parsedDate = new Date(dateString);
}
if (usaDate.isValid()) {
if (isValidDate(parsedDate)) {
// if active poker - store it
if (this.currentData().getPokerQuestion()) {
this._storeDate(usaDate.toDate());
+4 -4
View File
@@ -15,8 +15,8 @@ template(name="editCardSpentTime")
template(name="timeBadge")
if canModifyCard
a.js-edit-time.card-time(title="{{showTitle}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
| {{showTime}}
a.js-edit-time.card-time(title="{{_ 'time'}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
| ⏱️ {{showTime}}
else
a.card-time(title="{{showTitle}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
| {{showTime}}
a.card-time(title="{{_ 'time'}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
| ⏱️ {{showTime}}
+17 -18
View File
@@ -1,14 +1,14 @@
template(name="checklists")
.checklists-title
h3.card-details-item-title
i.fa.fa-check
| ✅
| {{_ 'checklists'}}
if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId position="top")
+addChecklistItemForm
else
a.add-checklist-top.js-open-inlined-form(title="{{_ 'add-checklist'}}")
i.fa.fa-plus
| ➕
if currentUser.isBoardMember
.material-toggle-switch(title="{{_ 'hide-finished-checklist'}}")
//span.toggle-switch-title
@@ -28,7 +28,7 @@ template(name="checklists")
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=false)
else
a.add-checklist.js-open-inlined-form(title="{{_ 'add-checklist'}}")
i.fa.fa-plus
| ➕
template(name="checklistDetail")
.js-checklist.checklist.nodragscroll
@@ -38,7 +38,7 @@ template(name="checklistDetail")
.checklist-title
span
if canModifyCard
a.fa.fa-navicon.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
a.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
if canModifyCard
h4.title.js-open-inlined-form.is-editable
@@ -63,12 +63,12 @@ template(name="checklistDeletePopup")
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
template(name="addChecklistItemForm")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
a(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
textarea.js-add-checklist-item(rows='1' autofocus)
.edit-controls.clearfix
button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
a.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
if showNewlineBecomesNewChecklistItem
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItem'}}")
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItem")
@@ -81,7 +81,7 @@ template(name="addChecklistItemForm")
| {{_ 'originOrder'}}
template(name="editChecklistItemForm")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
a(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
textarea.js-edit-checklist-item(rows='1' autofocus dir="auto")
if $eq type 'item'
@@ -90,12 +90,12 @@ template(name="editChecklistItemForm")
= checklist.title
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
a.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
span(title=createdAt) {{ moment createdAt }}
if canModifyCard
a.js-delete-checklist-item {{_ "delete"}}...
a.js-convert-checklist-item-to-card
i.fa.fa-copy
| 📋
| {{_ 'convertChecklistItemToCardPopup-title'}}
template(name="checklistItems")
@@ -105,7 +105,7 @@ template(name="checklistItems")
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true position="top")
else
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
i.fa.fa-plus
| ➕
.checklist-items.js-checklist-items
each item in checklist.items
+inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
@@ -117,7 +117,7 @@ template(name="checklistItems")
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true)
else
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
i.fa.fa-plus
| ➕
template(name='checklistItemDetail')
.js-checklist-item.checklist-item(class="{{#if item.isFinished }}is-checked{{#if checklist.hideCheckedChecklistItems}} invisible{{/if}}{{/if}}{{#if checklist.hideAllChecklistItems}} is-checked invisible{{/if}}"
@@ -125,8 +125,7 @@ template(name='checklistItemDetail')
if canModifyCard
.check-box-container
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
if isTouchScreenOrShowDesktopDragHandles
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer
= item.title
@@ -140,16 +139,16 @@ template(name="checklistActionsPopup")
ul.pop-over-list
li
a.js-delete-checklist.delete-checklist
i.fa.fa-trash
| 🗑️
| {{_ "delete"}} ...
a.js-move-checklist.move-checklist
i.fa.fa-arrow-right
| ➡️
| {{_ "moveChecklist"}} ...
a.js-copy-checklist.copy-checklist
i.fa.fa-copy
| 📋
| {{_ "copyChecklist"}} ...
a.js-hide-checked-checklist-items
i.fa.fa-eye-slash
| 🙈
| {{_ "hideCheckedChecklistItems"}} ...
.material-toggle-switch(title="{{_ 'hide-checked-items'}}")
if checklist.hideCheckedChecklistItems
@@ -158,7 +157,7 @@ template(name="checklistActionsPopup")
input.toggle-switch(type="checkbox" id="toggleHideCheckedChecklistItems_{{checklist._id}}")
label.toggle-label(for="toggleHideCheckedChecklistItems_{{checklist._id}}")
a.js-hide-all-checklist-items
i.fa.fa-ban
| 🚫
| {{_ "hideAllChecklistItems"}} ...
.material-toggle-switch(title="{{_ 'hideAllChecklistItems'}}")
if checklist.hideAllChecklistItems
+1
View File
@@ -38,6 +38,7 @@
.palette-colors {
display: flex;
flex-wrap: wrap;
justify-content: flex-start; /* left-align color chips in wider popovers */
}
.palette-colors .palette-color {
flex-grow: 1;
+4 -3
View File
@@ -6,7 +6,7 @@ template(name="formLabel")
.palette-colors: each labels
span.card-label.palette-color.js-palette-color(class="card-label-{{color}}")
if(isSelected color)
i.fa.fa-check
| ✅
template(name="createLabelPopup")
form.create-label
@@ -28,7 +28,8 @@ template(name="cardLabelsPopup")
ul.edit-labels-pop-over
each board.labels
li.js-card-label-item
a.card-label-edit-button.fa.fa-pencil.js-edit-label
a.card-label-edit-button.js-edit-label
| ✏️
if isTouchScreenOrShowDesktopDragHandles
span.fa.label-handle(class="fa-arrows" title="{{_ 'dragLabel'}}")
span.card-label.card-label-selectable.js-select-label.card-label-wrapper(class="card-label-{{color}}"
@@ -36,5 +37,5 @@ template(name="cardLabelsPopup")
+viewer
= name
if(isLabelSelected ../_id)
i.card-label-selectable-icon.fa.fa-check
| ✅
a.quiet-button.full.js-add-label {{_ 'label-create'}}
+26 -4
View File
@@ -125,8 +125,19 @@ Template.createLabelPopup.events({
.$('#labelName')
.val()
.trim();
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
board.addLabel(name, color);
// Find the selected color by looking for the palette color that contains the checkmark
let selectedColor = null;
templateInstance.$('.js-palette-color').each(function() {
if ($(this).text().includes('✅')) {
selectedColor = Blaze.getData(this).color;
return false; // break out of loop
}
});
if (selectedColor) {
board.addLabel(name, selectedColor);
}
Popup.back();
},
});
@@ -144,8 +155,19 @@ Template.editLabelPopup.events({
.$('#labelName')
.val()
.trim();
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
board.editLabel(this._id, name, color);
// Find the selected color by looking for the palette color that contains the checkmark
let selectedColor = null;
templateInstance.$('.js-palette-color').each(function() {
if ($(this).text().includes('✅')) {
selectedColor = Blaze.getData(this).color;
return false; // break out of loop
}
});
if (selectedColor) {
board.editLabel(this._id, name, selectedColor);
}
Popup.back();
},
});
+97 -8
View File
@@ -99,8 +99,8 @@
float: none;
}
.minicard .minicard-labels .minicard-label {
width: 1.5vw;
height: 1.5vw;
width: clamp(12px, 1.5vw, 16px);
height: clamp(12px, 1.5vw, 16px);
border-radius: 0.3vw;
margin-right: 0.4vw;
margin-bottom: 0.4vh;
@@ -130,8 +130,8 @@
margin-right: 0.5vw;
}
.minicard .handle {
width: 2.5vw;
height: 2.5vw;
width: clamp(20px, 2.5vw, 28px);
height: clamp(20px, 2.5vw, 28px);
position: absolute;
right: 0.7vw;
top: 0.7vh;
@@ -168,6 +168,80 @@
.minicard .date {
margin-right: 0.4vw;
}
/* Unicode icons for minicard dates - matching cardDate.css */
.minicard .card-date.end-date time::before {
content: "🏁"; /* Finish flag - represents end/completion */
}
.minicard .card-date.due-date time::before {
content: "⏰"; /* Alarm clock - represents due/deadline */
}
.minicard .card-date.start-date time::before {
content: "🚀"; /* Rocket - represents start/launch */
}
.minicard .card-date.received-date time::before {
content: "📥"; /* Inbox tray - represents received/incoming */
}
.minicard .card-date time::before {
font-size: inherit;
margin-right: 0.3em;
display: inline-block;
}
/* Date type specific colors for minicards - matching cardDate.css */
.minicard .card-date.received-date {
background-color: #dbdbdb; /* Grey for received - same as base card-date */
}
.minicard .card-date.received-date:hover,
.minicard .card-date.received-date.is-active {
background-color: #b3b3b3;
}
.minicard .card-date.start-date {
background-color: #90ee90; /* Light green for start */
color: #000; /* Black text for start */
}
.minicard .card-date.start-date:hover,
.minicard .card-date.start-date.is-active {
background-color: #7dd87d;
}
.minicard .card-date.due-date {
background-color: #ffd700; /* Yellow for due */
color: #000; /* Black text for due */
}
.minicard .card-date.due-date:hover,
.minicard .card-date.due-date.is-active {
background-color: #e6c200;
}
.minicard .card-date.end-date {
background-color: #ffb3b3; /* Light red for end */
color: #000; /* Black text for end */
}
.minicard .card-date.end-date:hover,
.minicard .card-date.end-date.is-active {
background-color: #ff9999;
}
/* Font Awesome icons in minicard dates */
.minicard .card-date i.fa {
margin-right: 0.3vw;
font-size: 0.9em;
vertical-align: middle;
}
/* Font Awesome icons in minicard spent time */
.minicard .card-time i.fa {
margin-right: 0.3vw;
font-size: 0.9em;
vertical-align: middle;
}
.minicard .badges {
float: left;
margin-top: 1vh;
@@ -220,8 +294,8 @@
.minicard .minicard-creator .member {
float: right;
border-radius: 50%;
height: 3.5vw;
width: 3.5vw;
height: clamp(24px, 3.5vw, 32px);
width: clamp(24px, 3.5vw, 32px);
margin-bottom: 0.5vh;
}
.minicard .minicard-members .assignee,
@@ -229,8 +303,8 @@
.minicard .minicard-creator .assignee {
float: right;
border-radius: 50%;
height: 3.5vw;
width: 3.5vw;
height: clamp(24px, 3.5vw, 32px);
width: clamp(24px, 3.5vw, 32px);
}
.minicard .minicard-members + .badges,
.minicard .minicard-assignees + .badges,
@@ -588,3 +662,18 @@
transform: scale(1.02);
transition: all 0.2s ease;
}
/* List name display on minicard */
.minicard-list-name {
font-size: 0.75em;
color: #8c8c8c;
margin-top: 0.2vh;
display: flex;
align-items: center;
gap: 0.3vw;
}
.minicard-list-name i.fa {
font-size: 0.8em;
opacity: 0.7;
}
+42 -44
View File
@@ -4,19 +4,13 @@ template(name="minicard")
class="{{#if isLinkedBoard}}linked-board{{/if}}"
class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}")
if canModifyCard
if isTouchScreenOrShowDesktopDragHandles
a.fa.fa-navicon.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
.handle
.fa.fa-arrows
else
a.fa.fa-navicon.minicard-details-menu.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") ☰
.handle
| ↕️
.dates
if getReceived
unless getStart
unless getDue
unless getEnd
.date
+minicardReceivedDate
.date
+minicardReceivedDate
if getStart
.date
+minicardStartDate
@@ -36,7 +30,7 @@ template(name="minicard")
if hasActiveUploads
.minicard-upload-progress
.upload-progress-header
i.fa.fa-upload
| 📤
span {{_ 'uploading-files'}} ({{uploadCount}})
each uploads
.upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
@@ -45,11 +39,11 @@ template(name="minicard")
.upload-progress-fill(style="width: {{progress}}%")
if $eq status 'error'
.upload-progress-error
i.fa.fa-exclamation-triangle
| ⚠️
span {{_ 'upload-failed'}}
else if $eq status 'completed'
.upload-progress-success
i.fa.fa-check
| ✅
span {{_ 'upload-completed'}}
.minicard-title
@@ -61,23 +55,17 @@ template(name="minicard")
| {{ parentCardName }}
if isLinkedBoard
a.js-linked-link
span.linked-icon.fa.fa-folder
span.linked-icon | 📁
else if isLinkedCard
a.js-linked-link
span.linked-icon.fa.fa-id-card
span.linked-icon | 🃏
if getArchived
span.linked-icon.linked-archived.fa.fa-archive
span.linked-icon.linked-archived | 📦
+viewer
if currentBoard.allowsCardNumber
span.card-number
| ##{getCardNumber}
= getTitle
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
.parent-subtext
| {{ parentString ' > ' }}
if $eq 'subtext-with-parent' currentBoard.presentParentTask
.parent-subtext
| {{ parentCardName }}
if labels
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
each labels
@@ -153,7 +141,7 @@ template(name="minicard")
if canModifyCard
if comments.length
.badge(title="{{_ 'card-comments-title' comments.length }}")
span.badge-icon.fa.fa-comment-o.badge-comment.badge-text
span.badge-icon.badge-comment.badge-text | 💬
= ' '
= comments.length
//span.badge-comment.badge-text
@@ -161,42 +149,52 @@ template(name="minicard")
if getDescription
unless currentBoard.allowsDescriptionTextOnMinicard
.badge.badge-state-image-only(title=getDescription)
span.badge-icon.fa.fa-align-left
span.badge-icon | 📝
if getVoteQuestion
.badge.badge-state-image-only(title=getVoteQuestion)
span.badge-icon.fa.fa-thumbs-up(class="{{#if voteState}}text-green{{/if}}")
span.badge-icon(class="{{#if voteState}}text-green{{/if}}") | 👍
span.badge-text {{ voteCountPositive }}
span.badge-icon.fa.fa-thumbs-down(class="{{#if $eq voteState false}}text-red{{/if}}")
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}") | 👎
span.badge-text {{ voteCountNegative }}
if getPokerQuestion
.badge.badge-state-image-only(title=getPokerQuestion)
span.badge-icon.fa.fa-check(class="{{#if pokerState}}text-green{{/if}}")
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}") | ✅
if expiredPoker
span.badge-text {{ getPokerEstimation }}
if attachments.length
if currentBoard.allowsBadgeAttachmentOnMinicard
.badge
span.badge-icon.fa.fa-paperclip
span.badge-icon | 📎
span.badge-text= attachments.length
if checklists.length
.badge(class="{{#if checklistFinished}}is-finished{{/if}}")
span.badge-icon.fa.fa-check-square-o
span.badge-icon | ☑️
span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
if allSubtasks.count
.badge
span.badge-icon.fa.fa-sitemap
span.badge-icon | 🌐
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
if currentBoard.allowsCardSortingByNumber
if currentBoard.allowsCardSortingByNumberOnMinicard
.badge
span.badge-icon.fa.fa-sort
span.badge-icon | 🔢
span.badge-text.check-list-sort {{ sort }}
if currentBoard.allowsDescriptionTextOnMinicard
if getDescription
.minicard-description
+viewer
| {{ getDescription }}
if shouldShowListOnMinicard
.minicard-list-name
| 📋
| {{ listName }}
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
.parent-subtext
| {{ parentString ' > ' }}
if $eq 'subtext-with-parent' currentBoard.presentParentTask
.parent-subtext
| {{ parentCardName }}
template(name="editCardSortOrderPopup")
input.js-edit-card-sort-popup(type='text' autofocus value=sort dir="auto")
@@ -208,50 +206,50 @@ template(name="minicardDetailsActionsPopup")
if canModifyCard
li
a.js-move-card
i.fa.fa-arrow-right
| ➡️
| {{_ 'moveCardPopup-title'}}
li
a.js-copy-card
i.fa.fa-copy
| 📋
| {{_ 'copyCardPopup-title'}}
hr
li
a.js-archive
i.fa.fa-arrow-right
i.fa.fa-archive
| ➡️
| 📦
| {{_ 'archive-card'}}
hr
li
a.js-move-card-to-top
i.fa.fa-arrow-up
| ⬆️
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
i.fa.fa-arrow-down
| ⬇️
| {{_ 'moveCardToBottom-title'}}
hr
li
a.js-add-labels
i.fa.fa-tags
| 🏷️
| {{_ 'card-edit-labels'}}
li
a.js-due-date
i.fa.fa-sign-in
| 📥
| {{_ 'editCardDueDatePopup-title'}}
li
a.js-set-card-color
i.fa.fa-paint-brush
| 🎨
| {{_ 'setCardColorPopup-title'}}
li
a.js-link
i.fa.fa-link
| 🔗
| {{_ 'link-card'}}
li
a.js-toggle-watch-card
if isWatching
i.fa.fa-eye
| 👁️
| {{_ 'unwatch'}}
else
i.fa.fa-eye-slash
| 👁️-slash
| {{_ 'watch'}}
+53 -37
View File
@@ -111,55 +111,58 @@ BlazeComponent.extendComponent({
'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
// Drag and drop file upload handlers
'dragover .minicard'(event) {
event.preventDefault();
event.stopPropagation();
// Only prevent default for file drags to avoid interfering with sortable
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
}
},
'dragenter .minicard'(event) {
event.preventDefault();
event.stopPropagation();
const card = this.data();
const board = card.board();
// Only allow drag-and-drop if user can modify card and board allows attachments
if (card.canModifyCard() && board && board.allowsAttachments) {
// Check if the drag contains files
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
const card = this.data();
const board = card.board();
// Only allow drag-and-drop if user can modify card and board allows attachments
if (Utils.canModifyCard() && board && board.allowsAttachments) {
$(event.currentTarget).addClass('is-dragging-over');
}
}
},
'dragleave .minicard'(event) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
}
},
'drop .minicard'(event) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
const card = this.data();
const board = card.board();
// Check permissions
if (!card.canModifyCard() || !board || !board.allowsAttachments) {
return;
}
// Check if this is a file drop (not a card reorder)
const dataTransfer = event.originalEvent.dataTransfer;
if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) {
return;
}
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
// Check if the drop contains files (not just text/HTML)
if (!dataTransfer.types.includes('Files')) {
return;
}
const card = this.data();
const board = card.board();
const files = dataTransfer.files;
if (files && files.length > 0) {
handleFileUpload(card, files);
// Check permissions
if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
return;
}
// Check if this is a file drop (not a card reorder)
if (!dataTransfer.files || dataTransfer.files.length === 0) {
return;
}
const files = dataTransfer.files;
if (files && files.length > 0) {
handleFileUpload(card, files);
}
}
},
}
@@ -196,6 +199,19 @@ Template.minicard.helpers({
},
uploadCount() {
return uploadProgressManager.getUploadCountForCard(this._id);
},
listName() {
const list = this.list();
return list ? list.title : '';
},
shouldShowListOnMinicard() {
// Show list name if either:
// 1. Board-wide setting is enabled, OR
// 2. This specific card has the setting enabled
const currentBoard = this.currentBoard;
if (!currentBoard) return false;
return currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
}
});
+3 -3
View File
@@ -13,7 +13,7 @@ template(name="resultCard")
.broken-cards-null
| NULL
if getBoard.archived
i.fa.fa-archive
| 📦
li.result-card-context.result-card-context-separator
= ' '
| {{_ 'context-separator'}}
@@ -27,7 +27,7 @@ template(name="resultCard")
.broken-cards-null
| NULL
if getSwimlane.archived
i.fa.fa-archive
| 📦
li.result-card-context.result-card-context-separator
= ' '
| {{_ 'context-separator'}}
@@ -41,4 +41,4 @@ template(name="resultCard")
.broken-cards-null
| NULL
if getList.archived
i.fa.fa-archive
| 📦
+8 -8
View File
@@ -1,6 +1,6 @@
template(name="subtasks")
h3.card-details-item-title
i.fa.fa-sitemap
| 🌐
| {{_ 'subtasks'}}
if currentUser.isBoardAdmin
if toggleDeleteDialog.get
@@ -16,7 +16,7 @@ template(name="subtasks")
+addSubtaskItemForm
else
a.js-open-inlined-form(title="{{_ 'add-subtask'}}")
i.fa.fa-plus
| ➕
template(name="subtaskDetail")
.js-subtasks.subtask
@@ -26,7 +26,7 @@ template(name="subtaskDetail")
.subtask-title
span
if canModifyCard
a.fa.fa-navicon.subtask-details-menu.js-open-subtask-details-menu(title="{{_ 'subtaskActionsPopup-title'}}")
a.subtask-details-menu.js-open-subtask-details-menu(title="{{_ 'subtaskActionsPopup-title'}}")
if canModifyCard
h2.title.js-open-inlined-form.is-editable
+viewer
@@ -40,7 +40,7 @@ template(name="addSubtaskItemForm")
textarea.js-add-subtask-item(rows='1' autofocus dir="auto")
.edit-controls.clearfix
button.primary.confirm.js-submit-add-subtask-item-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
template(name="editSubtaskItemForm")
textarea.js-edit-subtask-item(rows='1' autofocus dir="auto")
@@ -50,7 +50,7 @@ template(name="editSubtaskItemForm")
= subtask.title
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-subtask-item-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
span(title=createdAt) {{ moment createdAt }}
if canModifyCard
if currentUser.isBoardAdmin
@@ -68,7 +68,7 @@ template(name="subtasksItems")
+addSubtaskItemForm
else
a.add-subtask-item.js-open-inlined-form
i.fa.fa-plus
| ➕
| {{_ 'add-subtask-item'}}...
template(name='subtaskItemDetail')
@@ -92,10 +92,10 @@ template(name="subtaskActionsPopup")
ul.pop-over-list
li
a.js-view-subtask(title="{{ subtask.title }}")
i.fa.fa-eye
| 👁️
| {{_ "view-it"}}
if currentUser.isBoardAdmin
a.js-delete-subtask.delete-subtask
i.fa.fa-trash
| 🗑️
| {{_ "delete"}} ...
@@ -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;
}
}
@@ -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>
@@ -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;
+2 -3
View File
@@ -4,11 +4,10 @@ template(name="datepicker")
.fields
.left
label(for="date") {{_ 'date'}}
input.js-date-field#date(type="text" name="date" value=showDate placeholder=dateFormat autofocus)
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
.right
label(for="time") {{_ 'time'}}
input.js-time-field#time(type="text" name="time" value=showTime placeholder=timeFormat)
.js-datepicker
input.js-time-field#time(type="time" name="time" value=showTime)
if error.get
.warning {{_ error.get}}
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
+439 -12
View File
@@ -8,6 +8,228 @@
padding: 0;
float: left;
}
/* List resize handle */
.list-resize-handle {
position: absolute;
top: 0;
right: -3px;
width: 6px;
height: 100%;
cursor: col-resize;
z-index: 10;
background: transparent;
transition: background-color 0.2s ease;
border-radius: 2px;
/* Ensure the handle is clickable */
pointer-events: auto;
}
.list-resize-handle:hover {
background: rgba(0, 123, 255, 0.4);
box-shadow: 0 0 4px rgba(0, 123, 255, 0.3);
}
.list-resize-handle:active {
background: rgba(0, 123, 255, 0.6);
box-shadow: 0 0 6px rgba(0, 123, 255, 0.4);
}
/* Show resize handle only on hover */
.list:hover .list-resize-handle {
background: rgba(0, 0, 0, 0.1);
}
.list:hover .list-resize-handle:hover {
background: rgba(0, 123, 255, 0.4);
box-shadow: 0 0 4px rgba(0, 123, 255, 0.3);
}
/* Add a subtle indicator line */
.list-resize-handle::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2px;
height: 20px;
background: rgba(0, 0, 0, 0.2);
border-radius: 1px;
opacity: 0;
transition: opacity 0.2s ease;
}
.list-resize-handle:hover::before {
opacity: 1;
}
/* Disable resize handle for collapsed lists and mobile view */
.list.list-collapsed .list-resize-handle,
.list.mobile-view .list-resize-handle {
display: none;
}
/* Disable resize handle for auto-width lists */
.list.list-auto-width .list-resize-handle {
display: none;
}
/* Visual feedback during resize */
.list.list-resizing {
transition: none !important;
box-shadow: 0 0 10px rgba(0, 123, 255, 0.3);
/* Ensure the list maintains its new width during resize */
flex: none !important;
flex-basis: auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
/* Override any conflicting layout properties */
float: left !important;
display: block !important;
position: relative !important;
/* Force width to be respected */
width: var(--list-width, auto) !important;
min-width: var(--list-width, auto) !important;
max-width: var(--list-width, auto) !important;
/* Ensure the width is applied immediately */
overflow: visible !important;
}
body.list-resizing-active {
cursor: col-resize !important;
}
body.list-resizing-active * {
cursor: col-resize !important;
}
/* Ensure swimlane container doesn't interfere with list resizing */
.swimlane .list.list-resizing {
/* Override any swimlane flex properties */
flex: none !important;
flex-basis: auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
/* Ensure width is respected */
width: var(--list-width, auto) !important;
min-width: var(--list-width, auto) !important;
max-width: var(--list-width, auto) !important;
}
/* More aggressive override for any container that might interfere */
.js-swimlane .list.list-resizing,
.dragscroll .list.list-resizing,
[id^="swimlane-"] .list.list-resizing {
/* Force the width to be applied */
width: var(--list-width, auto) !important;
min-width: var(--list-width, auto) !important;
max-width: var(--list-width, auto) !important;
flex: none !important;
flex-basis: auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
float: left !important;
display: block !important;
}
/* Ensure the width persists after resize is complete */
.js-swimlane .list[style*="--list-width"],
.dragscroll .list[style*="--list-width"],
[id^="swimlane-"] .list[style*="--list-width"] {
/* Maintain the width after resize */
width: var(--list-width, auto) !important;
min-width: var(--list-width, auto) !important;
max-width: var(--list-width, auto) !important;
flex: none !important;
flex-basis: auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
float: left !important;
display: block !important;
}
/* Ensure consistent header height for all lists */
.list-header {
/* Maintain consistent height and padding for all lists */
min-height: 2.5vh !important;
height: auto !important;
padding: 2.5vh 1.5vw 0.5vh !important;
/* Make sure the background covers the full height */
background-color: #e4e4e4 !important;
border-bottom: 0.8vh solid #e4e4e4 !important;
/* Use original display for consistent button positioning */
display: block !important;
position: relative !important;
/* Prevent vertical expansion but allow normal height */
overflow: hidden !important;
}
/* Ensure title text doesn't cause height changes for all lists */
.list-header .list-header-name {
/* Prevent text wrapping to maintain consistent height */
white-space: nowrap !important;
/* Truncate text with ellipsis if too long */
text-overflow: ellipsis !important;
/* Ensure proper line height */
line-height: 1.2 !important;
/* Ensure it doesn't overflow */
overflow: hidden !important;
/* Add margin to prevent overlap with buttons */
margin-right: 120px !important;
}
/* Position drag handle at top-right corner for ALL lists */
.list-header .list-header-handle {
/* Position at top-right corner, aligned with title text top */
position: absolute !important;
top: 2.5vh !important;
right: 1.5vw !important;
/* Ensure it's above other elements */
z-index: 15 !important;
/* Remove margin since it's absolutely positioned */
margin-right: 0 !important;
/* Ensure proper display */
display: inline-block !important;
/* Ensure it's clickable and shows proper cursor */
cursor: move !important;
pointer-events: auto !important;
/* Add some padding for better clickability */
padding: 4px !important;
}
/* Ensure buttons maintain original positioning */
.js-swimlane .list[style*="--list-width"] .list-header .list-header-plus-top,
.js-swimlane .list[style*="--list-width"] .list-header .js-collapse,
.js-swimlane .list[style*="--list-width"] .list-header .js-open-list-menu,
.dragscroll .list[style*="--list-width"] .list-header .list-header-plus-top,
.dragscroll .list[style*="--list-width"] .list-header .js-collapse,
.dragscroll .list[style*="--list-width"] .list-header .js-open-list-menu,
[id^="swimlane-"] .list[style*="--list-width"] .list-header .list-header-plus-top,
[id^="swimlane-"] .list[style*="--list-width"] .list-header .js-collapse,
[id^="swimlane-"] .list[style*="--list-width"] .list-header .js-open-list-menu {
/* Use original positioning to maintain layout */
position: relative !important;
/* Maintain original spacing */
margin-right: 15px !important;
/* Ensure proper display */
display: inline-block !important;
}
/* Ensure watch icon and card count maintain original positioning */
.js-swimlane .list[style*="--list-width"] .list-header .list-header-watch-icon,
.dragscroll .list[style*="--list-width"] .list-header .list-header-watch-icon,
[id^="swimlane-"] .list[style*="--list-width"] .list-header .list-header-watch-icon,
.js-swimlane .list[style*="--list-width"] .list-header .cardCount,
.dragscroll .list[style*="--list-width"] .list-header .cardCount,
[id^="swimlane-"] .list[style*="--list-width"] .list-header .cardCount {
/* Use original positioning to maintain layout */
position: relative !important;
/* Maintain original spacing */
margin-right: 15px !important;
/* Ensure proper display */
display: inline-block !important;
}
[id^="swimlane-"] .list:first-child {
min-width: 2.5vw;
}
@@ -37,7 +259,70 @@
}
.list.list-collapsed {
flex: none;
min-width: 60px;
max-width: 80px;
width: 60px;
min-height: 60vh;
height: 60vh;
overflow: visible;
position: relative;
}
.list.list-collapsed .list-header {
padding: 1vh 1.5vw 0.5vh;
min-height: 2.5vh !important;
height: auto !important;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
position: relative;
overflow: visible !important;
width: 100%;
max-width: 60px;
margin: 0 auto;
}
.list.list-collapsed .list-header .js-collapse {
margin: 0 auto 20px auto;
z-index: 10;
padding: 8px 12px;
font-size: 12px;
white-space: nowrap;
display: block;
width: fit-content;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
height: auto !important;
margin: 20px 0 0 0 !important;
position: relative !important;
overflow: visible !important;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
text-align: left;
overflow: visible;
white-space: nowrap;
display: block !important;
font-size: 12px;
line-height: 1.2;
color: #333;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #ddd;
padding: 8px 4px;
border-radius: 4px;
margin: 0 auto;
width: 25vh;
height: 60vh;
position: absolute;
left: 50%;
top: 50%;
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
z-index: 10;
visibility: visible !important;
opacity: 1 !important;
pointer-events: none;
}
.list.list-composer .open-list-composer,
.list .list-composer .open-list-composer {
color: #8c8c8c;
@@ -121,11 +406,152 @@
color: #a6a6a6;
margin-right: 15px;
}
.list-header .list-header-uncollapse-left {
.list-header .js-collapse {
color: #a6a6a6;
margin-right: 15px;
display: inline-block;
vertical-align: middle;
padding: 5px 8px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f5f5f5;
cursor: pointer;
font-size: 14px;
}
.list-header .list-header-uncollapse-right {
color: #a6a6a6;
.list-header .js-collapse:hover {
background-color: #e0e0e0;
color: #333;
}
.list.list-collapsed .list-header .js-collapse {
display: inline-block !important;
visibility: visible !important;
opacity: 1 !important;
}
/* Responsive adjustments for collapsed lists */
@media (min-width: 768px) {
.list.list-collapsed {
min-width: 60px;
max-width: 80px;
width: 60px;
min-height: 60vh;
height: 60vh;
}
.list.list-collapsed .list-header {
max-width: 60px;
margin: 0 auto;
min-height: 2.5vh !important;
height: auto !important;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
height: auto !important;
margin: 20px 0 0 0 !important;
position: relative !important;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: 15vh;
font-size: 12px;
height: 30px;
line-height: 1.2;
padding: 8px 4px;
margin: 0 auto;
position: absolute;
left: 50%;
top: 50%;
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
text-align: left;
visibility: visible !important;
opacity: 1 !important;
display: block !important;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #ddd;
color: #333;
z-index: 10;
}
.list.list-collapsed .list-header .js-collapse {
margin: 0 auto 20px auto;
}
}
@media (min-width: 1024px) {
.list.list-collapsed {
min-height: 60vh;
height: 60vh;
}
.list.list-collapsed .list-header {
min-height: 2.5vh !important;
height: auto !important;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
height: auto !important;
margin: 20px 0 0 0 !important;
position: relative !important;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: 15vh;
font-size: 12px;
height: 30px;
line-height: 1.2;
padding: 8px 4px;
margin: 0 auto;
position: absolute;
left: 50%;
top: 50%;
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
text-align: left;
visibility: visible !important;
opacity: 1 !important;
display: block !important;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #ddd;
color: #333;
z-index: 10;
}
.list.list-collapsed .list-header .js-collapse {
margin: 0 auto 20px auto;
}
}
@media (min-width: 1200px) {
.list.list-collapsed {
min-height: 60vh;
height: 60vh;
}
.list.list-collapsed .list-header {
min-height: 2.5vh !important;
height: auto !important;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
height: auto !important;
margin: 20px 0 0 0 !important;
position: relative !important;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: 15vh;
font-size: 12px;
height: 30px;
line-height: 1.2;
padding: 8px 4px;
margin: 0 auto;
position: absolute;
left: 50%;
top: 50%;
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
text-align: left;
visibility: visible !important;
opacity: 1 !important;
display: block !important;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #ddd;
color: #333;
z-index: 10;
}
.list.list-collapsed .list-header .js-collapse {
margin: 0 auto 20px auto;
}
}
.list-header .list-header-collapse {
color: #a6a6a6;
@@ -220,7 +646,7 @@
height: auto;
width: 100%;
min-width: 100%;
border-left: 0px;
border-left: 0px !important;
border-bottom: 1px solid #ccc;
}
.list.mobile-view {
@@ -228,7 +654,7 @@
flex-basis: auto;
width: 100%;
min-width: 100%;
border-left: 0px;
border-left: 0px !important;
}
.list.mobile-view:first-child {
margin-left: 0px;
@@ -237,7 +663,7 @@
flex: 0 0 60px;
height: 60px;
width: 100%;
border-left: 0px;
border-left: 0px !important;
border-bottom: 1px solid #ccc;
}
.list.mobile-view.ui-sortable-helper .list-header.ui-sortable-handle {
@@ -247,7 +673,7 @@
flex: 0 0 60px;
height: 60px;
width: 100%;
border-left: 0px;
border-left: 0px !important;
border-bottom: 1px solid #ccc;
}
.list.mobile-view .list-body {
@@ -339,13 +765,14 @@
align-items: initial;
}
@media screen and (max-width: 800px) {
@media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
.mini-list {
flex: 0 0 60px;
height: auto;
width: 100%;
min-width: 100%;
border-left: 0px;
border-left: 0px !important;
border-bottom: 1px solid #ccc;
}
.list {
@@ -353,7 +780,7 @@
flex-basis: auto;
width: 100%;
min-width: 100%;
border-left: 0px;
border-left: 0px !important;
}
.list:first-child {
margin-left: 0px;
@@ -362,7 +789,7 @@
flex: 0 0 60px;
height: 60px;
width: 100%;
border-left: 0px;
border-left: 0px !important;
border-bottom: 1px solid #ccc;
}
.list.ui-sortable-helper .list-header.ui-sortable-handle {
@@ -372,7 +799,7 @@
flex: 0 0 60px;
height: 60px;
width: 100%;
border-left: 0px;
border-left: 0px !important;
border-bottom: 1px solid #ccc;
}
.list-body {
+1
View File
@@ -4,6 +4,7 @@ template(name='list')
class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}} {{#if isMiniScreen}}mobile-view{{/if}}")
+listHeader
+listBody
.list-resize-handle.js-list-resize-handle.nodragscroll
template(name='miniList')
a.mini-list.js-select-list.js-list(id="js-list-{{_id}}" class="{{#if isMiniScreen}}mobile-view{{/if}}")
+250 -12
View File
@@ -24,6 +24,9 @@ BlazeComponent.extendComponent({
onRendered() {
const boardComponent = this.parentComponent().parentComponent();
// Initialize list resize functionality immediately
this.initializeListResize();
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
const $cards = this.$('.js-minicards');
@@ -147,17 +150,13 @@ BlazeComponent.extendComponent({
});
this.autorun(() => {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$cards.sortable({
handle: '.handle',
});
} else {
$cards.sortable({
handle: '.minicard',
});
}
if ($cards.data('uiSortable') || $cards.data('sortable')) {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$cards.sortable('option', 'handle', '.handle');
} else {
$cards.sortable('option', 'handle', '.minicard');
}
$cards.sortable(
'option',
'disabled',
@@ -198,20 +197,259 @@ BlazeComponent.extendComponent({
listWidth() {
const user = ReactiveCache.getCurrentUser();
const list = Template.currentData();
return user.getListWidth(list.boardId, list._id);
if (!list) return 270; // Return default width if list is not available
if (user) {
// For logged-in users, get from user profile
return user.getListWidthFromStorage(list.boardId, list._id);
} else {
// For non-logged-in users, get from localStorage
try {
const stored = localStorage.getItem('wekan-list-widths');
if (stored) {
const widths = JSON.parse(stored);
if (widths[list.boardId] && widths[list.boardId][list._id]) {
return widths[list.boardId][list._id];
}
}
} catch (e) {
console.warn('Error reading list width from localStorage:', e);
}
return 270; // Return default width if not found
}
},
listConstraint() {
const user = ReactiveCache.getCurrentUser();
const list = Template.currentData();
return user.getListConstraint(list.boardId, list._id);
if (!list) return 550; // Return default constraint if list is not available
if (user) {
// For logged-in users, get from user profile
return user.getListConstraintFromStorage(list.boardId, list._id);
} else {
// For non-logged-in users, get from localStorage
try {
const stored = localStorage.getItem('wekan-list-constraints');
if (stored) {
const constraints = JSON.parse(stored);
if (constraints[list.boardId] && constraints[list.boardId][list._id]) {
return constraints[list.boardId][list._id];
}
}
} catch (e) {
console.warn('Error reading list constraint from localStorage:', e);
}
return 550; // Return default constraint if not found
}
},
autoWidth() {
const user = ReactiveCache.getCurrentUser();
const list = Template.currentData();
if (!user) {
// For non-logged-in users, auto-width is disabled
return false;
}
return user.isAutoWidth(list.boardId);
},
initializeListResize() {
// Check if we're still in a valid template context
if (!Template.currentData()) {
console.warn('No current template data available for list resize initialization');
return;
}
const list = Template.currentData();
const $list = this.$('.js-list');
const $resizeHandle = this.$('.js-list-resize-handle');
// Check if elements exist
if (!$list.length || !$resizeHandle.length) {
console.warn('List or resize handle not found, retrying in 100ms');
Meteor.setTimeout(() => {
if (!this.isDestroyed) {
this.initializeListResize();
}
}, 100);
return;
}
// Only enable resize for non-collapsed, non-auto-width lists
const isAutoWidth = this.autoWidth();
if (list.collapsed || isAutoWidth) {
$resizeHandle.hide();
return;
}
let isResizing = false;
let startX = 0;
let startWidth = 0;
let minWidth = 100; // Minimum width as defined in the existing code
let maxWidth = this.listConstraint() || 1000; // Use constraint as max width
let listConstraint = this.listConstraint(); // Store constraint value for use in event handlers
const component = this; // Store reference to component for use in event handlers
const startResize = (e) => {
isResizing = true;
startX = e.pageX || e.originalEvent.touches[0].pageX;
startWidth = $list.outerWidth();
// Add visual feedback
$list.addClass('list-resizing');
$('body').addClass('list-resizing-active');
// Prevent text selection during resize
$('body').css('user-select', 'none');
e.preventDefault();
e.stopPropagation();
};
const doResize = (e) => {
if (!isResizing) {
return;
}
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
const deltaX = currentX - startX;
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
// Apply the new width immediately for real-time feedback
$list[0].style.setProperty('--list-width', `${newWidth}px`);
$list[0].style.setProperty('width', `${newWidth}px`);
$list[0].style.setProperty('min-width', `${newWidth}px`);
$list[0].style.setProperty('max-width', `${newWidth}px`);
$list[0].style.setProperty('flex', 'none');
$list[0].style.setProperty('flex-basis', 'auto');
$list[0].style.setProperty('flex-grow', '0');
$list[0].style.setProperty('flex-shrink', '0');
e.preventDefault();
e.stopPropagation();
};
const stopResize = (e) => {
if (!isResizing) return;
isResizing = false;
// Calculate final width
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
const deltaX = currentX - startX;
const finalWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
// Ensure the final width is applied
$list[0].style.setProperty('--list-width', `${finalWidth}px`);
$list[0].style.setProperty('width', `${finalWidth}px`);
$list[0].style.setProperty('min-width', `${finalWidth}px`);
$list[0].style.setProperty('max-width', `${finalWidth}px`);
$list[0].style.setProperty('flex', 'none');
$list[0].style.setProperty('flex-basis', 'auto');
$list[0].style.setProperty('flex-grow', '0');
$list[0].style.setProperty('flex-shrink', '0');
// Remove visual feedback but keep the width
$list.removeClass('list-resizing');
$('body').removeClass('list-resizing-active');
$('body').css('user-select', '');
// Keep the CSS custom property for persistent width
// The CSS custom property will remain on the element to maintain the width
// Save the new width using the existing system
const boardId = list.boardId;
const listId = list._id;
// Use the new storage method that handles both logged-in and non-logged-in users
if (process.env.DEBUG === 'true') {
}
const currentUser = ReactiveCache.getCurrentUser();
if (currentUser) {
// For logged-in users, use server method
Meteor.call('applyListWidthToStorage', boardId, listId, finalWidth, listConstraint, (error, result) => {
if (error) {
console.error('Error saving list width:', error);
} else {
if (process.env.DEBUG === 'true') {
}
}
});
} else {
// For non-logged-in users, save to localStorage directly
try {
// Save list width
const storedWidths = localStorage.getItem('wekan-list-widths');
let widths = storedWidths ? JSON.parse(storedWidths) : {};
if (!widths[boardId]) {
widths[boardId] = {};
}
widths[boardId][listId] = finalWidth;
localStorage.setItem('wekan-list-widths', JSON.stringify(widths));
// Save list constraint
const storedConstraints = localStorage.getItem('wekan-list-constraints');
let constraints = storedConstraints ? JSON.parse(storedConstraints) : {};
if (!constraints[boardId]) {
constraints[boardId] = {};
}
constraints[boardId][listId] = listConstraint;
localStorage.setItem('wekan-list-constraints', JSON.stringify(constraints));
if (process.env.DEBUG === 'true') {
}
} catch (e) {
console.warn('Error saving list width/constraint to localStorage:', e);
}
}
e.preventDefault();
};
// Mouse events
$resizeHandle.on('mousedown', startResize);
$(document).on('mousemove', doResize);
$(document).on('mouseup', stopResize);
// Touch events for mobile
$resizeHandle.on('touchstart', startResize, { passive: false });
$(document).on('touchmove', doResize, { passive: false });
$(document).on('touchend', stopResize, { passive: false });
// Prevent dragscroll interference
$resizeHandle.on('mousedown', (e) => {
e.stopPropagation();
});
// Reactively update resize handle visibility when auto-width changes
component.autorun(() => {
if (component.autoWidth()) {
$resizeHandle.hide();
} else {
$resizeHandle.show();
}
});
// Clean up on component destruction
component.onDestroyed(() => {
$(document).off('mousemove', doResize);
$(document).off('mouseup', stopResize);
$(document).off('touchmove', doResize);
$(document).off('touchend', stopResize);
});
},
}).register('list');
Template.miniList.events({
+2 -2
View File
@@ -32,7 +32,7 @@ template(name="listBody")
+addCardForm(listId=_id position="bottom")
else
a.open-minicard-composer.js-card-composer.js-open-inlined-form(title="{{_ 'add-card-to-bottom-of-list'}}")
i.fa.fa-plus
| ➕
template(name="spinnerList")
.sk-spinner.sk-spinner-list(
@@ -54,7 +54,7 @@ template(name="addCardForm")
.add-controls.clearfix
button.primary.confirm(type="submit") {{_ 'add'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form | ❌
.add-controls.clearfix
unless currentBoard.isTemplatesBoard
unless currentBoard.isTemplateBoard
+8
View File
@@ -472,6 +472,14 @@ BlazeComponent.extendComponent({
if (!this.selectedBoardId.get()) {
return [];
}
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
if (!board) {
return [];
}
// Ensure default swimlane exists
board.getDefaultSwimline();
const swimlanes = ReactiveCache.getSwimlanes(
{
boardId: this.selectedBoardId.get()
+36 -33
View File
@@ -7,12 +7,10 @@ template(name="listHeader")
else
if isMiniScreen
if currentList
a.list-header-left-icon.fa.fa-angle-left.js-unselect-list
a.list-header-left-icon.js-unselect-list
| ◀️
else
if collapsed
a.js-collapse(title="{{_ 'uncollapse'}}")
i.fa.fa-arrow-left.list-header-uncollapse-left
i.fa.fa-arrow-right.list-header-uncollapse-right
if showCardsCountForList cards.length
br
span.cardCount {{cardsCount}}
@@ -29,6 +27,10 @@ template(name="listHeader")
if showCardsCountForList cards.length
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
else
if collapsed
a.js-collapse(title="{{_ 'uncollapse'}}")
| ⬅️
| ➡️
div(class="{{#if collapsed}}list-rotated{{/if}}")
h2.list-header-name(
title="{{ moment modifiedAt 'LLL' }}"
@@ -45,94 +47,95 @@ template(name="listHeader")
if isMiniScreen
if currentList
if isWatching
i.list-header-watch-icon.fa.fa-eye
i.list-header-watch-icon | 👁️
div.list-header-menu
unless currentUser.isCommentOnly
if canSeeAddCard
a.js-add-card.fa.fa-plus.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
a.fa.fa-navicon.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") ➕
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
else
a.list-header-menu-icon.fa.fa-angle-right.js-select-list
a.list-header-handle.handle.fa.fa-arrows.js-list-handle
a.list-header-menu-icon.js-select-list ▶️
a.list-header-handle.handle.js-list-handle ↕️
else if currentUser.isBoardMember
if isWatching
i.list-header-watch-icon.fa.fa-eye
i.list-header-watch-icon | 👁️
unless collapsed
div.list-header-menu
unless currentUser.isCommentOnly
//if isBoardAdmin
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
if canSeeAddCard
a.js-add-card.fa.fa-plus.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") ➕
a.js-collapse(title="{{_ 'collapse'}}")
i.fa.fa-arrow-right.list-header-collapse-right
i.fa.fa-arrow-left.list-header-collapse-left
a.fa.fa-navicon.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
if currentUser.isBoardAdmin
if isTouchScreenOrShowDesktopDragHandles
a.list-header-handle.handle.fa.fa-arrows.js-list-handle
| ⬅️
| ➡️
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
if currentUser.isBoardMember
unless currentUser.isCommentOnly
a.list-header-handle.handle.js-list-handle ↕️
template(name="editListTitleForm")
.list-composer
input.list-name-input.full-line(type="text" value=title autofocus)
.edit-controls.clearfix
button.primary.confirm(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
a.js-close-inlined-form
| ❌
template(name="listActionPopup")
ul.pop-over-list
li
a.js-add-card.list-header-plus-bottom
i.fa.fa-plus
i.fa.fa-arrow-down
| ➕
| ⬇️
| {{_ 'add-card-to-bottom-of-list'}}
hr
ul.pop-over-list
li
a.js-set-list-width
i.fa.fa-arrows-h
| ↔️
| {{_ 'set-list-width'}}
ul.pop-over-list
li
a.js-toggle-watch-list
if isWatching
i.fa.fa-eye
| 👁️
| {{_ 'unwatch'}}
else
i.fa.fa-eye-slash
| 🙈
| {{_ 'watch'}}
unless currentUser.isCommentOnly
unless currentUser.isWorker
ul.pop-over-list
li
a.js-set-color-list
i.fa.fa-paint-brush
| 🎨
| {{_ 'set-color-list'}}
ul.pop-over-list
if cards.length
li
a.js-select-cards
i.fa.fa-check-square
| ☑️
| {{_ 'list-select-cards'}}
if currentUser.isBoardAdmin
ul.pop-over-list
li
a.js-set-wip-limit
i.fa.fa-ban
| 🚫
| {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
unless currentUser.isWorker
hr
ul.pop-over-list
li
a.js-close-list
i.fa.fa-arrow-right
i.fa.fa-archive
| ➡️
| 📦
| {{_ 'archive-list'}}
hr
ul.pop-over-list
li
a.js-more
i.fa.fa-link
| 🔗
| {{_ 'listMorePopup-title'}}
template(name="boardLists")
@@ -149,7 +152,7 @@ template(name="listMorePopup")
span.clearfix
span {{_ 'link-list'}}
= ' '
i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
| {{#if board.isPublic}}🌐{{else}}🔒{{/if}}
input.inline-input(type="text" readonly value="{{ rootUrl }}")
| {{_ 'added'}}
span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
@@ -169,7 +172,7 @@ template(name="setWipLimitPopup")
ul.pop-over-list
li: a.js-enable-wip-limit {{_ 'enable-wip-limit'}}
if isWipLimitEnabled
i.fa.fa-check
| ✅
if isWipLimitEnabled
p
input.wip-limit-value(type="number" value="{{ wipLimitValue }}" min="1" max="99")
@@ -197,7 +200,7 @@ template(name="setListWidthPopup")
br
a.js-auto-width-board(
title="{{#if isAutoWidth}}{{_ 'click-to-disable-auto-width'}}{{else}}{{_ 'click-to-enable-auto-width'}}{{/if}}")
i.fa(class="fa-solid fa-{{#if isAutoWidth}}compress{{else}}expand{{/if}}")
| {{#if isAutoWidth}}🗜️{{else}}📏{{/if}}
span {{_ 'auto-list-width'}}
template(name="listWidthErrorPopup")
@@ -211,6 +214,6 @@ template(name="setListColorPopup")
// note: we use the swimlane palette to have more than just the border
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color)
i.fa.fa-check
| ✅
button.primary.confirm.js-submit {{_ 'save'}}
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
+29
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'}}
+55
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);
}
},
});
+9 -9
View File
@@ -1,23 +1,23 @@
template(name="dueCardsHeaderBar")
if currentUser
h1
i.fa.fa-calendar
| 📅
| {{_ 'dueCards-title'}}
.board-header-btns.left
a.board-header-btn.js-due-cards-view-change(title="{{_ 'dueCardsViewChange-title'}}")
i.fa.fa-caret-down
| ▼
if $eq dueCardsView 'me'
i.fa.fa-user
| 👤
| {{_ 'dueCardsViewChange-choice-me'}}
if $eq dueCardsView 'all'
i.fa.fa-users
| 👥
| {{_ 'dueCardsViewChange-choice-all'}}
template(name="dueCardsModalTitle")
if currentUser
h2
i.fa.fa-keyboard-o
| ⌨️
| {{_ 'dueCards-title'}}
template(name="dueCards")
@@ -40,18 +40,18 @@ template(name="dueCardsViewChangePopup")
li
with "dueCardsViewChange-choice-me"
a.js-due-cards-view-me
i.fa.fa-user.colorful
| 👤
| {{_ 'dueCardsViewChange-choice-me'}}
if $eq Utils.dueCardsView "me"
i.fa.fa-check
| ✅
hr
li
with "dueCardsViewChange-choice-all"
a.js-due-cards-view-all
i.fa.fa-users.colorful
| 👥
| {{_ 'dueCardsViewChange-choice-all'}}
span.sub-name
+viewer
| {{_ 'dueCardsViewChange-choice-all-description' }}
if $eq Utils.dueCardsView "all"
i.fa.fa-check
| ✅
+57 -8
View File
@@ -15,7 +15,7 @@ BlazeComponent.extendComponent({
dueCardsView() {
// eslint-disable-next-line no-console
// console.log('sort:', Utils.dueCardsView());
return Utils.dueCardsView();
return Utils && Utils.dueCardsView ? Utils.dueCardsView() : 'me';
},
events() {
@@ -38,12 +38,16 @@ BlazeComponent.extendComponent({
return [
{
'click .js-due-cards-view-me'() {
Utils.setDueCardsView('me');
if (Utils && Utils.setDueCardsView) {
Utils.setDueCardsView('me');
}
Popup.back();
},
'click .js-due-cards-view-all'() {
Utils.setDueCardsView('all');
if (Utils && Utils.setDueCardsView) {
Utils.setDueCardsView('all');
}
Popup.back();
},
},
@@ -54,7 +58,38 @@ BlazeComponent.extendComponent({
class DueCardsComponent extends CardSearchPagedComponent {
onCreated() {
super.onCreated();
// Add a small delay to ensure ReactiveCache is ready
this.searchRetryCount = 0;
this.maxRetries = 3;
// Use a timeout to ensure the search runs after the component is fully initialized
Meteor.setTimeout(() => {
this.performSearch();
}, 100);
}
performSearch() {
if (process.env.DEBUG === 'true') {
console.log('Performing due cards search, attempt:', this.searchRetryCount + 1);
}
// Check if user is authenticated
const currentUser = ReactiveCache.getCurrentUser();
if (!currentUser) {
if (process.env.DEBUG === 'true') {
console.log('User not authenticated, waiting...');
}
Meteor.setTimeout(() => {
this.performSearch();
}, 1000);
return;
}
if (process.env.DEBUG === 'true') {
console.log('User authenticated:', currentUser.username);
}
const queryParams = new QueryParams();
queryParams.addPredicate(OPERATOR_HAS, {
field: PREDICATE_DUE_AT,
@@ -66,17 +101,31 @@ class DueCardsComponent extends CardSearchPagedComponent {
order: ORDER_ASCENDING,
});
if (Utils.dueCardsView() !== 'all') {
queryParams.addPredicate(OPERATOR_USER, ReactiveCache.getCurrentUser().username);
}
// Note: User filtering is handled server-side based on board membership
// The OPERATOR_USER filter is too restrictive as it only shows cards where
// the user is assigned or a member of the card, not the board
// if (Utils && Utils.dueCardsView && Utils.dueCardsView() !== 'all') {
// const currentUser = ReactiveCache.getCurrentUser();
// if (currentUser && currentUser.username) {
// queryParams.addPredicate(OPERATOR_USER, currentUser.username);
// }
// }
// Debug: Log the query parameters
if (process.env.DEBUG === 'true') {
console.log('Due cards query params:', queryParams.params);
console.log('Due cards query text:', queryParams.text);
console.log('Due cards has predicates:', queryParams.getPredicates('has'));
console.log('Due cards sort predicates:', queryParams.getPredicates('sort'));
}
this.runGlobalSearch(queryParams);
}
dueCardsView() {
// eslint-disable-next-line no-console
//console.log('sort:', Utils.dueCardsView());
return Utils.dueCardsView();
return Utils && Utils.dueCardsView ? Utils.dueCardsView() : 'me';
}
sortByBoard() {
+4 -2
View File
@@ -1,6 +1,8 @@
template(name="editor")
a.fa.fa-brands.fa-markdown(title="{{_ 'convert-to-markdown'}}")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
a(title="{{_ 'convert-to-markdown'}}")
| 📝
a(title="{{_ 'copy-text-to-clipboard'}}")
| 📋
span.copied-tooltip {{_ 'copied'}}
textarea.editor(
dir="auto"
+6 -4
View File
@@ -1,20 +1,21 @@
template(name="globalSearchHeaderBar")
if currentUser
h1
i.fa.fa-search
| 🔍
| {{_ 'globalSearch-title'}}
template(name="globalSearchModalTitle")
if currentUser
h2
i.fa.fa-keyboard-o
| ⌨️
| {{_ 'globalSearch-title'}}
template(name="resultsPaged")
if resultsHeading.get
h1
= resultsHeading.get
a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}")
a(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}")
| 🔗
each card in results.get
+resultCard(card)
table.global-search-footer
@@ -41,7 +42,8 @@ template(name="globalSearch")
value="{{ query.get }}"
autofocus dir="auto"
)
a.js-new-search.fa.fa-eraser
a.js-new-search
| 🧹
if debug.get.show
h1 Debug
if debug.get.showSelector
+42 -9
View File
@@ -58,7 +58,7 @@
float: left;
overflow: hidden;
line-height: 28px;
margin: 0 2px;
margin: 0 12px;
}
#header #header-main-bar .board-header-btn i.fa {
float: left;
@@ -100,8 +100,9 @@
z-index: 1000;
padding: 10px 0px;
align-items: center;
flex-wrap: wrap; /* Allow wrapping on mobile */
min-height: 28px; /* Allow height to grow */
flex-wrap: nowrap; /* Prevent wrapping to keep single row */
min-height: 28px;
overflow: hidden; /* Prevent content from overflowing */
}
#header-quick-access .home-icon {
display: flex;
@@ -167,13 +168,39 @@
white-space: nowrap;
padding: 10px;
margin: -10px;
flex: 1; /* Take up available space */
min-width: 0; /* Allow shrinking below content size */
display: flex; /* Use flexbox for better control */
align-items: center;
scrollbar-width: thin; /* Firefox */
scrollbar-color: rgba(255, 255, 255, 0.3) transparent; /* Firefox */
}
/* Webkit scrollbar styling for better UX */
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar {
height: 4px;
}
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar-track {
background: transparent;
}
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
#header-quick-access ul.header-quick-access-list li {
display: inline;
display: inline-block; /* Keep inline-block for proper spacing */
width: auto;
color: #d9d9d9;
padding: 12px 0px;
margin: -10px 0px;
flex-shrink: 0; /* Prevent items from shrinking */
white-space: nowrap; /* Prevent text wrapping within items */
}
#header-quick-access ul.header-quick-access-list li a {
padding: 12px 10px;
@@ -220,6 +247,7 @@
margin: 0;
margin-top: 1px;
}
#header-quick-access #header-user-bar .header-user-bar-name,
#header-quick-access #header-help {
margin: 4px 8px 0 0;
@@ -314,7 +342,8 @@
}
/* Make zoom input wider on all mobile screens */
@media screen and (max-width: 800px) {
@media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
#header-quick-access .zoom-controls .zoom-input {
min-width: 50px !important; /* Wider on mobile */
width: 50px !important; /* Fixed width to show all numbers */
@@ -424,7 +453,8 @@
margin: 6px 5px 0;
width: 12px;
}
@media screen and (max-width: 800px) {
@media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
#header #header-main-bar {
height: 40px;
}
@@ -446,6 +476,8 @@
transition: background-color 0.4s;
width: 100%;
z-index: 30;
flex-wrap: nowrap !important; /* Force single row on mobile */
overflow: hidden; /* Prevent content overflow */
}
/* Mobile home icon styling */
@@ -489,11 +521,12 @@
screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) {
#header-quick-access {
height: auto !important; /* Allow height to grow */
height: 48px !important; /* Fixed height for mobile */
min-height: 48px !important; /* Minimum height for mobile */
flex-wrap: wrap !important; /* Force wrapping */
align-items: flex-start !important; /* Align to top when wrapping */
flex-wrap: nowrap !important; /* Force single row */
align-items: center !important; /* Center align items */
padding: 8px 0px !important; /* Adjust padding for mobile */
overflow: hidden !important; /* Prevent content overflow */
}
#header-quick-access {
font-size: 2em !important; /* 2x bigger base font size */
+14 -8
View File
@@ -9,10 +9,10 @@ template(name="header")
// Home icon - always at left side of logo
span.home-icon.allBoards
a(href="{{pathFor 'home'}}")
span.fa.fa-home
| 🏠
| {{_ 'all-boards'}}
// Logo - always visible in desktop mode
// Logo - visible; on mobile constrained by CSS
unless currentSetting.hideLogo
if currentSetting.customTopLeftCornerLogoImageUrl
if currentSetting.customTopLeftCornerLogoLinkUrl
@@ -80,14 +80,20 @@ template(name="header")
.mobile-mode-toggle
a.board-header-btn.js-mobile-mode-toggle(title="{{_ 'mobile-desktop-toggle'}}" class="{{#if mobileMode}}mobile-active{{else}}desktop-active{{/if}}")
i.fa.fa-mobile.mobile-icon(class="{{#if mobileMode}}active{{/if}}")
i.fa.fa-desktop.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}")
i.mobile-icon(class="{{#if mobileMode}}active{{/if}}") 📱
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}") 🖥️
// Bookmarks button - desktop opens popup, mobile routes to page
a.board-header-btn.js-open-bookmarks(title="{{_ 'bookmarks'}}")
| 🔖
// Notifications
+notifications
if currentSetting.customHelpLinkUrl
#header-help
a(href="{{currentSetting.customHelpLinkUrl}}", title="{{_ 'help'}}", target="_blank", rel="noopener noreferrer")
span.fa.fa-question
| ❓
+headerUserBar
@@ -106,15 +112,15 @@ template(name="header")
if hasAnnouncement
.announcement
p
i.fa.fa-bullhorn
| 📢
+viewer
| #{announcement}
i.fa.fa-times-circle.js-close-announcement
| ❌
template(name="offlineWarning")
.offline-warning
p
i.fa.fa-warning
| ⚠️
| {{_ 'app-is-offline'}}
a.app-try-reconnect {{_ 'app-try-reconnect'}}
+11
View File
@@ -104,6 +104,9 @@ Template.header.events({
const currentMode = Utils.getMobileMode();
Utils.setMobileMode(!currentMode);
},
'click .js-open-bookmarks'(evt) {
// Already added but ensure single definition -- safe guard
},
'click .js-close-announcement'() {
$('.announcement').hide();
},
@@ -124,6 +127,14 @@ Template.header.events({
location.reload();
}
},
'click .js-open-bookmarks'(evt) {
// Desktop: open popup, Mobile: route to page
if (Utils.isMiniScreen()) {
FlowRouter.go('bookmarks');
} else {
Popup.open('bookmarksPopup')(evt);
}
},
});
Template.offlineWarning.events({
@@ -1,12 +1,12 @@
template(name="shortcutsHeaderBar")
h1
a.back-btn(href="{{pathFor 'home'}}")
i.fa.fa-chevron-left
| ◀️
| {{_ 'keyboard-shortcuts'}}
template(name="shortcutsModalTitle")
h2
i.fa.fa-keyboard-o
| ⌨️
| {{_ 'keyboard-shortcuts'}}
template(name="keyboardShortcuts")
+281 -4
View File
@@ -52,9 +52,15 @@ input,
select,
textarea,
button {
font: clamp(12px, 2.5vw, 16px) Roboto, Poppins, "Helvetica Neue", Arial, Helvetica, sans-serif;
line-height: 1.3;
font: clamp(14px, 2.5vw, 18px) Roboto, Poppins, "Helvetica Neue", Arial, Helvetica, sans-serif;
line-height: 1.4;
color: #4d4d4d;
/* Improve text rendering */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Better text selection */
-webkit-user-select: text;
user-select: text;
}
html {
font-size: 100%;
@@ -460,20 +466,291 @@ a:not(.disabled).is-active i.fa {
.no-scrollbars::-webkit-scrollbar {
display: none !important;
}
@media screen and (max-width: 800px) {
/* ========================================
MOBILE & TABLET RESPONSIVE IMPROVEMENTS
======================================== */
/* Mobile devices (up to 800px) and all iPhone models */
@media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) and (orientation: landscape),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) and (orientation: portrait) {
#content {
margin: 1px 0px 0px 0px;
height: calc(100% - 0px);
/* Improve touch scrolling */
-webkit-overflow-scrolling: touch;
}
#content > .wrapper {
margin-top: 0px;
padding: 8px;
}
.wrapper {
height: calc(100% - 31px);
margin: 0px;
padding: 8px;
}
.panel-default {
width: 83vw;
width: 95vw;
max-width: 95vw;
margin: 0 auto;
}
/* Improve touch targets */
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 44px;
min-width: 44px;
padding: 12px 16px;
font-size: 16px; /* Prevent zoom on iOS */
touch-action: manipulation;
}
/* Form elements */
input, select, textarea {
font-size: 16px; /* Prevent zoom on iOS */
padding: 12px;
min-height: 44px;
touch-action: manipulation;
}
/* Cards and lists */
.minicard {
min-height: 48px;
padding: 12px;
margin-bottom: 8px;
touch-action: manipulation;
}
.list {
margin: 0 8px;
min-width: 280px;
}
/* Board canvas */
.board-canvas {
padding: 8px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* Header mobile layout */
#header {
padding: 8px;
/* Keep top bar on a single row on small screens */
flex-wrap: nowrap;
align-items: center;
gap: 8px;
}
#header-quick-access {
/* Keep quick-access items in one row */
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
width: 100%;
}
/* Hide elements that should move to the hamburger menu on mobile */
#header-quick-access .header-quick-access-list,
#header-quick-access #header-help {
display: none !important;
}
/* Show only the home icon (hide the trailing text) on mobile */
#header-quick-access .home-icon a {
display: inline-flex;
align-items: center;
max-width: 28px; /* enough to display the icon */
overflow: hidden;
white-space: nowrap;
}
/* Hide text in home icon on mobile, show only icon */
#header-quick-access .home-icon a span:not(.fa) {
display: none !important;
}
/* Ensure proper spacing for mobile header elements */
#header-quick-access .zoom-controls {
margin-left: auto;
margin-right: 8px;
}
.mobile-mode-toggle {
margin-right: 8px;
}
#header-user-bar {
margin-left: auto;
}
/* Ensure header elements don't wrap on very small screens */
#header-quick-access {
min-width: 0; /* Allow flexbox to shrink */
}
/* Make sure logo doesn't take too much space on mobile */
#header-quick-access img {
max-height: 24px;
max-width: 120px;
}
/* Ensure zoom controls are compact on mobile */
.zoom-controls .zoom-level {
padding: 4px 8px;
font-size: 12px;
}
/* Modal mobile optimization */
#modal .modal-content,
#modal .modal-content-wide {
width: 95vw;
max-width: 95vw;
margin: 2vh auto;
padding: 16px;
max-height: 90vh;
overflow-y: auto;
}
/* Table mobile optimization */
table {
font-size: 14px;
width: 100%;
display: block;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
/* Admin panel mobile optimization */
.setting-content .content-body {
flex-direction: column;
gap: 16px;
padding: 8px;
}
.setting-content .content-body .side-menu {
width: 100%;
order: 2;
}
.setting-content .content-body .main-body {
order: 1;
min-height: 60vh;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
/* Tablet devices (768px - 1024px) */
@media screen and (min-width: 768px) and (max-width: 1024px) {
#content > .wrapper {
padding: 12px;
}
.wrapper {
padding: 12px;
}
.panel-default {
width: 90vw;
max-width: 90vw;
}
/* Touch-friendly but more compact */
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 48px;
min-width: 48px;
padding: 10px 14px;
}
.minicard {
min-height: 40px;
padding: 10px;
}
.list {
margin: 0 12px;
min-width: 300px;
}
.board-canvas {
padding: 12px;
}
#header {
padding: 12px 16px;
}
#modal .modal-content {
width: 80vw;
max-width: 600px;
}
#modal .modal-content-wide {
width: 90vw;
max-width: 800px;
}
.setting-content .content-body {
gap: 20px;
}
.setting-content .content-body .side-menu {
width: 250px;
}
}
/* Large displays and digital signage (1920px+) */
@media screen and (min-width: 1920px) {
body {
font-size: 18px;
}
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 56px;
min-width: 56px;
padding: 16px 20px;
font-size: 18px;
}
.minicard {
min-height: 56px;
padding: 16px;
font-size: 18px;
}
.list {
margin: 0 8px;
min-width: 360px;
}
.board-canvas {
padding: 0;
}
#header {
padding: 0 8px;
}
#content > .wrapper {
padding: 0;
}
#modal .modal-content {
width: 600px;
}
#modal .modal-content-wide {
width: 1000px;
}
.setting-content .content-body {
gap: 32px;
}
.setting-content .content-body .side-menu {
width: 320px;
}
}
.inline-input {
+5 -3
View File
@@ -2,7 +2,7 @@ template(name="main")
html(lang="{{TAPi18n.getLanguage}}")
head
title
meta(name="viewport" content="width=device-width, initial-scale=1")
meta(name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes")
meta(http-equiv="X-UA-Compatible" content="IE=edge")
//- XXX We should use pathFor in the following `href` to support the case
where the application is deployed with a path prefix, but it seems to be
@@ -77,19 +77,21 @@ template(name="defaultLayout")
| {{{afterBodyStart}}}
+Template.dynamic(template=content)
| {{{beforeBodyEnd}}}
+migrationProgress
+boardConversionProgress
if (Modal.isOpen)
#modal
.overlay
if (Modal.isWide)
.modal-content-wide.modal-container
a.modal-close-btn.js-close-modal
i.fa.fa-times-thin
| ❌
+Template.dynamic(template=Modal.getHeaderName)
+Template.dynamic(template=Modal.getTemplateName)
else
.modal-content.modal-container
a.modal-close-btn.js-close-modal
i.fa.fa-times-thin
| ❌
+Template.dynamic(template=Modal.getHeaderName)
+Template.dynamic(template=Modal.getTemplateName)
+12
View File
@@ -92,6 +92,18 @@ Template.userFormsLayout.onRendered(() => {
if (loginInput && loginInput.name && (loginInput.name.toLowerCase().includes('user') || loginInput.name.toLowerCase().includes('email'))) {
loginInput.setAttribute('autocomplete', 'username email');
}
// Add autocomplete attributes to password fields for WCAG compliance
const passwordInputs = document.querySelectorAll('input[type="password"]');
passwordInputs.forEach(input => {
if (input.name && input.name.includes('password')) {
if (input.name.includes('password_again') || input.name.includes('new_password')) {
input.setAttribute('autocomplete', 'new-password');
} else {
input.setAttribute('autocomplete', 'current-password');
}
}
});
});
});
+9 -9
View File
@@ -3,23 +3,23 @@ template(name="myCardsHeaderBar")
h1
//a.back-btn(href="{{pathFor 'home'}}")
// i.fa.fa-chevron-left
i.fa.fa-list
| 📋
| {{_ 'my-cards'}}
.board-header-btns.left
a.board-header-btn.js-my-cards-view-change(title="{{_ 'myCardsViewChange-title'}}")
i.fa.fa-caret-down
| ▼
if $eq myCardsView 'boards'
i.fa.fa-trello
| 📋
| {{_ 'myCardsViewChange-choice-boards'}}
if $eq myCardsView 'table'
i.fa.fa-table
| 📊
| {{_ 'myCardsViewChange-choice-table'}}
template(name="myCardsModalTitle")
if currentUser
h2
i.fa.fa-keyboard-o
| ⌨️
| {{_ 'my-cards'}}
template(name="myCards")
@@ -102,15 +102,15 @@ template(name="myCardsViewChangePopup")
li
with "myCardsViewChange-choice-boards"
a.js-my-cards-view-boards
i.fa.fa-trello.colorful
| 📋
| {{_ 'myCardsViewChange-choice-boards'}}
if $eq Utils.myCardsView "boards"
i.fa.fa-check
| ✅
hr
li
with "myCardsViewChange-choice-table"
a.js-my-cards-view-table
i.fa.fa-table.colorful
| 📊
| {{_ 'myCardsViewChange-choice-table'}}
if $eq Utils.myCardsView "table"
i.fa.fa-check
| ✅
+317 -17
View File
@@ -5,7 +5,8 @@
border-bottom-color: #c2c2c2;
box-shadow: 0 0.2vh 0.8vh rgba(0,0,0,0.3);
position: absolute;
width: min(300px, 40vw);
/* Wider default to fit full color palette */
width: min(380px, 55vw);
z-index: 99999;
margin-top: 0.7vh;
}
@@ -72,20 +73,319 @@
}
.pop-over .content-wrapper {
width: 100%;
overflow: hidden;
max-height: calc(70vh + 20px);
overflow-y: auto;
overflow-x: hidden;
}
/* Allow dynamic max-height to override default constraint */
.pop-over[style*="max-height"] .content-wrapper {
max-height: inherit;
}
.pop-over .content-container {
width: 5000px;
max-height: 70vh;
width: 100%;
max-height: calc(70vh + 20px);
transition: transform 0.2s;
}
/* Allow dynamic max-height to override default constraint for content-container */
.pop-over[style*="max-height"] .content-container {
max-height: inherit;
}
/* Admin edit popups: use full height */
.pop-over[data-popup="editUser"],
.pop-over[data-popup="editOrg"],
.pop-over[data-popup="editTeam"] {
height: calc(100vh - 20px) !important;
max-height: calc(100vh - 20px) !important;
}
.pop-over[data-popup="editUser"] .content-wrapper,
.pop-over[data-popup="editOrg"] .content-wrapper,
.pop-over[data-popup="editTeam"] .content-wrapper {
max-height: calc(100vh - 80px) !important; /* Subtract header height */
height: calc(100vh - 80px) !important;
overflow-y: auto !important;
}
.pop-over[data-popup="editUser"] .content-container,
.pop-over[data-popup="editOrg"] .content-container,
.pop-over[data-popup="editTeam"] .content-container {
max-height: calc(100vh - 80px) !important; /* Subtract header height */
height: calc(100vh - 80px) !important;
}
/* Ensure language popup list can scroll properly */
.pop-over .pop-over-list {
max-height: none;
overflow: visible;
}
/* Specific styling for language popup list */
.pop-over[data-popup="changeLanguage"] .pop-over-list {
max-height: none;
overflow: visible;
height: auto;
flex: 1;
}
/* Ensure content div in language popup contains all items */
.pop-over[data-popup="changeLanguage"] .content {
height: auto;
min-height: 100%;
display: flex;
flex-direction: column;
}
/* Allow dynamic height for Change Language popup */
.pop-over[data-popup="changeLanguage"] .content-wrapper {
max-height: inherit; /* Use dynamic height from JavaScript */
}
.pop-over[data-popup="changeLanguage"] .content-container {
max-height: inherit; /* Use dynamic height from JavaScript */
}
/* Make language popup extend to bottom of browser window */
.pop-over[data-popup="changeLanguage"] {
height: calc(100vh - 30px);
min-height: 300px;
/* Adjust positioning to move popup 30px higher */
transform: translateY(-30px);
}
.pop-over[data-popup="changeLanguage"] .content-wrapper {
height: calc(100% - 50px); /* Subtract header height more precisely */
min-height: 250px;
overflow-y: auto;
max-height: none; /* Remove any max-height constraints */
display: flex;
flex-direction: column;
}
.pop-over[data-popup="changeLanguage"] .content-container {
height: auto; /* Let content determine height */
min-height: 250px;
max-height: none; /* Remove any max-height constraints */
flex: 1;
display: flex;
flex-direction: column;
}
/* Date popup sizing for native HTML inputs */
.pop-over[data-popup="editCardReceivedDatePopup"],
.pop-over[data-popup="editCardStartDatePopup"],
.pop-over[data-popup="editCardDueDatePopup"],
.pop-over[data-popup="editCardEndDatePopup"],
.pop-over[data-popup*="Date"] {
width: min(400px, 90vw) !important; /* Smaller width for native inputs */
min-width: 350px !important;
max-height: 80vh !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .content-wrapper,
.pop-over[data-popup="editCardStartDatePopup"] .content-wrapper,
.pop-over[data-popup="editCardDueDatePopup"] .content-wrapper,
.pop-over[data-popup="editCardEndDatePopup"] .content-wrapper,
.pop-over[data-popup*="Date"] .content-wrapper {
max-height: 60vh !important;
overflow-y: auto !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .content-container,
.pop-over[data-popup="editCardStartDatePopup"] .content-container,
.pop-over[data-popup="editCardDueDatePopup"] .content-container,
.pop-over[data-popup="editCardEndDatePopup"] .content-container,
.pop-over[data-popup*="Date"] .content-container {
max-height: 60vh !important;
}
/* Native HTML input styling */
.pop-over[data-popup*="Date"] .datepicker-container {
width: 100% !important;
padding: 15px !important;
}
.pop-over[data-popup*="Date"] .datepicker-container .fields {
display: flex !important;
gap: 15px !important;
margin-bottom: 15px !important;
}
.pop-over[data-popup*="Date"] .datepicker-container .fields .left,
.pop-over[data-popup*="Date"] .datepicker-container .fields .right {
flex: 1 !important;
width: auto !important;
}
.pop-over[data-popup*="Date"] .datepicker-container label {
display: block !important;
margin-bottom: 5px !important;
font-weight: bold !important;
}
.pop-over[data-popup*="Date"] .datepicker-container input[type="date"],
.pop-over[data-popup*="Date"] .datepicker-container input[type="time"] {
width: 100% !important;
padding: 8px !important;
border: 1px solid #ccc !important;
border-radius: 4px !important;
font-size: 14px !important;
box-sizing: border-box !important;
}
.pop-over[data-popup*="Date"] .datepicker-container input[type="date"]:focus,
.pop-over[data-popup*="Date"] .datepicker-container input[type="time"]:focus {
outline: none !important;
border-color: #007cba !important;
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2) !important;
}
/* Ensure date popup buttons stay within popup boundaries */
.pop-over[data-popup="editCardReceivedDatePopup"] .content,
.pop-over[data-popup="editCardStartDatePopup"] .content,
.pop-over[data-popup="editCardDueDatePopup"] .content,
.pop-over[data-popup="editCardEndDatePopup"] .content,
.pop-over[data-popup*="Date"] .content {
max-height: 60vh !important; /* Leave space for buttons */
overflow-y: auto !important;
padding-bottom: 100px !important; /* More space for buttons */
margin-bottom: 0 !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .datepicker-container,
.pop-over[data-popup="editCardStartDatePopup"] .datepicker-container,
.pop-over[data-popup="editCardDueDatePopup"] .datepicker-container,
.pop-over[data-popup="editCardEndDatePopup"] .datepicker-container,
.pop-over[data-popup*="Date"] .datepicker-container {
max-height: 50vh !important; /* Limit calendar height */
overflow-y: auto !important;
margin-bottom: 20px !important; /* Space before buttons */
}
/* Ensure buttons are properly positioned */
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date,
.pop-over[data-popup="editCardStartDatePopup"] .edit-date,
.pop-over[data-popup="editCardDueDatePopup"] .edit-date,
.pop-over[data-popup="editCardEndDatePopup"] .edit-date,
.pop-over[data-popup*="Date"] .edit-date {
display: flex !important;
flex-direction: column !important;
height: 100% !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date .fields,
.pop-over[data-popup="editCardStartDatePopup"] .edit-date .fields,
.pop-over[data-popup="editCardDueDatePopup"] .edit-date .fields,
.pop-over[data-popup="editCardEndDatePopup"] .edit-date .fields,
.pop-over[data-popup*="Date"] .edit-date .fields {
flex-shrink: 0 !important;
margin-bottom: 15px !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date .js-datepicker,
.pop-over[data-popup="editCardStartDatePopup"] .edit-date .js-datepicker,
.pop-over[data-popup="editCardDueDatePopup"] .edit-date .js-datepicker,
.pop-over[data-popup="editCardEndDatePopup"] .edit-date .js-datepicker,
.pop-over[data-popup*="Date"] .edit-date .js-datepicker {
flex: 1 !important;
overflow-y: auto !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date button,
.pop-over[data-popup="editCardStartDatePopup"] .edit-date button,
.pop-over[data-popup="editCardDueDatePopup"] .edit-date button,
.pop-over[data-popup="editCardEndDatePopup"] .edit-date button,
.pop-over[data-popup*="Date"] .edit-date button {
flex-shrink: 0 !important;
margin-top: 15px !important;
position: relative !important;
z-index: 10 !important;
}
.pop-over .content-container .content {
width: min(280px, 37vw);
/* Match wider popover, leave padding */
width: 100%;
padding: 0 1.3vw 1.3vh;
float: left;
box-sizing: border-box;
/* Ensure content is not shifted left */
margin-left: 0 !important;
transform: none !important;
}
/* Utility: remove left gutter inside specific popups */
.pop-over .content .flush-left {
margin-left: 0;
padding-left: 0;
width: 100%;
}
/* Swimlane popups: remove left gutter, align content fully left */
.pop-over .content form.swimlane-color-popup,
.pop-over .content .swimlane-height-popup {
margin-left: 0;
padding-left: 0;
width: 100%;
}
/* Color selection popups: ensure proper alignment */
.pop-over .content form.swimlane-color-popup .palette-colors,
.pop-over .content form.edit-label .palette-colors,
.pop-over .content form.create-label .palette-colors {
margin-left: 0;
padding-left: 0;
width: 100%;
}
/* Color palette items: ensure proper positioning */
.pop-over .content .palette-colors .palette-color {
margin-left: 0;
margin-right: 2px;
margin-bottom: 2px;
}
/* Global fix for all popup content to prevent left shifting */
.pop-over .content * {
margin-left: 0 !important;
transform: none !important;
}
/* Override any potential left shifting for specific elements */
.pop-over .content form,
.pop-over .content .palette-colors,
.pop-over .content .pop-over-list,
.pop-over .content .flush-left {
margin-left: 0 !important;
padding-left: 0 !important;
transform: none !important;
}
/* Fix popup depth containers that cause left shifting */
.pop-over .popup-container-depth-1,
.pop-over .popup-container-depth-2,
.pop-over .popup-container-depth-3,
.pop-over .popup-container-depth-4,
.pop-over .popup-container-depth-5,
.pop-over .popup-container-depth-6 {
transform: none !important;
margin-left: 0 !important;
padding-left: 0 !important;
}
/* Ensure buttons don’t reserve left space; align to flow */
.pop-over .content form.swimlane-color-popup .primary.confirm,
.pop-over .content form.swimlane-color-popup .negate.wide.right,
.pop-over .content .swimlane-height-popup .primary.confirm,
.pop-over .content .swimlane-height-popup .negate.wide.right {
float: none;
margin-left: 0;
}
.pop-over .content-container .content.no-height {
height: 2.5vh;
height: 0;
overflow: hidden;
padding: 0;
margin: 0;
visibility: hidden;
}
.pop-over .quiet {
/* padding: 6px 6px 4px;*/
@@ -104,7 +404,7 @@
.pop-over .at-form .at-error,
.pop-over .at-form .at-result {
padding: 8px 12px;
margin: -8px -10px 10px;
margin: 0 0 10px 0;
}
.pop-over .at-form .at-error {
background: #ef9a9a;
@@ -148,7 +448,7 @@
font-weight: 700;
padding: 1.5px 10px;
position: relative;
margin: 0 -10px;
margin: 0;
text-decoration: none;
overflow: hidden;
line-height: 33px;
@@ -307,12 +607,12 @@
margin: 48px 0px 0px 0px;
}
.pop-over .content-container {
width: 1000%;
width: 100%;
height: 100%;
max-height: 100%;
}
.pop-over .content-container .content {
width: calc(10% - 20px);
width: calc(100% - 20px);
height: calc(100% - 20px);
padding: 10px;
}
@@ -334,21 +634,21 @@
margin: 0px 0px;
}
.pop-over .popup-container-depth-1 {
transform: translateX(-10%);
transform: none !important;
}
.pop-over .popup-container-depth-2 {
transform: translateX(-20%);
transform: none !important;
}
.pop-over .popup-container-depth-3 {
transform: translateX(-30%);
transform: none !important;
}
.pop-over .popup-container-depth-4 {
transform: translateX(-40%);
transform: none !important;
}
.pop-over .popup-container-depth-5 {
transform: translateX(-50%);
transform: none !important;
}
.pop-over .popup-container-depth-6 {
transform: translateX(-60%);
transform: none !important;
}
}
+3 -3
View File
@@ -2,13 +2,13 @@
class="{{#unless title}}miniprofile{{/unless}}"
class=currentBoard.colorClass
class="{{#unless title}}no-title{{/unless}}"
style="left:{{offset.left}}px; top:{{offset.top}}px;")
style="left:{{offset.left}}px; top:{{offset.top}}px;{{#if offset.maxHeight}} max-height:{{offset.maxHeight}}px;{{/if}}")
.header
a.back-btn.js-back-view(class="{{#unless hasPopupParent}}is-hidden{{/unless}}")
i.fa.fa-chevron-left
| ◀️
span.header-title= title
a.close-btn.js-close-pop-over
i.fa.fa-times-thin
| ❌
.content-wrapper
//-
We display the all stack of popup content next to each other and move
+372
View File
@@ -0,0 +1,372 @@
/* Migration Progress Styles */
.migration-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
z-index: 10000;
display: none;
align-items: center;
justify-content: center;
overflow-y: auto;
}
.migration-overlay.active {
display: flex;
}
.migration-modal {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
max-width: 800px;
width: 95%;
max-height: 90vh;
overflow: hidden;
animation: slideInScale 0.4s ease-out;
margin: 20px;
}
@keyframes slideInScale {
from {
opacity: 0;
transform: translateY(-30px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.migration-header {
padding: 24px 32px 20px;
border-bottom: 2px solid #e0e0e0;
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.migration-header h3 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.migration-header h3 i {
margin-right: 12px;
color: #FFD700;
}
.migration-header p {
margin: 0;
font-size: 16px;
opacity: 0.9;
}
.migration-content {
padding: 24px 32px;
max-height: 60vh;
overflow-y: auto;
}
.migration-overview {
margin-bottom: 32px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.overall-progress {
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;
}
.estimated-time {
text-align: center;
color: #666;
font-size: 14px;
background-color: #fff3cd;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #ffeaa7;
}
.estimated-time i {
margin-right: 6px;
color: #f39c12;
}
.migration-steps {
margin-bottom: 24px;
}
.migration-steps h4 {
margin: 0 0 16px 0;
color: #333;
font-size: 18px;
font-weight: 600;
}
.steps-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.migration-step {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
transition: all 0.3s ease;
}
.migration-step:last-child {
border-bottom: none;
}
.migration-step.completed {
background-color: #d4edda;
border-left: 4px solid #28a745;
}
.migration-step.current {
background-color: #cce7ff;
border-left: 4px solid #667eea;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(102, 126, 234, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
}
}
.step-header {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.step-icon {
margin-right: 12px;
font-size: 18px;
width: 24px;
text-align: center;
}
.step-icon i.fa-check-circle {
color: #28a745;
}
.step-icon i.fa-cog.fa-spin {
color: #667eea;
}
.step-icon i.fa-circle-o {
color: #ccc;
}
.step-info {
flex: 1;
}
.step-name {
font-weight: 600;
color: #333;
font-size: 14px;
margin-bottom: 2px;
}
.step-description {
color: #666;
font-size: 12px;
line-height: 1.3;
}
.step-progress {
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;
}
.migration-status {
text-align: center;
color: #333;
font-size: 16px;
background-color: #e3f2fd;
padding: 12px 16px;
border-radius: 6px;
border: 1px solid #bbdefb;
margin-bottom: 16px;
}
.migration-status i {
margin-right: 8px;
color: #2196f3;
}
.migration-footer {
padding: 16px 32px 24px;
border-top: 1px solid #e0e0e0;
background-color: #f8f9fa;
}
.migration-info {
text-align: center;
color: #666;
font-size: 13px;
line-height: 1.4;
margin-bottom: 8px;
}
.migration-info i {
margin-right: 6px;
color: #667eea;
}
.migration-warning {
text-align: center;
color: #856404;
font-size: 12px;
line-height: 1.3;
background-color: #fff3cd;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #ffeaa7;
}
.migration-warning i {
margin-right: 6px;
color: #f39c12;
}
/* Responsive design */
@media (max-width: 768px) {
.migration-modal {
width: 98%;
margin: 10px;
}
.migration-header,
.migration-content,
.migration-footer {
padding-left: 16px;
padding-right: 16px;
}
.migration-header h3 {
font-size: 20px;
}
.step-header {
flex-direction: column;
align-items: flex-start;
}
.step-progress {
text-align: left;
margin-top: 8px;
}
.steps-list {
max-height: 200px;
}
}
+63
View File
@@ -0,0 +1,63 @@
template(name="migrationProgress")
.migration-overlay(class="{{#if isMigrating}}active{{/if}}")
.migration-modal
.migration-header
h3
| 🗄️
| {{_ 'database-migration'}}
p {{_ 'database-migration-description'}}
.migration-content
.migration-overview
.overall-progress
.progress-bar
.progress-fill(style="width: {{migrationProgress}}%")
.progress-text {{migrationProgress}}%
.progress-label {{_ 'overall-progress'}}
.current-step
| ⚙️
| {{migrationCurrentStep}}
.estimated-time(style="{{#unless migrationEstimatedTime}}display: none;{{/unless}}")
| ⏰
| {{_ 'estimated-time-remaining'}}: {{migrationEstimatedTime}}
.migration-steps
h4 {{_ 'migration-steps'}}
.steps-list
each migrationSteps
.migration-step(class="{{#if completed}}completed{{/if}}" class="{{#if isCurrentStep}}current{{/if}}")
.step-header
.step-icon
if completed
| ✅
else if isCurrentStep
| ⚙️
else
| ⭕
.step-info
.step-name {{name}}
.step-description {{description}}
.step-progress
if completed
.progress-text 100%
else if isCurrentStep
.progress-text {{progress}}%
else
.progress-text 0%
if isCurrentStep
.step-progress-bar
.progress-fill(style="width: {{progress}}%")
.migration-status
| ℹ️
| {{migrationStatus}}
.migration-footer
.migration-info
| 💡
| {{_ 'migration-info-text'}}
.migration-warning
| ⚠️
| {{_ 'migration-warning-text'}}
+54
View File
@@ -0,0 +1,54 @@
import { Template } from 'meteor/templating';
import {
migrationManager,
isMigrating,
migrationProgress,
migrationStatus,
migrationCurrentStep,
migrationEstimatedTime,
migrationSteps
} from '/client/lib/migrationManager';
Template.migrationProgress.helpers({
isMigrating() {
return isMigrating.get();
},
migrationProgress() {
return migrationProgress.get();
},
migrationStatus() {
return migrationStatus.get();
},
migrationCurrentStep() {
return migrationCurrentStep.get();
},
migrationEstimatedTime() {
return migrationEstimatedTime.get();
},
migrationSteps() {
const steps = migrationSteps.get();
const currentStep = migrationCurrentStep.get();
return steps.map(step => ({
...step,
isCurrentStep: step.name === currentStep
}));
}
});
Template.migrationProgress.onCreated(function() {
// Subscribe to migration state changes
this.autorun(() => {
isMigrating.get();
migrationProgress.get();
migrationStatus.get();
migrationCurrentStep.get();
migrationEstimatedTime.get();
migrationSteps.get();
});
});
@@ -1,5 +1,6 @@
template(name='notifications')
#notifications.board-header-btns.right
a.notifications-drawer-toggle.fa.fa-bell(class="{{#if $gt unreadNotifications 0}}alert{{/if}}" title="{{_ 'notifications'}}")
a.notifications-drawer-toggle(class="{{#if $gt unreadNotifications 0}}alert{{/if}}" title="{{_ 'notifications'}}")
| 🔔
if $.Session.get 'showNotificationsDrawer'
+notificationsDrawer(unreadNotifications=unreadNotifications)
@@ -10,7 +10,7 @@ template(name="boardActions")
div.trigger-text
| {{_'r-its-list'}}
div.trigger-button.js-add-gen-move-action.js-goto-rules
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -38,7 +38,7 @@ template(name="boardActions")
div.trigger-dropdown
input(id="swimlaneName",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-spec-move-action.js-goto-rules
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -49,7 +49,7 @@ template(name="boardActions")
div.trigger-text
| {{_'r-card'}}
div.trigger-button.js-add-arch-action.js-goto-rules
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -58,7 +58,7 @@ template(name="boardActions")
div.trigger-dropdown
input(id="swimlane-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-swimlane-action.js-goto-rules
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -75,7 +75,7 @@ template(name="boardActions")
div.trigger-dropdown
input(id="swimlane-name2",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-create-card-action.js-goto-rules
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -99,7 +99,7 @@ template(name="boardActions")
div.trigger-dropdown
input(id="swimlaneName-link",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-link-card-action.js-goto-rules
i.fa.fa-plus
| ➕
@@ -16,7 +16,7 @@ template(name="cardActions")
div.trigger-text
| {{_'r-to-current-datetime'}}
div.trigger-button.js-set-date-action.js-goto-rules
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -30,7 +30,7 @@ template(name="cardActions")
option(value="endAt") {{_'r-df-end-at'}}
option(value="receivedAt") {{_'r-df-received-at'}}
div.trigger-button.js-remove-datevalue-action.js-goto-rules
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -46,7 +46,7 @@ template(name="cardActions")
option(value="#{_id}")
= name
div.trigger-button.js-add-label-action.js-goto-rules
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -59,14 +59,14 @@ template(name="cardActions")
div.trigger-dropdown
input(id="member-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-member-action.js-goto-rules
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
div.trigger-text
| {{_'r-remove-all'}}
div.trigger-button.js-add-removeall-action.js-goto-rules
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -77,12 +77,12 @@ template(name="cardActions")
class="card-details-{{cardColorButton}}")
| {{_ cardColorButtonText }}
div.trigger-button.js-set-color-action.js-goto-rules
i.fa.fa-plus
| ➕
template(name="setCardActionsColorPopup")
form.edit-label
.palette-colors: each colors
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color)
i.fa.fa-check
| ✅
button.primary.confirm.js-submit {{_ 'save'}}
@@ -10,7 +10,7 @@ template(name="checklistActions")
div.trigger-dropdown
input(id="checklist-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-checklist-action.js-goto-rules
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -23,7 +23,7 @@ template(name="checklistActions")
div.trigger-dropdown
input(id="checklist-name2",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-checkall-action.js-goto-rules
i.fa.fa-plus
| ➕
div.trigger-item
@@ -41,7 +41,7 @@ template(name="checklistActions")
div.trigger-dropdown
input(id="checklist-name3",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-check-item-action.js-goto-rules
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -54,7 +54,7 @@ template(name="checklistActions")
div.trigger-dropdown
input(id="checklist-items",type=text,placeholder="{{_'r-items-list'}}")
div.trigger-button.js-add-checklist-items-action.js-goto-rules
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -8,4 +8,4 @@ template(name="mailActions")
input(id="email-subject",type=text,placeholder="{{_'r-subject'}}")
textarea(id="email-msg")
div.trigger-button.trigger-button-email.js-mail-action.js-goto-rules
i.fa.fa-plus
| ➕
+2 -2
View File
@@ -1,7 +1,7 @@
template(name="ruleDetails")
.rules
h2
i.fa.fa-magic
| ✨
| {{_ 'r-rule-details' }}
.triggers-content
.triggers-body
@@ -20,5 +20,5 @@ template(name="ruleDetails")
= action
div.rules-back
button.js-goback
i.fa.fa-chevron-left
| ◀️
| {{_ 'back'}}
+6 -6
View File
@@ -1,19 +1,19 @@
template(name="rulesActions")
h2
i.fa.fa-magic
| ✨
| {{_ 'r-rule' }} "#{data.ruleName.get}" - {{_ 'r-add-action'}}
.triggers-content
.triggers-body
.triggers-side-menu
ul
li.active.js-set-board-actions
i.fa.fa-columns
| 📊
li.js-set-card-actions
i.fa.fa-sticky-note
| 📝
li.js-set-checklist-actions
i.fa.fa-check
| ✅
li.js-set-mail-actions
i.fa.fa-at
| @
.triggers-main-body
if ($eq currentActions.get 'board')
+boardActions(ruleName=data.ruleName triggerVar=data.triggerVar)
@@ -25,5 +25,5 @@ template(name="rulesActions")
+mailActions(ruleName=data.ruleName triggerVar=data.triggerVar)
div.rules-back
button.js-goback
i.fa.fa-chevron-left
| ◀️
| {{_ 'back'}}
+6 -6
View File
@@ -1,7 +1,7 @@
template(name="rulesList")
.rules
h2
i.fa.fa-magic
| ✨
| {{_ 'r-board-rules' }}
ul.rules-list
@@ -11,27 +11,27 @@ template(name="rulesList")
= title
div.rules-btns-group
button.js-goto-details
i.fa.fa-eye
| 👁️
| {{_ 'r-view-rule'}}
if currentUser.isAdmin
button.js-delete-rule
i.fa.fa-trash-o
| 🗑️
| {{_ 'r-delete-rule'}}
else if currentUser.isBoardAdmin
button.js-delete-rule
i.fa.fa-trash-o
| 🗑️
| {{_ 'r-delete-rule'}}
else
li.no-items-message {{_ 'r-no-rules' }}
if currentUser.isAdmin
div.rules-add
button.js-goto-trigger
i.fa.fa-plus
| ➕
| {{_ 'r-add-rule'}}
input(type=text,placeholder="{{_ 'r-new-rule-name' }}",id="ruleTitle")
else if currentUser.isBoardAdmin
div.rules-add
button.js-goto-trigger
i.fa.fa-plus
| ➕
| {{_ 'r-add-rule'}}
input(type=text,placeholder="{{_ 'r-new-rule-name' }}",id="ruleTitle")
+5 -5
View File
@@ -1,17 +1,17 @@
template(name="rulesTriggers")
h2
i.fa.fa-magic
| ✨
| {{_ 'r-rule' }} "#{data.ruleName.get}" - {{_ 'r-add-trigger'}}
.triggers-content
.triggers-body
.triggers-side-menu
ul
li.active.js-set-board-triggers
i.fa.fa-columns
| 📊
li.js-set-card-triggers
i.fa.fa-sticky-note
| 📝
li.js-set-checklist-triggers
i.fa.fa-check
| ✅
.triggers-main-body
if showBoardTrigger.get
+boardTriggers
@@ -21,5 +21,5 @@ template(name="rulesTriggers")
+checklistTriggers
div.rules-back
button.js-goback
i.fa.fa-chevron-left
| ◀️
| {{_ 'back'}}
@@ -4,7 +4,7 @@ template(name="boardTriggers")
div.trigger-text
| {{_'r-when-a-card'}}
div.trigger-inline-button.js-open-card-title-popup
i.fa.fa-filter
| 🔍
div.trigger-text
| {{_'r-is'}}
div.trigger-text
@@ -18,39 +18,39 @@ template(name="boardTriggers")
div.trigger-dropdown
input(id="create-swimlane-name",type=text,placeholder="{{_'r-swimlane-name'}}")
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-create-trigger.js-goto-action
i.fa.fa-plus
| ➕
div.trigger-item#trigger-three
div.trigger-content
div.trigger-text
| {{_'r-when-a-card'}}
div.trigger-inline-button.js-open-card-title-popup
i.fa.fa-filter
| 🔍
div.trigger-text
| {{_'r-is-moved'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-moved-trigger.js-goto-action
i.fa.fa-plus
| ➕
div.trigger-item#trigger-four
div.trigger-content
div.trigger-text
| {{_'r-when-a-card'}}
div.trigger-inline-button.js-open-card-title-popup
i.fa.fa-filter
| 🔍
div.trigger-text
| {{_'r-is'}}
div.trigger-dropdown
@@ -66,21 +66,21 @@ template(name="boardTriggers")
div.trigger-dropdown
input(id="create-swimlane-name-2",type=text,placeholder="{{_'r-swimlane-name'}}")
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-moved-trigger.js-goto-action
i.fa.fa-plus
| ➕
div.trigger-item#trigger-five
div.trigger-content
div.trigger-text
| {{_'r-when-a-card'}}
div.trigger-inline-button.js-open-card-title-popup
i.fa.fa-filter
| 🔍
div.trigger-text
| {{_'r-is'}}
div.trigger-dropdown
@@ -88,14 +88,14 @@ template(name="boardTriggers")
option(value="archived") {{_'r-archived'}}
option(value="unarchived") {{_'r-unarchived'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-arch-trigger.js-goto-action
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -10,14 +10,14 @@ template(name="cardTriggers")
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-label-trigger.js-goto-action
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -37,14 +37,14 @@ template(name="cardTriggers")
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-spec-label-trigger.js-goto-action
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -57,14 +57,14 @@ template(name="cardTriggers")
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-member-trigger.js-goto-action
i.fa.fa-plus
| ➕
div.trigger-item
@@ -82,14 +82,14 @@ template(name="cardTriggers")
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-spec-member-trigger.js-goto-action
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -104,11 +104,11 @@ template(name="cardTriggers")
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-attachment-trigger.js-goto-action
i.fa.fa-plus
| ➕
@@ -10,14 +10,14 @@ template(name="checklistTriggers")
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-check-trigger.js-goto-action
i.fa.fa-plus
| ➕
div.trigger-item
@@ -35,14 +35,14 @@ template(name="checklistTriggers")
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-spec-check-trigger.js-goto-action
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -53,14 +53,14 @@ template(name="checklistTriggers")
option(value="completed") {{_'r-completed'}}
option(value="uncompleted") {{_'r-made-incomplete'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-comp-trigger.js-goto-action
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -75,14 +75,14 @@ template(name="checklistTriggers")
option(value="completed") {{_'r-completed'}}
option(value="uncompleted") {{_'r-made-incomplete'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-spec-comp-trigger.js-goto-action
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -93,14 +93,14 @@ template(name="checklistTriggers")
option(value="checked") {{_'r-checked'}}
option(value="unchecked") {{_'r-unchecked'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-gen-check-item-trigger.js-goto-action
i.fa.fa-plus
| ➕
div.trigger-item
div.trigger-content
@@ -115,11 +115,11 @@ template(name="checklistTriggers")
option(value="checked") {{_'r-checked'}}
option(value="unchecked") {{_'r-unchecked'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
| 👤
div.user-details.hide-element
div.trigger-text
| {{_'r-by'}}
div.trigger-dropdown
input(class="user-name",type=text,placeholder="{{_'username'}}")
div.trigger-button.js-add-spec-check-item-trigger.js-goto-action
i.fa.fa-plus
| ➕
+5 -5
View File
@@ -8,27 +8,27 @@ template(name="adminReports")
ul
li
a.js-report-broken(data-id="report-broken")
i.fa.fa-chain-broken
| 🔗
| {{_ 'broken-cards'}}
li
a.js-report-files(data-id="report-files")
i.fa.fa-paperclip
| 📎
| {{_ 'filesReportTitle'}}
li
a.js-report-rules(data-id="report-rules")
i.fa.fa-magic
| ✨
| {{_ 'rulesReportTitle'}}
li
a.js-report-boards(data-id="report-boards")
i.fa.fa-magic
| ✨
| {{_ 'boardsReportTitle'}}
li
a.js-report-cards(data-id="report-cards")
i.fa.fa-magic
| ✨
| {{_ 'cardsReportTitle'}}
.main-body
@@ -0,0 +1,241 @@
template(name="attachmentSettings")
ul#attachment-setting.setting-detail
li
h3 {{_ 'attachment-storage-configuration'}}
.form-group
label {{_ 'writable-path'}}
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
small.form-text.text-muted {{_ 'filesystem-path-description'}}
.form-group
label {{_ 'attachments-path'}}
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
small.form-text.text-muted {{_ 'attachments-path-description'}}
.form-group
label {{_ 'avatars-path'}}
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
small.form-text.text-muted {{_ 'avatars-path-description'}}
li
h3 {{_ 'mongodb-gridfs-storage'}}
.form-group
label {{_ 'gridfs-enabled'}}
input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
li
h3 {{_ 's3-minio-storage'}}
.form-group
label {{_ 's3-enabled'}}
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
small.form-text.text-muted {{_ 's3-enabled-description'}}
.form-group
label {{_ 's3-endpoint'}}
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
small.form-text.text-muted {{_ 's3-endpoint-description'}}
.form-group
label {{_ 's3-bucket'}}
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
small.form-text.text-muted {{_ 's3-bucket-description'}}
.form-group
label {{_ 's3-region'}}
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
small.form-text.text-muted {{_ 's3-region-description'}}
.form-group
label {{_ 's3-access-key'}}
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
small.form-text.text-muted {{_ 's3-access-key-description'}}
.form-group
label {{_ 's3-secret-key'}}
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
small.form-text.text-muted {{_ 's3-secret-key-description'}}
.form-group
label {{_ 's3-ssl-enabled'}}
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
.form-group
label {{_ 's3-port'}}
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
small.form-text.text-muted {{_ 's3-port-description'}}
.form-group
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
template(name="storageSettings")
.storage-settings
h3 {{_ 'attachment-storage-configuration'}}
.storage-config-section
h4 {{_ 'filesystem-storage'}}
.form-group
label {{_ 'writable-path'}}
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
small.form-text.text-muted {{_ 'filesystem-path-description'}}
.form-group
label {{_ 'attachments-path'}}
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
small.form-text.text-muted {{_ 'attachments-path-description'}}
.form-group
label {{_ 'avatars-path'}}
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
small.form-text.text-muted {{_ 'avatars-path-description'}}
.storage-config-section
h4 {{_ 'mongodb-gridfs-storage'}}
.form-group
label {{_ 'gridfs-enabled'}}
input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
.storage-config-section
h4 {{_ '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'}}
.storage-actions
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
template(name="attachmentMigration")
.attachment-migration
h3 {{_ 'attachment-migration'}}
.migration-controls
.form-group
label {{_ 'migration-batch-size'}}
input.wekan-form-control#migration-batch-size(type="number" value="{{migrationBatchSize}}" min="1" max="100")
small.form-text.text-muted {{_ 'migration-batch-size-description'}}
.form-group
label {{_ 'migration-delay-ms'}}
input.wekan-form-control#migration-delay-ms(type="number" value="{{migrationDelayMs}}" min="100" max="10000")
small.form-text.text-muted {{_ 'migration-delay-ms-description'}}
.form-group
label {{_ 'migration-cpu-threshold'}}
input.wekan-form-control#migration-cpu-threshold(type="number" value="{{migrationCpuThreshold}}" min="10" max="90")
small.form-text.text-muted {{_ 'migration-cpu-threshold-description'}}
.migration-actions
.migration-buttons
button.js-migrate-all-to-filesystem.btn.btn-primary {{_ 'migrate-all-to-filesystem'}}
button.js-migrate-all-to-gridfs.btn.btn-primary {{_ 'migrate-all-to-gridfs'}}
button.js-migrate-all-to-s3.btn.btn-primary {{_ 'migrate-all-to-s3'}}
.migration-controls
button.js-pause-migration.btn.btn-warning {{_ 'pause-migration'}}
button.js-resume-migration.btn.btn-success {{_ 'resume-migration'}}
button.js-stop-migration.btn.btn-danger {{_ 'stop-migration'}}
.migration-progress
h4 {{_ 'migration-progress'}}
.progress
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
| {{migrationProgress}}%
.migration-stats
.stat-item
span.label {{_ 'total-attachments'}}:
span.value {{totalAttachments}}
.stat-item
span.label {{_ 'migrated-attachments'}}:
span.value {{migratedAttachments}}
.stat-item
span.label {{_ 'remaining-attachments'}}:
span.value {{remainingAttachments}}
.stat-item
span.label {{_ 'migration-status'}}:
span.value {{migrationStatus}}
.migration-log
h4 {{_ 'migration-log'}}
.log-container
pre#migration-log-content {{migrationLog}}
template(name="attachmentMonitoring")
.attachment-monitoring
h3 {{_ 'attachment-monitoring'}}
.monitoring-stats
.stats-grid
.stat-card
h5 {{_ 'total-attachments'}}
.stat-value {{totalAttachments}}
.stat-card
h5 {{_ 'filesystem-attachments'}}
.stat-value {{filesystemAttachments}}
.stat-card
h5 {{_ 'gridfs-attachments'}}
.stat-value {{gridfsAttachments}}
.stat-card
h5 {{_ 's3-attachments'}}
.stat-value {{s3Attachments}}
.stat-card
h5 {{_ 'total-size'}}
.stat-value {{totalSize}}
.stat-card
h5 {{_ 'filesystem-size'}}
.stat-value {{filesystemSize}}
.stat-card
h5 {{_ 'gridfs-size'}}
.stat-value {{gridfsSize}}
.stat-card
h5 {{_ 's3-size'}}
.stat-value {{s3Size}}
.monitoring-charts
h4 {{_ 'storage-distribution'}}
.chart-container
canvas#storage-distribution-chart
.monitoring-actions
button.js-refresh-monitoring.btn.btn-secondary {{_ 'refresh-monitoring'}}
button.js-export-monitoring.btn.btn-primary {{_ 'export-monitoring'}}
+4 -4
View File
@@ -8,7 +8,7 @@ template(name="attachments")
ul
li
a.js-move-attachments(data-id="move-attachments")
i.fa.fa-arrow-right
| ➡️
| {{_ 'attachment-move'}}
.main-body
@@ -80,17 +80,17 @@ template(name="moveAttachment")
td
if $neq version.storageName "fs"
button.js-move-storage-fs
i.fa.fa-arrow-right
| ➡️
| {{_ 'attachment-move-storage-fs'}}
if $neq version.storageName "gridfs"
if version.storageName
button.js-move-storage-gridfs
i.fa.fa-arrow-right
| ➡️
| {{_ 'attachment-move-storage-gridfs'}}
if $neq version.storageName "s3"
if version.storageName
button.js-move-storage-s3
i.fa.fa-arrow-right
| ➡️
| {{_ 'attachment-move-storage-s3'}}
+864
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%;
}
}
@@ -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'}}
@@ -5,14 +5,14 @@ template(name='information')
else
.content-title
span
i.fa.fa-info-circle
| ℹ️
| {{_ 'info'}}
.content-body
.side-menu
ul
li.active
a.js-setting-menu(data-id="information-display")
i.fa.fa-info-circle
| ℹ️
| {{_ 'info'}}
.main-body
+statistics
+17 -5
View File
@@ -1,11 +1,16 @@
.main-body {
overflow: scroll;
}
/* Scrollbar styles are now handled by settingBody.css for all admin pages */
table {
color: #000;
min-width: 1200px !important;
width: max-content !important;
table-layout: auto !important;
}
/* Force People/Organizations/Teams tables to always trigger horizontal scrolling */
table td,
table th {
white-space: nowrap;
min-width: 120px;
border: 1px solid #d2d0d0;
text-align: left;
padding: 8px;
@@ -66,6 +71,13 @@ table tr:nth-child(even) {
left: 0 !important;
display: block !important;
}
/* Make checkbox column fit content */
table th:first-child,
table td:first-child {
width: auto;
min-width: auto;
}
#divAddOrRemoveTeam {
background: #008000;
display: none;
@@ -136,7 +148,7 @@ table tr:nth-child(even) {
}
.account-active-status {
width: 20px;
width: auto;
text-align: center;
}
@@ -165,7 +177,7 @@ table tr:nth-child(even) {
}
.account-status {
width: 20px;
width: auto;
text-align: center;
}
+37 -85
View File
@@ -9,34 +9,34 @@ template(name="people")
+spinner
else if orgSetting.get
span
i.fa.fa-sitemap
| 🌐
unless isMiniScreen
| {{_ 'organizations'}}
input#searchOrgInput(placeholder="{{_ 'search'}}")
button#searchOrgButton
i.fa.fa-search
| 🔍
| {{_ 'search'}}
.ext-box-right
span {{#unless isMiniScreen}}{{_ 'org-number'}}{{/unless}} #{orgNumber}
else if teamSetting.get
span
i.fa.fa-users
| 👥
unless isMiniScreen
| {{_ 'teams'}}
input#searchTeamInput(placeholder="{{_ 'search'}}")
button#searchTeamButton
i.fa.fa-search
| 🔍
| {{_ 'search'}}
.ext-box-right
span {{#unless isMiniScreen}}{{_ 'team-number'}}{{/unless}} #{teamNumber}
else if peopleSetting.get
span
i.fa.fa-user
| 👤
unless isMiniScreen
| {{_ 'people'}}
input#searchInput(placeholder="{{_ 'search'}}")
button#searchButton
i.fa.fa-search
| 🔍
| {{_ 'search'}}
.divLockedUsersFilter
.flex-container
@@ -46,18 +46,19 @@ template(name="people")
option(value="locked") {{_ 'admin-people-filter-locked'}}
option(value="active") {{_ 'admin-people-filter-active'}}
option(value="inactive") {{_ 'admin-people-filter-inactive'}}
option(value="admin") Admin
button#unlockAllUsers.unlock-all-btn
i.fa.fa-unlock
| 🔓
| {{_ 'accounts-lockout-unlock-all'}}
.ext-box-right
span {{#unless isMiniScreen}}{{_ 'people-number'}}{{/unless}} #{peopleNumber}
.divAddOrRemoveTeam#divAddOrRemoveTeam
button#addOrRemoveTeam
i.fa.fa-edit
| ✏️
| {{_ 'add'}} / {{_ 'delete'}} {{_ 'teams'}}
else if lockedUsersSetting.get
span
i.fa.fa-lock.text-red
span.text-red 🔒
unless isMiniScreen
| {{_ 'accounts-lockout-locked-users'}}
@@ -66,19 +67,19 @@ template(name="people")
ul
li.active
a.js-org-menu(data-id="org-setting")
i.fa.fa-sitemap
| 🌐
| {{_ 'organizations'}}
li
a.js-team-menu(data-id="team-setting")
i.fa.fa-users
| 👥
| {{_ 'teams'}}
li
a.js-people-menu(data-id="people-setting")
i.fa.fa-user
| 👤
| {{_ 'people'}}
li
a.js-locked-users-menu(data-id="locked-users-setting")
i.fa.fa-lock.text-red
span.text-red 🔒
| {{_ 'accounts-lockout-locked-users'}}
.main-body
if loading.get
@@ -100,7 +101,6 @@ template(name="orgGeneral")
th {{_ 'displayName'}}
th {{_ 'description'}}
th {{_ 'shortName'}}
th {{_ 'autoAddUsersWithDomainName'}}
th {{_ 'website'}}
th {{_ 'createdAt'}}
th {{_ 'active'}}
@@ -140,16 +140,9 @@ template(name="peopleGeneral")
th {{_ 'admin-people-active-status'}}
th {{_ 'username'}}
th {{_ 'fullname'}}
th {{_ 'initials'}}
th {{_ 'admin'}}
th {{_ 'email'}}
th {{_ 'verified'}}
th {{_ 'createdAt'}}
th {{_ 'active'}}
th {{_ 'authentication-method'}}
th {{_ 'import-usernames'}}
th {{_ 'organizations'}}
th {{_ 'teams'}}
th
+newUserRow
tbody
@@ -163,17 +156,17 @@ template(name="selectAllUser")
template(name="newOrgRow")
a.new-org
i.fa.fa-plus-square
| ➕
| {{_ 'new'}}
template(name="newTeamRow")
a.new-team
i.fa.fa-plus-square
| ➕
| {{_ 'new'}}
template(name="newUserRow")
a.new-user
i.fa.fa-plus-square
| ➕
| {{_ 'new'}}
template(name="orgRow")
@@ -190,10 +183,6 @@ template(name="orgRow")
td {{ orgData.orgShortName }}
else
td <s>{{ orgData.orgShortName }}</s>
if orgData.orgIsActive
td {{ orgData.orgAutoAddUsersWithDomainName }}
else
td <s>{{ orgData.orgAutoAddUsersWithDomainName }}</s>
if orgData.orgIsActive
td {{ orgData.orgWebsite }}
else
@@ -209,10 +198,10 @@ template(name="orgRow")
| {{_ 'no'}}
td
a.edit-org
i.fa.fa-edit
| ✏️
| {{_ 'edit'}}
a.more-settings-org
i.fa.fa-ellipsis-h
| ⋯
template(name="teamRow")
tr
@@ -243,10 +232,10 @@ template(name="teamRow")
| {{_ 'no'}}
td
a.edit-team
i.fa.fa-edit
| ✏️
| {{_ 'edit'}}
a.more-settings-team
i.fa.fa-ellipsis-h
| ⋯
template(name="peopleRow")
tr
@@ -258,14 +247,14 @@ template(name="peopleRow")
input.selectUserChkBox(type="checkbox", id="{{userData._id}}")
td.account-status
if isUserLocked
i.fa.fa-lock.text-red.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="true", title="{{_ 'accounts-lockout-click-to-unlock'}}")
span.text-red.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="true", title="{{_ 'accounts-lockout-click-to-unlock'}}") 🔒
else
i.fa.fa-unlock.text-green.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="false", title="{{_ 'accounts-lockout-user-unlocked'}}")
span.text-green.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="false", title="{{_ 'accounts-lockout-user-unlocked'}}") 🔓
td.account-active-status
if userData.loginDisabled
i.fa.fa-ban.text-red.js-toggle-active-status(data-user-id=userData._id, data-is-active="false", title="{{_ 'admin-people-user-inactive'}}")
span.text-red.js-toggle-active-status(data-user-id=userData._id, data-is-active="false", title="{{_ 'admin-people-user-inactive'}}") 🚫
else
i.fa.fa-check-circle.text-green.js-toggle-active-status(data-user-id=userData._id, data-is-active="true", title="{{_ 'admin-people-user-active'}}")
span.text-green.js-toggle-active-status(data-user-id=userData._id, data-is-active="true", title="{{_ 'admin-people-user-active'}}") ✅
if userData.loginDisabled
td.username <s>{{ userData.username }}</s>
else if isUserLocked
@@ -276,10 +265,6 @@ template(name="peopleRow")
td <s>{{ userData.profile.fullname }}</s>
else
td {{ userData.profile.fullname }}
if userData.loginDisabled
td <s>{{ userData.profile.initials }}</s>
else
td {{ userData.profile.initials }}
if userData.loginDisabled
td
if userData.isAdmin
@@ -296,49 +281,16 @@ template(name="peopleRow")
td <s>{{ userData.emails.[0].address }}</s>
else
td {{ userData.emails.[0].address }}
if userData.loginDisabled
td
if userData.emails.[0].verified
| <s>{{_ 'yes'}}</s>
else
| <s>{{_ 'no'}}</s>
else
td
if userData.emails.[0].verified
| {{_ 'yes'}}
else
| {{_ 'no'}}
if userData.loginDisabled
td <s>{{ moment userData.createdAt 'LLL' }}</s>
else
td {{ moment userData.createdAt 'LLL' }}
td
if userData.loginDisabled
| {{_ 'no'}}
else
| {{_ 'yes'}}
if userData.loginDisabled
td <s>{{_ userData.authenticationMethod }}</s>
else
td {{_ userData.authenticationMethod }}
if userData.loginDisabled
td <s>{{ userData.importUsernamesString }}</s>
else
td {{ userData.importUsernamesString }}
if userData.loginDisabled
td <s>{{ userData.orgsUserBelongs }}</s>
else
td {{ userData.orgsUserBelongs }}
if userData.loginDisabled
td <s>{{ userData.teamsUserBelongs }}</s>
else
td {{ userData.teamsUserBelongs }}
td
a.edit-user
i.fa.fa-edit
| ✏️
| {{_ 'edit'}}
a.more-settings-user
i.fa.fa-ellipsis-h
| ⋯
template(name="editOrgPopup")
form
@@ -448,8 +400,8 @@ template(name="editUserPopup")
option(value="{{value}}") {{_ value}}
label
| {{_ 'organizations'}}
i.fa.fa-plus-square#addUserOrg
i.fa.fa-minus-square#removeUserOrg
span#addUserOrg ➕
span#removeUserOrg ➖
select.js-orgs#jsOrgs
option(value="-1") {{_ 'organizations'}} :
each value in orgsDatas
@@ -458,8 +410,8 @@ template(name="editUserPopup")
input#jsUserOrgIdsInPut.js-userOrgIds.hide(type="hidden" value=user.orgIdsUserBelongs)
label
| {{_ 'teams'}}
i.fa.fa-plus-square#addUserTeam
i.fa.fa-minus-square#removeUserTeam
span#addUserTeam ➕
span#removeUserTeam ➖
select.js-teams#jsTeams
option(value="-1") {{_ 'teams'}} :
each value in teamsDatas
@@ -591,8 +543,8 @@ template(name="newUserPopup")
option(value="{{value}}") {{_ value}}
label
| {{_ 'organizations'}}
i.fa.fa-plus-square#addUserOrgNewUser
i.fa.fa-minus-square#removeUserOrgNewUser
span#addUserOrgNewUser ➕
span#removeUserOrgNewUser ➖
select.js-orgsNewUser#jsOrgsNewUser
option(value="-1") {{_ 'organizations'}} :
each value in orgsDatas
@@ -601,8 +553,8 @@ template(name="newUserPopup")
input#jsUserOrgIdsInPutNewUser.js-userOrgIdsNewUser.hide(type="text" value=user.orgIdsUserBelongs)
label
| {{_ 'teams'}}
i.fa.fa-plus-square#addUserTeamNewUser
i.fa.fa-minus-square#removeUserTeamNewUser
span#addUserTeamNewUser ➕
span#removeUserTeamNewUser ➖
select.js-teamsNewUser#jsTeamsNewUser
option(value="-1") {{_ 'teams'}} :
each value in teamsDatas
@@ -636,7 +588,7 @@ template(name="settingsOrgPopup")
// to impersonate organization?
// li
// a.impersonate-org
// i.fa.fa-user
// | 👤
// | {{_ 'impersonate-org'}}
//
//
@@ -659,7 +611,7 @@ template(name="settingsUserPopup")
ul.pop-over-list
li
a.impersonate-user
i.fa.fa-user
| 👤
| {{_ 'impersonate-user'}}
br
hr
+13
View File
@@ -172,6 +172,10 @@ BlazeComponent.extendComponent({
// Show only inactive users (loginDisabled is true)
query['loginDisabled'] = true;
break;
case 'admin':
// Show only admin users (isAdmin is true)
query['isAdmin'] = true;
break;
case 'all':
default:
// Show all users, no additional filter
@@ -203,26 +207,35 @@ BlazeComponent.extendComponent({
this.loading.set(w);
},
orgList() {
const limitOrgs = this.page.get() * orgsPerPage;
const orgs = ReactiveCache.getOrgs(this.findOrgsOptions.get(), {
sort: { orgDisplayName: 1 },
limit: limitOrgs,
fields: { _id: true },
});
// Count only the items currently loaded to browser, not total from database
this.numberOrgs.set(orgs.length);
return orgs;
},
teamList() {
const limitTeams = this.page.get() * teamsPerPage;
const teams = ReactiveCache.getTeams(this.findTeamsOptions.get(), {
sort: { teamDisplayName: 1 },
limit: limitTeams,
fields: { _id: true },
});
// Count only the items currently loaded to browser, not total from database
this.numberTeams.set(teams.length);
return teams;
},
peopleList() {
const limitUsers = this.page.get() * usersPerPage;
const users = ReactiveCache.getUsers(this.findUsersOptions.get(), {
sort: { username: 1 },
limit: limitUsers,
fields: { _id: true },
});
// Count only the items currently loaded to browser, not total from database
this.numberPeople.set(users.length);
return users;
},
+73 -1
View File
@@ -59,7 +59,79 @@
-ms-user-select: text;
user-select: text;
max-height: 100%;
overflow: auto;
overflow-x: scroll !important;
overflow-y: scroll !important;
scrollbar-gutter: stable;
/* Force horizontal scrollbar to always be visible */
min-width: 100%;
width: 100%;
}
/* Ensure scrollbars are always visible with proper styling for all admin pages */
.setting-content .content-body .main-body::-webkit-scrollbar {
width: 12px;
height: 12px;
display: block !important;
visibility: visible !important;
}
.setting-content .content-body .main-body::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 6px;
display: block !important;
visibility: visible !important;
}
.setting-content .content-body .main-body::-webkit-scrollbar-thumb {
background: #888;
border-radius: 6px;
display: block !important;
visibility: visible !important;
}
.setting-content .content-body .main-body::-webkit-scrollbar-thumb:hover {
background: #555;
}
.setting-content .content-body .main-body::-webkit-scrollbar-corner {
background: #f1f1f1;
}
/* Ensure tables and content are wide enough to trigger horizontal scrolling */
.setting-content .content-body .main-body table {
min-width: 1200px !important;
width: max-content !important;
table-layout: auto !important;
}
/* Force tables to always trigger horizontal scrolling */
.setting-content .content-body .main-body table td,
.setting-content .content-body .main-body table th {
white-space: nowrap;
min-width: 120px;
}
/* Ensure there's enough content to trigger vertical scrolling */
.setting-content .content-body .main-body {
min-height: calc(100vh - 200px);
padding-bottom: 50px;
}
/* Force horizontal scrollbar to always be visible at bottom */
.setting-content .content-body .main-body {
position: relative;
}
/* Add invisible content to force horizontal scrolling when needed */
.setting-content .content-body .main-body::after {
content: '';
display: block;
width: 100vw;
height: 1px;
position: absolute;
bottom: 0;
left: 0;
pointer-events: none;
}
.setting-content .content-body .main-body ul li {
padding: 0.5rem 0.5rem;
+189 -28
View File
@@ -3,64 +3,225 @@ template(name="setting")
unless currentUser.isAdmin
| {{_ 'error-notAuthorized'}}
else
.content-title
i.fa.fa-cog
span {{_ 'settings'}}
.content-title.ext-box
if isGeneralSetting
span
| 🔑
| {{_ 'registration'}}
else if isEmailSetting
span
| ✉️
| {{_ 'email'}}
else if isAccountSetting
span
| 👥
| {{_ 'accounts'}}
else if isTableVisibilityModeSetting
span
| 👁️
| {{_ 'tableVisibilityMode'}}
else if isAnnouncementSetting
span
| 📢
| {{_ 'admin-announcement'}}
else if isAccessibilitySetting
span
| ♿
| {{_ 'accessibility'}}
else if isLayoutSetting
span
| 🔗
| {{_ 'layout'}}
else if isWebhookSetting
span
| 🌐
| {{_ 'global-webhook'}}
else if isAttachmentSettings
span
| 📎
| {{_ 'attachments'}}
else if isCronSettings
span
| ⏰
| {{_ 'cron'}}
.content-body
.side-menu
ul
li.active
li(class="{{#if isGeneralSetting}}active{{/if}}")
a.js-setting-menu(data-id="registration-setting")
i.fa.fa-sign-in
| 🔑
| {{_ 'registration'}}
unless isSandstorm
li
li(class="{{#if isEmailSetting}}active{{/if}}")
a.js-setting-menu(data-id="email-setting")
i.fa.fa-envelope
| ✉️
| {{_ 'email'}}
li
li(class="{{#if isAccountSetting}}active{{/if}}")
a.js-setting-menu(data-id="account-setting")
i.fa.fa-users
| 👥
| {{_ 'accounts'}}
li
li(class="{{#if isTableVisibilityModeSetting}}active{{/if}}")
a.js-setting-menu(data-id="tableVisibilityMode-setting")
i.fa.fa-eye
| 👁️
| {{_ 'tableVisibilityMode'}}
li
li(class="{{#if isAnnouncementSetting}}active{{/if}}")
a.js-setting-menu(data-id="announcement-setting")
i.fa.fa-bullhorn
| 📢
| {{_ 'admin-announcement'}}
li
li(class="{{#if isAccessibilitySetting}}active{{/if}}")
a.js-setting-menu(data-id="accessibility-setting")
i.fa.fa-universal-access
| ♿
| {{_ 'accessibility'}}
li
li(class="{{#if isLayoutSetting}}active{{/if}}")
a.js-setting-menu(data-id="layout-setting")
i.fa.fa-object-group
| 🔗
| {{_ 'layout'}}
li
li(class="{{#if isWebhookSetting}}active{{/if}}")
a.js-setting-menu(data-id="webhook-setting")
i.fa.fa-globe
| 🌐
| {{_ 'global-webhook'}}
li(class="{{#if isAttachmentSettings}}active{{/if}}")
a.js-setting-menu(data-id="attachment-settings")
| 📎
| {{_ 'attachments'}}
li(class="{{#if isCronSettings}}active{{/if}}")
a.js-setting-menu(data-id="cron-settings")
| ⏰
| {{_ 'cron'}}
.main-body
if loading.get
if isLoading
+spinner
else if generalSetting.get
else if isAttachmentSettings
ul#attachment-setting.setting-detail
li
h3 {{_ 'attachment-storage-configuration'}}
.form-group
label {{_ 'writable-path'}}
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
small.form-text.text-muted {{_ 'filesystem-path-description'}}
.form-group
label {{_ 'attachments-path'}}
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
small.form-text.text-muted {{_ 'attachments-path-description'}}
.form-group
label {{_ 'avatars-path'}}
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
small.form-text.text-muted {{_ 'avatars-path-description'}}
li
h3 {{_ 'mongodb-gridfs-storage'}}
.form-group
label {{_ 'gridfs-enabled'}}
input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
li
h3 {{_ 's3-minio-storage'}}
.form-group
label {{_ 's3-enabled'}}
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
small.form-text.text-muted {{_ 's3-enabled-description'}}
.form-group
label {{_ 's3-endpoint'}}
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
small.form-text.text-muted {{_ 's3-endpoint-description'}}
.form-group
label {{_ 's3-bucket'}}
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
small.form-text.text-muted {{_ 's3-bucket-description'}}
.form-group
label {{_ 's3-region'}}
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
small.form-text.text-muted {{_ 's3-region-description'}}
.form-group
label {{_ 's3-access-key'}}
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
small.form-text.text-muted {{_ 's3-access-key-description'}}
.form-group
label {{_ 's3-secret-key'}}
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
small.form-text.text-muted {{_ 's3-secret-key-description'}}
.form-group
label {{_ 's3-ssl-enabled'}}
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
.form-group
label {{_ 's3-port'}}
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
small.form-text.text-muted {{_ 's3-port-description'}}
.form-group
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
else if isCronSettings
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'}}
else if isGeneralSetting
+general
else if emailSetting.get
else if isEmailSetting
unless isSandstorm
+email
else if accountSetting.get
else if isAccountSetting
+accountSettings
else if tableVisibilityModeSetting.get
else if isTableVisibilityModeSetting
+tableVisibilityModeSettings
else if announcementSetting.get
else if isAnnouncementSetting
+announcementSettings
else if accessibilitySetting.get
else if isAccessibilitySetting
+accessibilitySettings
else if layoutSetting.get
else if isLayoutSetting
+layoutSettings
else if webhookSetting.get
else if isWebhookSetting
+webhookSettings
template(name="webhookSettings")
+299 -22
View File
@@ -3,11 +3,12 @@ import { TAPi18n } from '/imports/i18n';
import { ALLOWED_WAIT_SPINNERS } from '/config/const';
import LockoutSettings from '/models/lockoutSettings';
BlazeComponent.extendComponent({
onCreated() {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
this.forgotPasswordSetting = new ReactiveVar(true);
this.forgotPasswordSetting = new ReactiveVar(false);
this.generalSetting = new ReactiveVar(true);
this.emailSetting = new ReactiveVar(false);
this.accountSetting = new ReactiveVar(false);
@@ -16,6 +17,8 @@ BlazeComponent.extendComponent({
this.accessibilitySetting = new ReactiveVar(false);
this.layoutSetting = new ReactiveVar(false);
this.webhookSetting = new ReactiveVar(false);
this.attachmentSettings = new ReactiveVar(false);
this.cronSettings = new ReactiveVar(false);
Meteor.subscribe('setting');
Meteor.subscribe('mailServer');
@@ -27,14 +30,243 @@ BlazeComponent.extendComponent({
Meteor.subscribe('lockoutSettings');
},
setError(error) {
this.error.set(error);
},
// Template helpers moved to BlazeComponent - using different names to avoid conflicts
isGeneralSetting() {
return this.generalSetting && this.generalSetting.get();
},
isEmailSetting() {
return this.emailSetting && this.emailSetting.get();
},
isAccountSetting() {
return this.accountSetting && this.accountSetting.get();
},
isTableVisibilityModeSetting() {
return this.tableVisibilityModeSetting && this.tableVisibilityModeSetting.get();
},
isAnnouncementSetting() {
return this.announcementSetting && this.announcementSetting.get();
},
isAccessibilitySetting() {
return this.accessibilitySetting && this.accessibilitySetting.get();
},
isLayoutSetting() {
return this.layoutSetting && this.layoutSetting.get();
},
isWebhookSetting() {
return this.webhookSetting && this.webhookSetting.get();
},
isAttachmentSettings() {
return this.attachmentSettings && this.attachmentSettings.get();
},
isCronSettings() {
return this.cronSettings && this.cronSettings.get();
},
isLoading() {
return this.loading && this.loading.get();
},
// Attachment settings helpers
filesystemPath() {
return process.env.WRITABLE_PATH || '/data';
},
attachmentsPath() {
const writablePath = process.env.WRITABLE_PATH || '/data';
return `${writablePath}/attachments`;
},
avatarsPath() {
const writablePath = process.env.WRITABLE_PATH || '/data';
return `${writablePath}/avatars`;
},
gridfsEnabled() {
return process.env.GRIDFS_ENABLED === 'true';
},
s3Enabled() {
return process.env.S3_ENABLED === 'true';
},
s3Endpoint() {
return process.env.S3_ENDPOINT || '';
},
s3Bucket() {
return process.env.S3_BUCKET || '';
},
s3Region() {
return process.env.S3_REGION || '';
},
s3SslEnabled() {
return process.env.S3_SSL_ENABLED === 'true';
},
s3Port() {
return process.env.S3_PORT || 443;
},
// Cron settings helpers
migrationStatus() {
return TAPi18n.__('idle'); // Placeholder
},
migrationProgress() {
return 0; // Placeholder
},
cronJobs() {
return []; // Placeholder
},
setLoading(w) {
this.loading.set(w);
},
// Event handlers for attachment settings
'click button.js-test-s3-connection'(event) {
event.preventDefault();
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'));
}
});
},
'click button.js-save-s3-settings'(event) {
event.preventDefault();
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
}
});
},
// Event handlers for cron settings
'click button.js-start-all-migrations'(event) {
event.preventDefault();
Meteor.call('startAllMigrations', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('migration-started'));
}
});
},
'click button.js-pause-all-migrations'(event) {
event.preventDefault();
Meteor.call('pauseAllMigrations', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('migration-paused'));
}
});
},
'click button.js-stop-all-migrations'(event) {
event.preventDefault();
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
Meteor.call('stopAllMigrations', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('migration-stopped'));
}
});
}
},
'click button.js-schedule-board-cleanup'(event) {
event.preventDefault();
Meteor.call('scheduleBoardCleanup', (error, result) => {
if (error) {
alert(TAPi18n.__('board-cleanup-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('board-cleanup-scheduled'));
}
});
},
'click button.js-schedule-board-archive'(event) {
event.preventDefault();
Meteor.call('scheduleBoardArchive', (error, result) => {
if (error) {
alert(TAPi18n.__('board-archive-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('board-archive-scheduled'));
}
});
},
'click button.js-schedule-board-backup'(event) {
event.preventDefault();
Meteor.call('scheduleBoardBackup', (error, result) => {
if (error) {
alert(TAPi18n.__('board-backup-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('board-backup-scheduled'));
}
});
},
'click button.js-pause-job'(event) {
event.preventDefault();
const jobId = $(event.target).data('job-id');
Meteor.call('pauseCronJob', jobId, (error, result) => {
if (error) {
alert(TAPi18n.__('cron-job-pause-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('cron-job-paused'));
}
});
},
'click button.js-delete-job'(event) {
event.preventDefault();
const jobId = $(event.target).data('job-id');
if (confirm(TAPi18n.__('cron-job-delete-confirm'))) {
Meteor.call('deleteCronJob', jobId, (error, result) => {
if (error) {
alert(TAPi18n.__('cron-job-delete-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('cron-job-deleted'));
}
});
}
},
'click button.js-add-cron-job'(event) {
event.preventDefault();
// Placeholder for adding a new cron job (e.g., open a modal)
alert(TAPi18n.__('add-cron-job-placeholder'));
},
checkField(selector) {
const value = $(selector).val();
if (!value || value.trim() === '') {
@@ -99,21 +331,62 @@ BlazeComponent.extendComponent({
toggleDisplayAuthenticationMethod() {
$('#display-authentication-method').toggleClass('is-checked');
},
initializeAttachmentSubMenu() {
// Set default sub-menu state for attachment settings
// This will be handled by the attachment settings component
console.log('Initializing attachment sub-menu');
},
initializeCronSubMenu() {
// Set default sub-menu state for cron settings
// This will be handled by the cron settings template
console.log('Initializing cron sub-menu');
},
switchMenu(event) {
const target = $(event.target);
if (!target.hasClass('active')) {
$('.side-menu li.active').removeClass('active');
target.parent().addClass('active');
const targetID = target.data('id');
this.forgotPasswordSetting.set('forgot-password-setting' === targetID);
this.generalSetting.set('registration-setting' === targetID);
this.emailSetting.set('email-setting' === targetID);
this.accountSetting.set('account-setting' === targetID);
this.announcementSetting.set('announcement-setting' === targetID);
this.accessibilitySetting.set('accessibility-setting' === targetID);
this.layoutSetting.set('layout-setting' === targetID);
this.webhookSetting.set('webhook-setting' === targetID);
this.tableVisibilityModeSetting.set('tableVisibilityMode-setting' === targetID);
// Reset all settings to false
this.forgotPasswordSetting.set(false);
this.generalSetting.set(false);
this.emailSetting.set(false);
this.accountSetting.set(false);
this.tableVisibilityModeSetting.set(false);
this.announcementSetting.set(false);
this.accessibilitySetting.set(false);
this.layoutSetting.set(false);
this.webhookSetting.set(false);
this.attachmentSettings.set(false);
this.cronSettings.set(false);
// Set the selected setting to true
if (targetID === 'registration-setting') {
this.generalSetting.set(true);
} else if (targetID === 'email-setting') {
this.emailSetting.set(true);
} else if (targetID === 'account-setting') {
this.accountSetting.set(true);
} else if (targetID === 'tableVisibilityMode-setting') {
this.tableVisibilityModeSetting.set(true);
} else if (targetID === 'announcement-setting') {
this.announcementSetting.set(true);
} else if (targetID === 'accessibility-setting') {
this.accessibilitySetting.set(true);
} else if (targetID === 'layout-setting') {
this.layoutSetting.set(true);
} else if (targetID === 'webhook-setting') {
this.webhookSetting.set(true);
} else if (targetID === 'attachment-settings') {
this.attachmentSettings.set(true);
this.initializeAttachmentSubMenu();
} else if (targetID === 'cron-settings') {
this.cronSettings.set(true);
this.initializeCronSubMenu();
}
}
},
@@ -211,9 +484,6 @@ BlazeComponent.extendComponent({
const displayAuthenticationMethod =
$('input[name=displayAuthenticationMethod]:checked').val() === 'true';
const defaultAuthenticationMethod = $('#defaultAuthenticationMethod').val();
const accessibilityPageEnabled = $('input[name=accessibilityPageEnabled]:checked').val() === 'true';
const accessibilityTitle = ($('#accessibility-title').val() || '').trim();
const accessibilityContent = ($('#accessibility-content').val() || '').trim();
const spinnerName = ($('#spinnerName').val() || '').trim();
try {
@@ -237,9 +507,6 @@ BlazeComponent.extendComponent({
oidcBtnText,
mailDomainName,
legalNotice,
accessibilityPageEnabled,
accessibilityTitle,
accessibilityContent,
},
});
} catch (e) {
@@ -451,18 +718,27 @@ BlazeComponent.extendComponent({
},
saveAccessibility() {
this.setLoading(true);
const title = $('#admin-accessibility-title')
.val()
.trim();
const content = $('#admin-accessibility-content')
.val()
.trim();
AccessibilitySettings.update(AccessibilitySettings.findOne()._id, {
$set: {
title: title,
body: content
},
});
try {
AccessibilitySettings.update(AccessibilitySettings.findOne()._id, {
$set: {
title: title,
body: content
},
});
} catch (e) {
console.error('Error saving accessibility settings:', e);
return;
} finally {
this.setLoading(false);
}
},
toggleAccessibility() {
@@ -525,3 +801,4 @@ Template.selectSpinnerName.helpers({
return Template.instance().data.spinnerName === match;
},
});
@@ -5,31 +5,31 @@ template(name="settingHeaderBar")
.setting-header-btns.left
if currentUser
a.setting-header-btn.settings(href="{{pathFor 'setting'}}")
i.fa(class="fa-cog")
| ⚙️
span {{_ 'settings'}}
a.setting-header-btn.people(href="{{pathFor 'people'}}")
i.fa(class="fa-users")
| 👥
span {{_ 'people'}}
a.setting-header-btn.informations(href="{{pathFor 'admin-reports'}}")
i.fa(class="fa-list")
| 📋
span {{_ 'reports'}}
a.setting-header-btn.informations(href="{{pathFor 'attachments'}}")
i.fa(class="fa-paperclip")
| 📎
span {{_ 'attachments'}}
a.setting-header-btn.informations(href="{{pathFor 'translation'}}")
i.fa(class="fa-font")
| 🔤
span {{_ 'translation'}}
a.setting-header-btn.informations(href="{{pathFor 'information'}}")
i.fa(class="fa-info-circle")
| ℹ️
span {{_ 'info'}}
else
a.setting-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
| 🚪
span {{_ 'log-in'}}
@@ -1,6 +1,4 @@
.main-body {
overflow: scroll;
}
/* Scrollbar styles are now handled by settingBody.css for all admin pages */
table {
color: #000;
}
@@ -9,12 +9,12 @@ template(name="translation")
+spinner
else if translationSetting.get
span
i.fa.fa-font
| 🔤
unless isMiniScreen
| {{_ 'translation'}}
input#searchTranslationInput(placeholder="{{_ 'search'}}")
button#searchTranslationButton
i.fa.fa-search
| 🔍
| {{_ 'search'}}
.ext-box-right
span {{#unless isMiniScreen}}{{_ 'translation-number'}}{{/unless}} #{translationNumber}
@@ -24,7 +24,7 @@ template(name="translation")
ul
li.active
a.js-translation-menu(data-id="translation-setting")
i.fa.fa-font
| 🔤
| {{_ 'translation'}}
.main-body
if loading.get
@@ -47,7 +47,7 @@ template(name="translationGeneral")
template(name="newTranslationRow")
a.new-translation
i.fa.fa-plus-square
| ➕
| {{_ 'new'}}
template(name="translationRow")
@@ -57,10 +57,10 @@ template(name="translationRow")
td {{translationData.translationText}}
td
a.edit-translation
i.fa.fa-edit
| ✏️
| {{_ 'edit'}}
a.more-settings-translation
i.fa.fa-ellipsis-h
| ⋯
template(name="editTranslationPopup")
form
+76 -71
View File
@@ -7,14 +7,14 @@ template(name="sidebar")
.sidebar-actions
.sidebar-shortcuts
a.sidebar-btn.js-shortcuts(title="{{_ 'keyboard-shortcuts' }}")
i.fa.fa-keyboard-o
| ⌨️
span {{_ 'keyboard-shortcuts' }}
a.sidebar-btn.js-keyboard-shortcuts-toggle(
title="{{#if isKeyboardShortcuts}}{{_ 'keyboard-shortcuts-enabled'}}{{else}}{{_ 'keyboard-shortcuts-disabled'}}{{/if}}")
i.fa(class="fa-solid fa-{{#if isKeyboardShortcuts}}check-square-o{{else}}ban{{/if}}")
| {{#if isKeyboardShortcuts}}✅{{else}}🚫{{/if}}
if isAccessibilityEnabled
a.sidebar-accessibility
i.fa.fa-universal-access
| ♿
span {{_ 'accessibility'}}
a.sidebar-xmark.js-close-sidebar &#10005;
.sidebar-content.js-board-sidebar-content
@@ -22,13 +22,12 @@ template(name="sidebar")
// i.fa.fa-navicon
unless isDefaultView
h2
a.fa.fa-chevron-left.js-back-home
a.js-back-home | ⬅️
= getViewTitle
if isOpen
+Template.dynamic(template=getViewTemplate)
template(name='homeSidebar')
hr
+membersWidget
hr
+labelsWidget
@@ -38,11 +37,12 @@ template(name='homeSidebar')
span {{_ 'hide-minicard-label-text'}}
b &nbsp;
.materialCheckBox(class="{{#if hiddenMinicardLabelText}}is-checked{{/if}}")
ul#cards.vertical-scrollbars-toggle
a.flex.js-vertical-scrollbars-toggle(title="{{_ 'enable-vertical-scrollbars'}}")
span {{_ 'enable-vertical-scrollbars'}}
b &nbsp;
.materialCheckBox(class="{{#if isVerticalScrollbars}}is-checked{{/if}}")
if currentUser
ul#cards.vertical-scrollbars-toggle
a.flex.js-vertical-scrollbars-toggle(title="{{_ 'enable-vertical-scrollbars'}}")
span {{_ 'enable-vertical-scrollbars'}}
b &nbsp;
.materialCheckBox(class="{{#if isVerticalScrollbars}}is-checked{{/if}}")
ul#cards.show-week-of-year-toggle
a.flex.js-show-week-of-year-toggle(title="{{_ 'show-week-of-year'}}")
span {{_ 'show-week-of-year'}}
@@ -51,7 +51,7 @@ template(name='homeSidebar')
hr
unless currentUser.isNoComments
h3.activity-title
i.fa.fa-comments-o
| 💬
| {{_ 'activities'}}
.material-toggle-switch(title="{{_ 'show-activities'}}")
@@ -67,11 +67,11 @@ template(name="membersWidget")
unless currentUser.isWorker
h3
a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}")
i.board-header-btn-icon.fa.fa-cog
| ⚙️
| {{_ 'boardMenuPopup-title'}}
hr
h3
i.fa.fa-users
| 👥
| {{_ 'members'}}
+basicTabs(tabs=tabs)
+tabContent(slug="people")
@@ -83,15 +83,15 @@ template(name="membersWidget")
if isSandstorm
if currentUser.isBoardMember
a.member.add-member.sandstorm-powerbox-request-identity(title="{{_ 'add-members'}}")
i.fa.fa-plus
| ➕
else if currentUser.isBoardAdmin
a.member.add-member.js-manage-board-members(title="{{_ 'add-members'}}")
i.fa.fa-plus
| ➕
.clearfix
if isInvited
hr
p
i.fa.fa-exclamation-circle
| ⚠️
| {{_ 'just-invited'}}
button.js-member-invite-accept.primary {{_ 'accept'}}
button.js-member-invite-decline {{_ 'decline'}}
@@ -127,7 +127,7 @@ template(name="boardOrgGeneral")
th
if currentUser.isBoardAdmin
a.member.orgOrTeamMember.add-member.js-manage-board-addOrg(title="{{_ 'add-members'}}")
i.addTeamFaPlus.fa.fa-plus
| ➕
.divaddfaplusminus
| {{_ 'add'}}
each org in currentBoard.activeOrgs
@@ -148,7 +148,7 @@ template(name="boardTeamGeneral")
th
if currentUser.isBoardAdmin
a.member.orgOrTeamMember.add-member.js-manage-board-addTeam(title="{{_ 'add-members'}}")
i.addTeamFaPlus.fa.fa-plus
| ➕
.divaddfaplusminus
| {{_ 'add'}}
each currentBoard.activeTeams
@@ -161,7 +161,7 @@ template(name="boardChangeColorPopup")
span.background-box(class="board-color-{{this}}")
span {{this}}
if isSelected
i.fa.fa-check
| ✅
template(name="boardChangeBackgroundImagePopup")
form
@@ -187,14 +187,14 @@ template(name="boardInfoOnMyBoardsPopup")
a.flex.js-field-has-cardcounterlist(class="{{#if allowsCardCounterList}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCardCounterList}}is-checked{{/if}}")
span
i.fa.fa-sign-out
| 🚪
| {{_ 'show-card-counter-per-list'}}
unless currentSetting.hideBoardMemberList
div.check-div
a.flex.js-field-has-boardmemberlist(class="{{#if allowsBoardMemberList}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsBoardMemberList}}is-checked{{/if}}")
span
i.fa.fa-hourglass-start
| ⏳
| {{_ 'show-board_members-avatar'}}
template(name="boardCardSettingsPopup")
@@ -204,143 +204,149 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-receiveddate(class="{{#if allowsReceivedDate}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsReceivedDate}}is-checked{{/if}}")
span
i.fa.fa-sign-out
| 🚪
| {{_ 'card-received'}}
div.check-div
a.flex.js-field-has-startdate(class="{{#if allowsStartDate}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsStartDate}}is-checked{{/if}}")
span
i.fa.fa-hourglass-start
| ⏳
| {{_ 'card-start'}}
div.check-div
a.flex.js-field-has-duedate(class="{{#if allowsDueDate}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsDueDate}}is-checked{{/if}}")
span
i.fa.fa-sign-in
| 🚪
| {{_ 'card-due'}}
div.check-div
a.flex.js-field-has-enddate(class="{{#if allowsEndDate}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsEndDate}}is-checked{{/if}}")
span
i.fa.fa-hourglass-end
| ⏰
| {{_ 'card-end'}}
div.check-div
a.flex.js-field-has-members(class="{{#if allowsMembers}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsMembers}}is-checked{{/if}}")
span
i.fa.fa-users
| 👥
| {{_ 'members'}}
div.check-div
a.flex.js-field-has-creator(class="{{#if allowsCreator}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCreator}}is-checked{{/if}}")
span
i.fa.fa-user
| 👤
| {{_ 'creator'}}
div.check-div
a.flex.js-field-has-creator-on-minicard(class="{{#if allowsCreatorOnMinicard}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCreatorOnMinicard}}is-checked{{/if}}")
span
i.fa.fa-user
| 👤
| {{_ 'creator-on-minicard'}}
div.check-div
a.flex.js-field-has-assignee(class="{{#if allowsAssignee}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsAssignee}}is-checked{{/if}}")
span
i.fa.fa-user
| 👤
| {{_ 'assignee'}}
div.check-div
a.flex.js-field-has-assigned-by(class="{{#if allowsAssignedBy}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsAssignedBy}}is-checked{{/if}}")
span
i.fa.fa-shopping-cart
| 🛒
| {{_ 'assigned-by'}}
div.check-div
a.flex.js-field-has-requested-by(class="{{#if allowsRequestedBy}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsRequestedBy}}is-checked{{/if}}")
span
i.fa.fa-user-plus
| 👤➕
| {{_ 'requested-by'}}
div.check-div
a.flex.js-field-has-card-sorting-by-number(class="{{#if allowsCardSortingByNumber}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCardSortingByNumber}}is-checked{{/if}}")
span
i.fa.fa-sort
| 🔢
| {{_ 'card-sorting-by-number'}}
div.check-div
a.flex.js-field-has-card-sorting-by-number-on-minicard(class="{{#if allowsCardSortingByNumberOnMinicard}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCardSortingByNumberOnMinicard}}is-checked{{/if}}")
span
i.fa.fa-sort
| 🔢
| {{_ 'card-sorting-by-number-on-minicard'}}
div.check-div
a.flex.js-field-has-card-show-lists(class="{{#if allowsShowLists}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsShowLists}}is-checked{{/if}}")
span
i.fa.fa-list
| 📋
| {{_ 'card-show-lists'}}
div.check-div
a.flex.js-field-has-labels(class="{{#if allowsLabels}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsLabels}}is-checked{{/if}}")
span
i.fa.fa-tags
| 🏷️
| {{_ 'labels'}}
div.check-div
a.flex.js-field-has-card-show-lists-on-minicard(class="{{#if allowsShowListsOnMinicard}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsShowListsOnMinicard}}is-checked{{/if}}")
span
| 📋
| {{_ 'card-show-lists-on-minicard'}}
div.check-div
a.flex.js-field-has-card-number(class="{{#if allowsCardNumber}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCardNumber}}is-checked{{/if}}")
span
i.fa.fa-hashtag
| #️⃣
| {{_ 'card'}}
| {{_ 'number'}}
div.check-div
a.flex.js-field-has-description-title(class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
span
i.fa.fa-align-left
| 📝
| {{_ 'description'}}
| {{_ 'title'}}
div.check-div
a.flex.js-field-has-description-text(class="{{#if allowsDescriptionText}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsDescriptionText}}is-checked{{/if}}")
span
i.fa.fa-align-left
| 📝
| {{_ 'description'}}
| {{_ 'custom-field-text'}}
div.check-div
a.flex.js-field-has-description-text-on-minicard(class="{{#if allowsDescriptionTextOnMinicard}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsDescriptionTextOnMinicard}}is-checked{{/if}}")
span
i.fa.fa-align-left
| 📝
| {{_ 'description-on-minicard'}}
div.check-div
a.flex.js-field-has-checklists(class="{{#if allowsChecklists}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsChecklists}}is-checked{{/if}}")
span
i.fa.fa-check
| ✅
| {{_ 'checklists'}}
div.check-div
a.flex.js-field-has-subtasks(class="{{#if allowsSubtasks}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsSubtasks}}is-checked{{/if}}")
span
i.fa.fa-sitemap
| 🌐
| {{_ 'subtasks'}}
div.check-div
a.flex.js-field-has-attachments(class="{{#if allowsAttachments}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsAttachments}}is-checked{{/if}}")
span
i.fa.fa-paperclip
| 📎
| {{_ 'attachments'}}
div.check-div
a.flex.js-field-has-badge-attachment-on-minicard(class="{{#if allowsBadgeAttachmentOnMinicard}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsBadgeAttachmentOnMinicard}}is-checked{{/if}}")
span
i.fa.fa-paperclip
| 📎
| {{_ 'badge-attachment-on-minicard'}}
div.check-div
a.flex.js-field-has-cover-attachment-on-minicard(class="{{#if allowsCoverAttachmentOnMinicard}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCoverAttachmentOnMinicard}}is-checked{{/if}}")
span
i.fa.fa-book
i.fa.fa-picture-o
| 📖
| 🖼️
| {{_ 'cover-attachment-on-minicard'}}
//div.check-div
// a.flex.js-field-has-comments(class="{{#if allowsComments}}is-checked{{/if}}")
@@ -417,7 +423,7 @@ template(name="chooseBoardSource")
template(name="archiveBoardPopup")
p {{_ 'close-board-pop'}}
button.js-confirm.negate.full(type="submit")
i.fa.fa-archive
| 📦
| {{_ 'archive'}}
template(name="outgoingWebhooksPopup")
@@ -453,25 +459,25 @@ template(name="boardMenuPopup")
if currentUser.isBoardAdmin
li
a.js-open-rules-view(title="{{_ 'rules'}}")
i.fa.fa-magic
| ✨
| {{_ 'rules'}}
if currentUser.isBoardAdmin
li
a.js-custom-fields
i.fa.fa-list-alt
| 📝
| {{_ 'custom-fields'}}
li
a.js-open-archives
i.fa.fa-archive
| 📦
| {{_ 'archived-items'}}
if currentUser.isBoardAdmin
li
a.js-change-board-color
i.fa.fa-paint-brush
| 🎨
| {{_ 'board-change-color'}}
li
a.js-change-background-image
i.fa.fa-picture-o
| 🖼️
| {{_ 'board-change-background-image'}}
//Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
//if currentUser.isBoardAdmin
@@ -486,20 +492,20 @@ template(name="boardMenuPopup")
if withApi
li
a.js-export-board
i.fa.fa-share-alt
| 📤
| {{_ 'export-board'}}
if currentUser.isBoardAdmin
li
a.js-outgoing-webhooks
i.fa.fa-globe
| 🌐
| {{_ 'outgoing-webhooks'}}
li
a.js-card-settings
i.fa.fa-id-card-o
| 🃏
| {{_ 'card-settings'}}
li
a.js-subtask-settings
i.fa.fa-sitemap
| 🌐
| {{_ 'subtask-settings'}}
unless currentBoard.isTemplatesBoard
if currentUser.isBoardAdmin
@@ -507,41 +513,40 @@ template(name="boardMenuPopup")
ul.pop-over-list
li
a.js-archive-board
i.fa.fa-arrow-right
i.fa.fa-archive
| ➡️📦
| {{_ 'archive-board'}}
template(name="exportBoard")
ul.pop-over-list
li
a.download-json-link(href="{{exportUrl}}", download="{{exportJsonFilename}}")
i.fa.fa-share-alt
| 📤
| {{_ 'export-board-json'}}
li
a(href="{{exportUrlExcel}}", download="{{exportFilenameExcel}}")
i.fa.fa-share-alt
| 📤
| {{_ 'export-board-excel'}}
li
a(href="{{exportCsvUrl}}", download="{{exportCsvFilename}}")
i.fa.fa-share-alt
| 📤
| {{_ 'export-board-csv'}} ,
li
a(href="{{exportScsvUrl}}", download="{{exportCsvFilename}}")
i.fa.fa-share-alt
| 📤
| {{_ 'export-board-csv'}} ;
li
a(href="{{exportTsvUrl}}", download="{{exportTsvFilename}}")
i.fa.fa-share-alt
| 📤
| {{_ 'export-board-tsv'}}
li
a.html-export-board
i.fa.fa-archive
| 📦
| {{_ 'export-board-html'}}
template(name="labelsWidget")
.board-widget.board-widget-labels
h3
i.fa.fa-tags
| 🏷️
| {{_ 'labels'}}
.board-widget-content
each currentBoard.labels
@@ -552,7 +557,7 @@ template(name="labelsWidget")
= name
if currentUser.isBoardAdmin
a.card-label.add-label.js-add-label(title="{{_ 'label-create'}}")
i.fa.fa-plus
| ➕
template(name="memberPopup")
.board-member-menu
@@ -564,7 +569,7 @@ template(name="memberPopup")
p.quiet @#{user.username}
if isInvited
p
i.fa.fa-exclamation-circle
| ⚠️
| {{_ 'not-accepted-yet'}}
ul.pop-over-list
@@ -659,31 +664,31 @@ template(name="changePermissionsPopup")
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-admin{{/if}}")
| {{_ 'admin'}}
if isAdmin
i.fa.fa-check
| ✅
span.sub-name {{_ 'admin-desc'}}
li
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-normal{{/if}}")
| {{_ 'normal'}}
if isNormal
i.fa.fa-check
| ✅
span.sub-name {{_ 'normal-desc'}}
li
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-no-comments{{/if}}")
| {{_ 'no-comments'}}
if isNoComments
i.fa.fa-check
| ✅
span.sub-name {{_ 'no-comments-desc'}}
li
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-comment-only{{/if}}")
| {{_ 'comment-only'}}
if isCommentOnly
i.fa.fa-check
| ✅
span.sub-name {{_ 'comment-only-desc'}}
li
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-worker{{/if}}")
| {{_ 'worker'}}
if isWorker
i.fa.fa-check
| ✅
span.sub-name {{_ 'worker-desc'}}
if isLastAdmin
hr
+28 -3
View File
@@ -65,8 +65,10 @@ BlazeComponent.extendComponent({
},
reachNextPeak() {
const activitiesComponent = this.childComponents('activities')[0];
activitiesComponent.loadNextPage();
const activitiesChildren = this.childComponents('activities');
if (activitiesChildren && activitiesChildren.length > 0 && activitiesChildren[0] && typeof activitiesChildren[0].loadNextPage === 'function') {
activitiesChildren[0].loadNextPage();
}
},
isTongueHidden() {
@@ -201,6 +203,8 @@ BlazeComponent.extendComponent({
},
}).register('homeSidebar');
Template.boardInfoOnMyBoardsPopup.helpers({
hideCardCounterList() {
return Utils.isMiniScreen() && Session.get('currentBoard');
@@ -996,10 +1000,15 @@ BlazeComponent.extendComponent({
return this.currentBoard.allowsShowLists;
},
allowsLabels() {
return this.currentBoard.allowsLabels;
},
allowsShowListsOnMinicard() {
return this.currentBoard.allowsShowListsOnMinicard;
},
allowsChecklists() {
return this.currentBoard.allowsChecklists;
},
@@ -1282,13 +1291,29 @@ BlazeComponent.extendComponent({
this.currentBoard.setAllowsLabels(this.currentBoard.allowsLabels);
$(`.js-field-has-labels ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsAssignee,
this.currentBoard.allowsLabels,
);
$('.js-field-has-labels').toggleClass(
CKCLS,
this.currentBoard.allowsLabels,
);
},
'click .js-field-has-card-show-lists-on-minicard'(evt) {
evt.preventDefault();
this.currentBoard.allowsShowListsOnMinicard = !this.currentBoard
.allowsShowListsOnMinicard;
this.currentBoard.setAllowsShowListsOnMinicard(
this.currentBoard.allowsShowListsOnMinicard,
);
$(`.js-field-has-card-show-lists-on-minicard ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsShowListsOnMinicard,
);
$('.js-field-has-card-show-lists-on-minicard').toggleClass(
CKCLS,
this.currentBoard.allowsShowListsOnMinicard,
);
},
'click .js-field-has-description-title'(evt) {
evt.preventDefault();
this.currentBoard.allowsDescriptionTitle = !this.currentBoard
@@ -4,7 +4,8 @@ template(name="customFieldsSidebar")
li
div.minicard-wrapper.js-minicard
div.minicard
a.fa.fa-pencil.js-edit-custom-field.minicard-edit-button
a.js-edit-custom-field.minicard-edit-button
| ✏️
div.minicard-title
+viewer
=name
@@ -13,7 +14,7 @@ template(name="customFieldsSidebar")
if currentUser.isBoardMember
hr
a.sidebar-btn.js-open-create-custom-field
i.fa.fa-plus
| ➕
span {{_ 'createCustomField'}}
template(name="createCustomFieldPopup")
Loaded 100 of 437 files, more files were not shown because too many files have changed in this diff. Show more