Compare commits

...
417 Commits
Author SHA1 Message Date
Lauri Ojansivu 6b2759f97d v8.78 2026-04-09 04:52:05 +03:00
Lauri Ojansivu adf4312e5a v8.77 2026-04-09 04:40:21 +03:00
Lauri Ojansivu b1707b8691 Upgraded to Meteor 3.5-beta.7
Thanks to Meteor developers !
2026-04-09 04:37:36 +03:00
Lauri Ojansivu 25dff3f2da v8.76 2026-04-09 04:19:51 +03:00
Lauri Ojansivu bc88c0dfc2 Fix GitHub Actions builds.
Thanks to xet7 !
2026-04-09 04:14:46 +03:00
Lauri Ojansivu 1ca2dfd1b1 v8.75 2026-04-09 03:58:01 +03:00
Lauri Ojansivu bdb9274485 Updated to Node.js 24.x at release scripts.
Thanks to xet7 !
2026-04-09 03:56:06 +03:00
Lauri Ojansivu 14f9ac39ad Upgraded to Meteor 3.5-beta.7 and Node.js 24.14.1. Now prefers MongoDB Change Streams at Snap Candidate and docker-compose.yml.
Thanks to Meteor developers and xet7 !
2026-04-09 03:44:58 +03:00
Lauri Ojansivu d4b52f7de9 Updated ChangeLog. 2026-04-09 02:19:44 +03:00
Lauri Ojansivu bdc65ec6f4 Merge pull request #6261 from KhaoulaMaleh/fix/translation-refinement
Refine translations
2026-04-08 23:30:39 +03:00
KhaoulaMaleh 7fb31cecd5 fix refinement 2026-04-08 00:45:27 -11:00
KhaoulaMaleh 317c82a9f0 fixed after review 2026-04-08 00:16:18 -11:00
KhaoulaMaleh 9b5c89d235 Enhance translations with new key:value pairs 2026-04-07 23:36:43 -11:00
Lauri Ojansivu c23e190e8e Updated ChangeLog. 2026-04-08 02:16:21 +03:00
Lauri Ojansivu 4a73371499 Merge pull request #6260 from KhaoulaMaleh/fix/Card_setting_checkbox
fixed Card settings checkboxes
2026-04-08 02:14:28 +03:00
KhaoulaMaleh 6f8af55489 fixed Card settings checkboxes 2026-04-08 00:04:48 -11:00
Lauri Ojansivu 2a05f62bf9 Updated ChangeLog. 2026-04-08 01:49:33 +03:00
Lauri Ojansivu d6eb1eb76f Merge pull request #6259 from KhaoulaMaleh/fix/enhance_translation
Enhance translations with new key:value pairs
2026-04-08 01:46:33 +03:00
Lauri Ojansivu 27542f3a51 v8.74 2026-04-08 00:34:49 +03:00
Lauri Ojansivu 0328e70d5f Fix can not save Card Data. Part 2.
Thanks to xet7 !

Fixes #6254
2026-04-08 00:31:40 +03:00
Lauri Ojansivu c46932ee30 v8.73 2026-04-07 23:18:14 +03:00
Lauri Ojansivu c7408ad8d1 Fix board and card activities. Part 2.
Thanks to xet7 !

Related #6252
2026-04-07 23:12:26 +03:00
Lauri Ojansivu 05dc8f8f93 Updated ChangeLog. 2026-04-07 22:01:59 +03:00
Lauri Ojansivu aea8c285c2 Fix Change list inside or card.
Thanks to mimZD and xet7 !

Fixes #6161
2026-04-07 22:00:11 +03:00
Lauri Ojansivu a859137971 Updated ChangeLog. 2026-04-07 21:53:11 +03:00
Lauri Ojansivu 08ebad0fe4 Fix Checklists add and Subtasks reorder. Fix Board Activity part 1.
Thanks to xet7 !

Related #6252
2026-04-07 21:49:26 +03:00
Lauri Ojansivu d54ebfa0e1 Updated ChangeLog. 2026-04-07 20:39:55 +03:00
Lauri Ojansivu 304a007d07 Fix Search All Boards result opened card missing Copy Card Link etc that can be used to open correct card.
Thanks bcook-konza and xet7 !

Fixes #6251
2026-04-07 20:35:20 +03:00
Lauri Ojansivu b6b58c9d95 Updated ChangeLog. 2026-04-07 20:10:38 +03:00
Lauri Ojansivu 7f0c42cca5 Fix Error in choosing member or assignee in a card.
Thanks to hmeunier95 and xet7 !

Fixes #6257
2026-04-07 20:08:04 +03:00
Lauri Ojansivu 9508d46ebd Updated ChangeLog. 2026-04-07 19:25:15 +03:00
Lauri Ojansivu bcbf4771ea Fix 8.72 Bug: LDAP login does not work. ReactiveCache is not defined.
Thanks to poc-sm and xet7 !

Fixes #6255
2026-04-07 19:23:06 +03:00
Lauri Ojansivu 9da5ec25c6 Updated ChangeLog. 2026-04-07 19:21:20 +03:00
Lauri Ojansivu 028b404377 Removed exclude-arch from rebuild-wekan.sh, it is not necessary anymore.
Thanks to xet7 !
2026-04-07 19:19:53 +03:00
Lauri Ojansivu 8da0f34ee6 Fix can not save Card Data.
Thanks to xet7 !

Fixes #6254
2026-04-07 19:17:35 +03:00
Lauri Ojansivu cac2628b19 Updated ChangeLog. 2026-04-07 11:40:46 +03:00
Lauri Ojansivu 9c69530256 Fix Link to Windows release doesn't work.
Thanks to hmeunier95 and xet7 !

Fixes #6253
2026-04-07 11:37:50 +03:00
Lauri Ojansivu 6e108358d7 v8.72 2026-04-07 10:44:37 +03:00
Lauri Ojansivu 87f592c67f Updated release scripts.
Thanks to xet7 !
2026-04-07 10:42:45 +03:00
Lauri Ojansivu fc95abd587 v8.72 2026-04-07 09:52:41 +03:00
Lauri Ojansivu ce3e51b147 Fix Impersonate User. Show/Edit Avatar at Admin Panel/People/People.
Thanks to ahlgrimma, DnT1mmer, robbagithub, norrig and xet7 !

Fixes #6226,
fixes #6101,
fixes #3327,
fixes #4748
2026-04-07 09:52:41 +03:00
Lauri Ojansivu bb48b23266 Merge pull request #6250 from wekan/dependabot/github_actions/docker/login-action-4.1.0
Bump docker/login-action from 4.0.0 to 4.1.0
2026-04-07 09:26:54 +03:00
Lauri Ojansivu fdf00e06f5 Merge pull request #6249 from wekan/dependabot/github_actions/actions/cache-5
Bump actions/cache from 4 to 5
2026-04-07 09:26:38 +03:00
Lauri Ojansivu 950b246da8 Merge pull request #6248 from wekan/dependabot/github_actions/actions/upload-artifact-7
Bump actions/upload-artifact from 4 to 7
2026-04-07 09:26:25 +03:00
Lauri Ojansivu 6ac0df8448 Merge pull request #6247 from wekan/dependabot/github_actions/actions/download-artifact-8
Bump actions/download-artifact from 4 to 8
2026-04-07 09:25:51 +03:00
dependabot[bot] 80ae3a18a8 Bump docker/login-action from 4.0.0 to 4.1.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/b45d80f862d83dbcd57f89517bcf500b2ab88fb2...4907a6ddec9925e35a0a9e82d7399ccc52663121)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 20:33:05 +00:00
dependabot[bot] 972bfb9677 Bump actions/cache from 4 to 5
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 20:33:01 +00:00
dependabot[bot] b1149e4d6e Bump actions/upload-artifact from 4 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 20:32:55 +00:00
dependabot[bot] 3cd2ba847b Bump actions/download-artifact from 4 to 8
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 20:32:49 +00:00
Lauri Ojansivu 8bf869654c Updated ChangeLog. 2026-04-06 05:09:38 +03:00
Lauri Ojansivu 3b98f862bc Fix Admin Panel / Version not showing name of MongoDB storage engine and status of MongoDB Oplog enabled.
Thanks to xet7 !
2026-04-06 05:05:19 +03:00
Lauri Ojansivu 9bc889e274 v8.71 2026-04-06 03:40:47 +03:00
Lauri Ojansivu 5df924f416 Fix: Added missing null check to notification.
Thanks to xet7 !
2026-04-06 03:35:28 +03:00
Lauri Ojansivu 5ef94b7079 v8.70 2026-04-06 02:43:39 +03:00
Lauri Ojansivu dfcaaf0ae7 Convert upsert calls to async in authentication.js .
Thanks to snowsky and xet7 !

Fixes https://github.com/wekan/wekan/pull/6243,
fixes https://github.com/wekan/wekan/pull/6245
2026-04-06 02:36:45 +03:00
Lauri Ojansivu 34e7ff532f v8.69 2026-04-06 01:32:30 +03:00
Lauri Ojansivu fbdfcead50 Try to fix Snap to start MongoDB OpLog correctly.
Thanks to xet7.
2026-04-06 01:28:46 +03:00
Lauri Ojansivu b8bbe4431b v8.68 2026-04-06 00:26:18 +03:00
Lauri Ojansivu 9e31666280 Try to fix Snap to start MongoDB OpLog correctly.
Thanks to xet7 !
2026-04-06 00:21:39 +03:00
Lauri Ojansivu 5846d9c9b0 v8.67 2026-04-05 23:22:53 +03:00
Lauri Ojansivu 7707a5b7fc Try to fix GitHub Actions release bundle builds.
Thanks to xet7.
2026-04-05 23:20:35 +03:00
Lauri Ojansivu 18c495940e v8.65 2026-04-05 23:11:32 +03:00
Lauri Ojansivu d3155d24c6 Try to fix GitHub Actions release bundle builds.
Thanks to xet7.
2026-04-05 23:09:36 +03:00
Lauri Ojansivu c0661506c4 Fix Docker Actions Test suite.
Thanks to xet7 !
2026-04-05 22:56:29 +03:00
Lauri Ojansivu fa2b34764f v8.64 2026-04-05 22:51:36 +03:00
Lauri Ojansivu 7a7909b49f Try to fix GitHub Actions release bundle builds.
Thanks to xet7 !
2026-04-05 22:49:33 +03:00
Lauri Ojansivu 17d3b3088f v8.63 2026-04-05 22:38:05 +03:00
Lauri Ojansivu 6a78a178e2 Try to fix GitHub Actions release bundle builds.
Thanks to xet7 !
2026-04-05 22:35:50 +03:00
Lauri Ojansivu 749de39e2a v8.62 2026-04-05 22:20:33 +03:00
Lauri Ojansivu cd55c0e12e Try to fix GitHub Actions bundle builds.
Thanks to xet7 !
2026-04-05 22:17:12 +03:00
Lauri Ojansivu d04037739e v8.61 2026-04-05 21:58:49 +03:00
Lauri Ojansivu f3a97b9938 Delete all GitHub Actions Caches, so that old stuff does not break builds.
Thanks to xet7 !
2026-04-05 21:55:09 +03:00
Lauri Ojansivu 104fbef37a Reverted disabling GitHub workflows. Still trying to fix them.
Thanks to xet7 !
2026-04-05 21:28:19 +03:00
Lauri Ojansivu 98b3dc71af Updated ChangeLog. 2026-04-05 21:15:52 +03:00
Lauri Ojansivu 7b28e4d589 Merge pull request #6246 from KhaoulaMaleh/fix/comment
Fix: Server crash during comment deletion
2026-04-05 21:14:09 +03:00
KhaoulaMaleh 57291dbe50 fixing deletion comment 2026-04-05 17:51:29 -11:00
Lauri Ojansivu 9f9eefce27 Updated ChangeLog. 2026-04-05 21:13:50 +03:00
Lauri Ojansivu af3361b35b Disabled not working GitHub workflows.
Thanks to xet7 !
2026-04-05 21:12:08 +03:00
Lauri Ojansivu 24d65e1e72 Disabled GitHub workflows that produce broken builds.
Thanks to xet7 !
2026-04-05 21:06:49 +03:00
Lauri Ojansivu 29922fca6a Updated ChangeLog. 2026-04-05 18:46:12 +03:00
Lauri Ojansivu 81319fc48b Updated Docker build script to check directories and repos exist.
Thanks to xet7 !
2026-04-05 18:44:07 +03:00
Lauri Ojansivu 28b7209407 v8.60 2026-04-05 18:14:31 +03:00
Lauri Ojansivu 0b59f87887 Updated Docker build script.
Thanks to xet7 !
2026-04-05 18:10:57 +03:00
Lauri Ojansivu b22df9e93d v8.59 2026-04-05 18:09:10 +03:00
Lauri Ojansivu 1fa72fd34a Fix Snap to use OpLog.
Thanks to xet7 !
2026-04-05 18:05:29 +03:00
Lauri Ojansivu a2f38e7494 Updated ChangeLog. 2026-04-05 17:41:52 +03:00
Lauri Ojansivu 30a1792c92 Fix docker-compose.yml to use OpLog.
Thanks to xet7 !
2026-04-05 17:40:01 +03:00
Lauri Ojansivu 3d70f24a78 v8.58 2026-04-05 16:45:54 +03:00
Lauri Ojansivu b3d14fb5e8 Updated build docs.
Thanks to xet7 !
2026-04-05 16:39:25 +03:00
Lauri Ojansivu 59956ca720 Try to fix .zip Bundle, Docker and Snap.
Thanks to xet7 !
2026-04-05 16:34:58 +03:00
Lauri Ojansivu aaf7690b9a v8.57 2026-04-05 15:18:00 +03:00
Lauri Ojansivu 9f549dfcd2 Try to fix Snap.
Thanks to xet7 !
2026-04-05 15:11:40 +03:00
Lauri Ojansivu f191392e3a v8.56 2026-04-05 07:36:06 +03:00
Lauri Ojansivu 56f26418ea Try to fix Snap.
Thanks to xet7 !
2026-04-05 07:33:22 +03:00
Lauri Ojansivu 118e0e5d5c v8.55 2026-04-05 06:58:46 +03:00
Lauri Ojansivu 264dcca42b Updated ChangeLog. 2026-04-05 06:48:47 +03:00
Lauri Ojansivu 73d69f4631 Updated dependencies.
Thanks to developers of dependencies !
2026-04-05 06:47:10 +03:00
Lauri Ojansivu 485b773338 Updated ChangeLog. 2026-04-05 06:39:23 +03:00
Lauri Ojansivu 8443e3511d Try to fix Snap.
Thanks to xet7 !
2026-04-05 06:38:16 +03:00
Lauri Ojansivu b2a349aec5 Updated ChangeLog. 2026-04-05 06:18:30 +03:00
Lauri Ojansivu faf8453c6d Fix Search All Boards.
Thanks to YosserDerbali and xet7 !

Fixes #6229,
fixes #6225
2026-04-05 06:13:53 +03:00
Lauri Ojansivu 7484c8cce3 Updated ChangeLog. 2026-04-05 01:34:55 +03:00
Lauri Ojansivu 2b02e7712e Fix Cards Reports is not working.
Thanks to titver968 and xet7 !

Fixes #6209
2026-04-05 01:32:04 +03:00
Lauri Ojansivu 8a3a7b19aa Updated ChangeLog. 2026-04-04 23:03:59 +03:00
Lauri Ojansivu 9fa6574c80 Docs: IMPORTANT: Do NOT npm audit fix --force, it breaks builds.
Thanks to xet7 !

Fixes #6224
2026-04-04 23:00:29 +03:00
Lauri Ojansivu 2bad17010c Docs: IMPORTANT: Do NOT npm audit fix --force, it breaks builds. 2026-04-04 22:59:16 +03:00
Lauri Ojansivu 4168385607 Updated ChangeLog. 2026-04-04 22:54:48 +03:00
Lauri Ojansivu c0aa1ef375 v8.54 2026-04-04 18:20:00 +03:00
Lauri Ojansivu 811b79b641 Revert v8.53 Refactor upsert calls to use async method.
Thanks to xet7 !
2026-04-04 18:17:26 +03:00
Lauri Ojansivu dab9755dd4 v8.53 2026-04-04 18:07:57 +03:00
Lauri Ojansivu 6863479c09 Updated ChangeLog. 2026-04-04 18:04:57 +03:00
Lauri Ojansivu e05eebb8b0 Merge pull request #6243 from snowsky/main
Refactor upsert calls to use async method
2026-04-04 18:02:03 +03:00
Hao 28ef0e6e89 Refactor upsert calls to use async method 2026-04-04 08:34:32 -05:00
Lauri Ojansivu 09e65ccac2 v8.52 2026-04-03 20:38:13 +03:00
Lauri Ojansivu 03e0cfdc15 Try to fix Docker and Snap.
Thanks to xet7 !
2026-04-03 20:35:54 +03:00
Lauri Ojansivu 7634cd6c2e v8.51 2026-04-03 20:26:08 +03:00
Lauri Ojansivu 4594711be4 Updated release scripts.
Thanks to xet7 !
2026-04-03 20:21:18 +03:00
Lauri Ojansivu c267933bf7 Updated ChangeLog. 2026-04-03 20:20:03 +03:00
Lauri Ojansivu edc6b18b5f Fix Bug: WeKan v8.50 WRITABLE_PATH.
Thanks to snowsky and xet7 !

Fixes #6242
2026-04-03 20:16:32 +03:00
Lauri Ojansivu 60657c6e94 v8.50 2026-04-03 04:40:19 +03:00
Lauri Ojansivu 3c189f9c7f Updated ChangeLog. 2026-04-03 04:32:15 +03:00
Lauri Ojansivu bf02c9a453 Updated dependencies.
Thanks to developers of dependencies.
2026-04-03 04:23:09 +03:00
Lauri Ojansivu e6b6cd3036 Updated ChangeLog. 2026-04-03 03:43:24 +03:00
Lauri Ojansivu 6e8589b1f9 Updated npm package @wekanteam/exceljs.
Thanks to xet7 !
2026-04-03 03:38:22 +03:00
Lauri Ojansivu 1354f84a70 Updated ChangeLog. 2026-04-03 00:31:11 +03:00
Lauri Ojansivu d07cf92f7c Merge pull request #6240 from snowsky/main
Fix indentation for release-all.yml workflow
2026-04-03 00:28:26 +03:00
Hao Wang e079644ed6 Fix indentation for release-all.yml workflow 2026-04-02 17:16:00 -04:00
Lauri Ojansivu 6b393178dc Updated translations. 2026-04-02 23:10:21 +03:00
Lauri Ojansivu 26e1c9ab69 Updated ChangeLog. 2026-04-01 00:57:36 +03:00
Lauri Ojansivu 145a17fee5 Uncommented build steps. Not yet tested are required dependencies included.
Thanks to xet7 !
2026-04-01 00:55:12 +03:00
Lauri Ojansivu ccc9301ed4 v8.49 2026-03-31 19:21:00 +03:00
Lauri Ojansivu f2e46d660f Updated ChangeLog. 2026-03-31 18:55:50 +03:00
Lauri Ojansivu 8b4676175c Try to fix Snap.
Thanks to xet7 !
2026-03-31 18:54:49 +03:00
Lauri Ojansivu e9a547c89b Updated ChangeLog. 2026-03-31 18:47:01 +03:00
Lauri Ojansivu d07fcceb6a Fix docker-compose.yml.
Thanks to xet7 !
2026-03-31 18:45:54 +03:00
Lauri Ojansivu 6b5002f314 Updated ChangeLog. 2026-03-31 18:21:50 +03:00
Lauri Ojansivu 5418ffd558 Merge pull request #6237 from kovacs-andras/main
fix: missing module '@meteorjs/reify/lib/runtime'
2026-03-31 18:17:38 +03:00
Andras Kovacs ea98afeb68 fix: missing module '@meteorjs/reify/lib/runtime' 2026-03-31 15:09:17 +02:00
Lauri Ojansivu be7d7e26fb v8.48 2026-03-31 05:56:09 +03:00
Lauri Ojansivu 56377aaf4c Updated release scripts. 2026-03-31 05:51:38 +03:00
Lauri Ojansivu 43601c5e3e Updated release scripts. 2026-03-31 05:31:29 +03:00
Lauri Ojansivu 1db7f3a57d v8.47 2026-03-31 05:04:33 +03:00
Lauri Ojansivu e58e99c285 Updated attachments and avatar path to be at WRITABLE_PATH/files/attachments etc.
Thanks to xet7 !
2026-03-31 05:00:17 +03:00
Lauri Ojansivu b38e25c9f1 v8.46 2026-03-31 04:32:57 +03:00
Lauri Ojansivu ce52b8a0ab Updated release scripts. 2026-03-31 04:29:23 +03:00
Lauri Ojansivu f7800846a5 Updated ChangeLog. 2026-03-31 04:00:52 +03:00
Lauri Ojansivu 02b75824e5 Updated docker-compose.yml to change from FerretDB/PostgreSQL to MongoDB.
Thanks to xet7 !
2026-03-31 03:59:07 +03:00
Lauri Ojansivu cf52c2f6e4 Updated ChangeLog. 2026-03-31 03:44:02 +03:00
Lauri Ojansivu 6bc68ba57b Updated release scripts.
Thanks to xet7 !
2026-03-31 03:42:19 +03:00
Lauri Ojansivu 5ec9cfa537 v8.45 2026-03-31 03:23:36 +03:00
Lauri Ojansivu 3457cab0b1 Updated ChangeLog. 2026-03-31 03:20:21 +03:00
Lauri Ojansivu 8349fb6b63 Updated release scripts. Added missing dependencies.
Thanks to xet7 !
2026-03-31 02:56:06 +03:00
Lauri Ojansivu 33267fa2e4 Try to fix Snap 2026-03-31 01:54:28 +03:00
Lauri Ojansivu 5c4130311a Try to fix Snap 2026-03-31 01:45:46 +03:00
Lauri Ojansivu ed718f083a Try to fix Snap 2026-03-31 01:40:40 +03:00
Lauri Ojansivu 2aa8ecfc39 Try to fix Snap 2026-03-31 01:35:24 +03:00
Lauri Ojansivu cdf1d3305b Try to fix Snap 2026-03-31 01:30:47 +03:00
Lauri Ojansivu fd58bb9e4d Try to fix Snap 2026-03-31 01:25:11 +03:00
Lauri Ojansivu 55ec0b5ff5 Try to fix Snap.
Thanks to xet7 !
2026-03-31 01:17:58 +03:00
Lauri Ojansivu e5c551f009 Docker registry Quay.io is currently read only, it's not possible to push newest WeKan Docker image to Quay.io
Thanks to xet7 !
2026-03-31 01:08:09 +03:00
Lauri Ojansivu 7666c6c0e1 Merge pull request #6235 from wekan/dependabot/github_actions/docker/login-action-4
Bump docker/login-action from 3 to 4
2026-03-31 00:58:41 +03:00
Lauri Ojansivu e6ccbfd3b8 Merge pull request #6234 from wekan/dependabot/github_actions/docker/setup-qemu-action-4
Bump docker/setup-qemu-action from 3 to 4
2026-03-31 00:58:23 +03:00
Lauri Ojansivu d8a2e06a95 Merge pull request #6233 from wekan/dependabot/github_actions/docker/build-push-action-7
Bump docker/build-push-action from 6 to 7
2026-03-31 00:58:06 +03:00
Lauri Ojansivu c38fa1c272 Merge pull request #6232 from wekan/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 4 to 6
2026-03-31 00:57:48 +03:00
Lauri Ojansivu 1a5e23617d Merge pull request #6231 from wekan/dependabot/github_actions/actions/setup-node-6
Bump actions/setup-node from 4 to 6
2026-03-31 00:57:22 +03:00
Lauri Ojansivu 87916c20bf Try to fix Snap. Part 2.
Thanks to xet7 !
2026-03-31 00:49:33 +03:00
Lauri Ojansivu 12c223d226 Try to fix Snap.
Thanks to xet7 !
2026-03-31 00:37:53 +03:00
dependabot[bot] 9ae2842fa4 Bump docker/login-action from 3 to 4
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 21:33:06 +00:00
dependabot[bot] 79853a2fb7 Bump docker/setup-qemu-action from 3 to 4
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 21:33:02 +00:00
dependabot[bot] ec1e383558 Bump docker/build-push-action from 6 to 7
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 21:32:59 +00:00
dependabot[bot] df067f01e8 Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 21:32:55 +00:00
dependabot[bot] 4b8cc0683e Bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 21:32:47 +00:00
Lauri Ojansivu f43b76c323 Updated release scripts.
Thanks to xet7 !
2026-03-31 00:07:19 +03:00
Lauri Ojansivu e90b2e384c Add release-all workflow for multi-platform builds
Add a GitHub Actions workflow for releasing all platforms, including version extraction, building bundles for multiple architectures, and creating a GitHub release.
2026-03-30 23:54:05 +03:00
Lauri Ojansivu 6125822cf7 Updated release scripts. 2026-03-30 23:53:23 +03:00
Lauri Ojansivu 4223c2a79f Updated release scripts.
Thanks to xet7 !
2026-03-30 23:39:29 +03:00
Lauri Ojansivu 7059cc244a v8.44 2026-03-30 22:36:22 +03:00
Lauri Ojansivu 5fc608e310 Updated ChangeLog. 2026-03-30 21:43:33 +03:00
Lauri Ojansivu 29550cbd93 Updated release scripts. Part 2.
Thanks to xet7 !
2026-03-30 21:42:24 +03:00
Lauri Ojansivu 35674a205b Updated ChangeLog. 2026-03-30 20:57:13 +03:00
Lauri Ojansivu a702596bee Updated release scripts.
Thanks to xet7 !
2026-03-30 20:53:20 +03:00
Lauri Ojansivu 02fef72e7e Updated ChangeLog. 2026-03-30 20:38:18 +03:00
Lauri Ojansivu d5c9e402ce Fix myCards crash.
Thanks to cristomoya !

Fixes #6202
2026-03-30 20:35:17 +03:00
Lauri Ojansivu b788d398b9 Fix globalSearch.
Thanks to snowsky !

Fixes #6230
2026-03-30 20:29:40 +03:00
Lauri Ojansivu 5c3b73e3b0 Updated ChangeLog. 2026-03-30 19:32:26 +03:00
Lauri Ojansivu 82f1de078f Updated dependencies.
Thanks to developers of dependencies !
2026-03-30 19:30:26 +03:00
Lauri Ojansivu ddc353023c Updated ChangeLog. 2026-03-30 18:54:44 +03:00
Lauri Ojansivu ef75b9ecac Always show all data, like Shared Lists that were before Per-Swimlanes lists, Orphaned Swimlanes/Lists/Cards etc.
Thanks to xet7 !
2026-03-30 18:46:27 +03:00
Lauri Ojansivu cf2b3e4fc9 From FerretDB/PostgreSQL back to MongoDB 7.x.
Thanks to xet7 !
2026-03-30 18:21:05 +03:00
Lauri Ojansivu cf21991a3f Updated translations. 2026-03-30 18:00:08 +03:00
Lauri Ojansivu 9e4f72e2a4 Updated Node.js at Windows docs 2026-03-28 01:04:30 +02:00
Lauri Ojansivu f7da243c9e Updated release scripts. Part 5.
Thanks to xet7 !
2026-03-28 00:49:32 +02:00
Lauri Ojansivu a71efd2668 v8.43 2026-03-28 00:37:33 +02:00
Lauri Ojansivu 6a23290584 Updated ChangeLog. 2026-03-27 23:25:02 +02:00
Lauri Ojansivu 0e59033ea6 Updated release scripts. Part 4.
Thanks to xet7 !
2026-03-27 23:24:02 +02:00
Lauri Ojansivu 186f7a6b82 Updated ChangeLog. 2026-03-27 23:21:41 +02:00
Lauri Ojansivu 5cf3bba68f Updated release scripts. Part 3.
Thanks to xet7 !
2026-03-27 23:19:55 +02:00
Lauri Ojansivu 4647ddc2b7 Updated ChangeLog. 2026-03-27 23:10:51 +02:00
Lauri Ojansivu 1ac708b0d9 Updated ChangeLog. 2026-03-27 22:49:43 +02:00
Lauri Ojansivu 07eb062474 Updated release scripts. Part 2.
Thanks to xet7 !
2026-03-27 22:45:52 +02:00
Lauri Ojansivu f660817627 Updated ChangeLog. 2026-03-27 20:55:15 +02:00
Lauri Ojansivu 0fdf45e847 Updated release scripts.
Thanks to xet7 !
2026-03-27 20:48:39 +02:00
Lauri Ojansivu 0c3cebd30a Merge pull request #6223 from YosserDerbali/fix/list-actions-popup-overflow-scroll
Fix/list actions popup overflow scroll
2026-03-27 20:47:09 +02:00
Yosser Derbali c589918989 Merge upstream/main and resolve popup.css conflict
- Keep upstream's cardDetailsActionsPopup styling
- Combine date picker container styling from both versions
- Ensure both list actions and card details popups work correctly
2026-03-27 19:42:25 +01:00
Yosser Derbali 256aa0a053 fix(popup): keep list actions popup inside viewport and scrollable 2026-03-27 19:17:00 +01:00
Lauri Ojansivu 67c56e41b0 Merge pull request #6222 from KhaoulaMaleh/fix/global-search
fix search: guard async methods in ReactiveCache and search publications
2026-03-27 20:16:52 +02:00
KhaoulaMaleh ade9e0f589 fix search: guard async methods in ReactiveCache and search publications 2026-03-27 18:36:32 -11:00
Lauri Ojansivu e14dfb9e7e Merge pull request #6220 from aminsaidane/fix/card-actions-popup-overflow
Fix card actions popup overflow
2026-03-27 20:15:36 +02:00
Lauri Ojansivu c26cc313fc Updated ChangeLog. 2026-03-27 19:22:45 +02:00
Lauri Ojansivu 55660e0e69 Updated dependencies. Part 2.
Thanks to developers of dependencies !
2026-03-27 19:20:27 +02:00
Lauri Ojansivu 3701544f6a Updated ChangeLog. 2026-03-27 19:04:28 +02:00
Lauri Ojansivu acfb081b90 Updated dependencies.
Thanks to developers of dependencies !
2026-03-27 19:02:42 +02:00
Lauri Ojansivu a30d7cabe3 Updated ChangeLog. 2026-03-27 18:59:12 +02:00
Lauri Ojansivu 517d148387 Moved Meteor 3 migration docs to docs/Upgrade/
Thanks to xet7 !
2026-03-27 18:56:43 +02:00
aminsaidane f8aec4de73 Fix card actions popup overflow
- Added max-height constraint to card details actions popup
- Ensured popup fits within viewport on all screen sizes
- Implemented scrollable content when menu exceeds viewport
- Used min(380px, 85vw) for responsive width sizing
- Prevents menu items from being cut off or hidden

Resolves card pop-up menu content overflow issue
2026-03-27 17:47:00 +01:00
Lauri Ojansivu e1b6cf6124 Updated translations 2026-03-27 18:37:27 +02:00
Lauri Ojansivu 1eac00e7fa Updated ChangeLog. 2026-03-27 18:18:24 +02:00
Lauri Ojansivu 278ed23440 Updated ChangeLog. 2026-03-27 18:15:31 +02:00
Lauri Ojansivu b42689a54d Fix attachment rename fails for clipboard uploads and keep extension metadata in sync.
Thanks to snowsky, YosserDerbali and xet7 !

Fixes #6203,
related https://github.com/wekan/wekan/pull/6217
2026-03-27 18:14:10 +02:00
Lauri Ojansivu 10bfa30458 Merge pull request #6218 from aminsaidane/fix/workspace-settings-modal-overflow
Fix workspace settings modal overflow
2026-03-27 18:09:37 +02:00
aminsaidane a0a1cda0a7 Fix workspace settings modal overflow
- Added top padding to popup content to prevent crowding
- Added max-height constraints for workspace actions popup
- Ensured modal fits within viewport on all screen sizes
- Implemented scrollable content when exceeding viewport
- Fixes issue with partially hidden controls in Workspace Settings modal

Closes #6200
2026-03-27 16:27:14 +01:00
Lauri Ojansivu 001af3fabd Updated translations. 2026-03-26 16:52:06 +02:00
Lauri Ojansivu c0316e9446 Merge pull request #6215 from KhaoulaMaleh/fix/csv-export
Fix: Clarify CSV export separator labels for i18n
2026-03-26 15:53:33 +02:00
KhaoulaMaleh d80a6e7e38 Fix: Clarify CSV export separator labels for i18n 2026-03-26 04:20:21 -11:00
Lauri Ojansivu 8d8d8f9cee Updated ChangeLog. 2026-03-26 01:45:40 +02:00
Lauri Ojansivu 0630d62ad0 Fix Set Color of Swimlane, List and Card.
Thanks to SVANNER, brlin-tw and xet7 !

Fixes #6207,
fixes #6212,
fixes #5660
2026-03-26 01:36:06 +02:00
Lauri Ojansivu c07b282dc9 Updated translations 2026-03-25 23:54:41 +02:00
Lauri Ojansivu 5aa39463b0 Updated ChangeLog. 2026-03-25 23:45:08 +02:00
Lauri Ojansivu f1e71247c7 Updated rebuild-wekan.sh for macOS 26.4.
Thanks to xet7 !
2026-03-25 23:40:47 +02:00
Lauri Ojansivu 419fb2535b Updated ChangeLog. 2026-03-25 21:57:25 +02:00
Lauri Ojansivu 23697d2a33 Fixed typo.
Thanks to YosserDerbali and xet7 !

Related https://github.com/wekan/wekan/pull/6210
2026-03-25 21:54:52 +02:00
Lauri Ojansivu 55edc7ef63 Updated ChangeLog. 2026-03-25 02:28:16 +02:00
Lauri Ojansivu c1e3ce64a7 Removed temporary debug logs.
Thanks to xet7 !
2026-03-25 02:26:52 +02:00
Lauri Ojansivu d34163402c Updated ChangeLog. 2026-03-25 01:40:03 +02:00
Lauri Ojansivu 5c5ed102ca Export Card to Excel. Part 3. Also, fixed uploading attachments.
Thanks to xet7 !

Fixes #5324
2026-03-25 01:37:42 +02:00
Lauri Ojansivu 0bbadfe9a6 Updated ChangeLog. 2026-03-24 21:53:49 +02:00
Lauri Ojansivu 21ab640686 Updated @meteorjs/rspack.
Thanks to Meteor developers!
2026-03-24 21:52:20 +02:00
Lauri Ojansivu a3f6d200ac Updated ChangeLog. 2026-03-24 21:41:58 +02:00
Lauri Ojansivu 701c10d618 Updated @meteorjs/rspack.
Thanks to Meteor developers!
2026-03-24 21:40:00 +02:00
Lauri Ojansivu 7f8de1f988 Updated ChangeLog. 2026-03-24 21:28:01 +02:00
Lauri Ojansivu 95ff8ef1f2 Updated dependencies of npm package @wekanteam/exceljs.
Thanks to xet7 !
2026-03-24 21:26:14 +02:00
Lauri Ojansivu 4eb885f88a Updated translations 2026-03-24 20:59:27 +02:00
Lauri Ojansivu 9a7a0b7271 Updated ChangeLog. 2026-03-24 20:57:26 +02:00
Lauri Ojansivu 4a8293f881 Fixed rebuild-wekan.sh to build WeKan correctly.
Thanks to xet7 !
2026-03-24 20:55:54 +02:00
Lauri Ojansivu 56ed341510 Updated ChangeLog. 2026-03-24 20:44:53 +02:00
Lauri Ojansivu 7584afda6d Upgraded to Meteor 3.4, Node.js 22.22, MongoDB 7.x, here related fixes.
Thanks to harryadel and xet7 !
2026-03-24 20:39:21 +02:00
Harry Adel bbb8f8e51d Merge branch 'main' into meteor-3-migration
# Conflicts:
#	.meteor/packages
#	.meteor/versions
#	models/attachments.js
#	models/boards.js
#	models/server/ExporterExcelCard.js
#	package-lock.json
2026-03-24 02:22:57 +02:00
Harry Adel ddd873c7a0 Fix client runtime regressions after Meteor 3 migration 2026-03-24 01:51:20 +02:00
Harry Adel 63ce45c538 Split shared models from Meteor 3 server runtime 2026-03-24 01:50:39 +02:00
Harry Adel 8ffc0cb5ff Centralize client boot and rspack package loading 2026-03-24 01:50:25 +02:00
Lauri Ojansivu ce95907778 v8.42 2026-03-23 20:07:35 +02:00
Lauri Ojansivu 4ed3250352 Updated ChangeLog. 2026-03-23 20:04:28 +02:00
Lauri Ojansivu 2dce7a84cd Fix SSO login is broken.
Thanks to sbaecker, vladimirdulov and xet7 !

Fixes #6204
2026-03-23 20:00:26 +02:00
Lauri Ojansivu b9631659f7 Updated version. 2026-03-23 18:36:04 +02:00
Lauri Ojansivu 405cf3efa3 v8.41 2026-03-23 18:34:52 +02:00
Lauri Ojansivu 462b3801ee Updated translations. 2026-03-23 18:31:51 +02:00
Lauri Ojansivu 7ec42880f0 Updated ChangeLog. 2026-03-23 18:29:17 +02:00
Lauri Ojansivu 3d6797a305 Fixed Board lost its member list or permissions.
Thanks to marylinereumont, AymenHassini19, muabila and xet7 !

Fixes #6198
2026-03-23 18:25:38 +02:00
Lauri Ojansivu 065fe35b6a Updated translations. 2026-03-23 11:39:51 +02:00
Lauri Ojansivu 4a4e4135ab Updated ChangeLog. 2026-03-23 11:28:22 +02:00
Lauri Ojansivu e388d18aad Merge pull request #6199 from KhaoulaMaleh/fix/cards
Fix: prevent cards from disappearing after dragging in List view
2026-03-23 11:25:00 +02:00
KhaoulaMaleh 60d9698343 Fix: prevent cards from disappearing after dragging in List view 2026-03-23 03:44:28 -11:00
Lauri Ojansivu a7c9f7aa17 Updated ChangeLog. 2026-03-22 22:17:51 +02:00
Lauri Ojansivu 7de1b04ebc Export Card to Excel. Part 2.
Thanks to xet7 !

Fixes #5324
2026-03-22 22:15:33 +02:00
Lauri Ojansivu 024ec23b7f Updated ChangeLog. 2026-03-22 19:47:08 +02:00
Lauri Ojansivu 7b2128b356 Fixed 2nd new card should not have text of 1st card, it should be empty.
Thanks to xet7 !
2026-03-22 19:41:44 +02:00
Lauri Ojansivu 6d54b826e8 Merge pull request #6197 from harryadel/use-email-sendAsync
Replace Email.send with Email.sendAsync
2026-03-22 19:12:47 +02:00
Lauri Ojansivu 30c4010512 Merge pull request #6196 from harryadel/remove-meteorhacks-aggregate
Remove unused meteorhacks:aggregate for Meteor 3.0 migration
2026-03-22 19:11:40 +02:00
Lauri Ojansivu c4c8446fc2 Merge pull request #6195 from harryadel/migrate-communitypackages
Replace useraccounts packages with communitypackages alternatives
2026-03-22 19:10:55 +02:00
Lauri Ojansivu c374c87c08 Updated translations 2026-03-22 19:09:21 +02:00
Harry Adel 8486bb1e87 Fix load order: two-stage entry points with require() bootstrap
ES module import statements are hoisted and evaluated in dependency-graph
order, not textual order. This means `import '/imports/collectionHelpers'`
being first in the file does NOT guarantee it runs first.

Fix: Split entry points into two stages:
- main.js: uses require() only (sequential, not hoisted)
  1. require('/imports/collectionHelpers') — loads collection2, SimpleSchema
  2. require('./imports') — loads all app code
- imports.js: contains all the import statements (safe because
  collectionHelpers has already fully loaded via require())

This pattern guarantees attachSchema/SimpleSchema/helpers are available
before ANY model file evaluates, regardless of circular dependency chains.
2026-03-22 19:03:11 +02:00
Harry Adel 655f44919a Fix attachSchema: convert collectionHelpers to pure require()
The root cause: import statements are hoisted and trigger evaluation
chains that reach model files (via reactiveCache circular deps)
BEFORE collection2 has patched Mongo.Collection.prototype.attachSchema.

Fix: Convert collectionHelpers.js to use ONLY require() calls.
require() is NOT hoisted — each call completes synchronously before
the next, guaranteeing collection2 fully loads before any model
file can run.
2026-03-22 18:59:09 +02:00
Harry Adel 17ea7978f0 Fix attachSchema: require collection2 directly in settings.js
settings.js is loaded early via circular dependency chain before
the entry point's hoisted imports finish. Add explicit require()
for aldeed:collection2 to ensure attachSchema is available.
2026-03-22 18:56:37 +02:00
Harry Adel b518946c41 Centralize collection2/SimpleSchema setup in collectionHelpers shim
Move all collection infrastructure into imports/collectionHelpers.js:
- Import aldeed:collection2 (provides attachSchema on Mongo.Collection)
- Import and set SimpleSchema as global
- Register denyUpdate/denyInsert as allowed schema extensions
- Provide Collection.helpers() shim

This ensures collection2 is loaded before any model file uses
attachSchema, regardless of circular dependency import ordering.
2026-03-22 18:54:01 +02:00
Harry Adel ca8a4af354 Fix SimpleSchema v2: add missing parent object declarations
SimpleSchema v2.0 requires explicit parent object declarations for
dot-notation sub-keys:
- settings.js: Add mailServer as Object parent for mailServer.* sub-keys
- watchable.js: Add watchers as [Object] parent for watchers.$.* sub-keys
2026-03-22 18:51:30 +02:00
Harry Adel 1682f8834d Fix denyUpdate/denyInsert: register as allowed SimpleSchema extensions
aldeed:simple-schema v2.0 no longer includes denyUpdate/denyInsert as
built-in properties. Register them via SimpleSchema.extendOptions() in:
- settings.js (first model loaded via circular dep chain)
- client/main.js and server/main.js (for all other models)
2026-03-22 18:42:53 +02:00
Harry Adel eac472c54b Fix SimpleSchema: use default import (not named export)
aldeed:simple-schema v2.0 uses `export default SimpleSchema`, not
a named export. Fix all import/require sites:
- settings.js: require().default
- attachmentStorageSettings.js: import default
- client/main.js, server/main.js: import default for global
2026-03-22 18:41:31 +02:00
Harry Adel 3a873559f8 Fix settings.js: use require() for ALL deps to prevent import hoisting
ES module import statements are hoisted above all code, so placing
Settings = new Mongo.Collection() between imports has no effect.
Convert ALL imports to require() so the collection definition
truly executes first, breaking the circular dependency.
2026-03-22 18:36:32 +02:00
Harry Adel aec6779601 Fix SimpleSchema not defined: import explicitly + set global
- settings.js: Import SimpleSchema explicitly from aldeed:simple-schema
  before attachSchema call (breaks circular dep ordering)
- client/main.js, server/main.js: Set SimpleSchema as global for the
  29 other model files that use it without importing
2026-03-22 18:30:38 +02:00
Harry Adel 973d9eccae Fix runtime errors: circular dependency in settings.js, fibers in users.js
- settings.js: Break circular import (settings → ReactiveCache → i18n →
  translation → ReactiveCache → settings) by defining Settings collection
  before require() calls instead of after import statements
- users.js: Make Users.after.insert hook async for template container
  creation (already committed fibers/future removal)
- fileStoreStrategy.js: Remove duplicate sanitizeFilename (already committed)
- wekanCreator.js: Fix forEach→for...of for await (already committed)
2026-03-22 18:27:41 +02:00
Harry Adel 7468b30001 Fix build errors: fibers/future, duplicate sanitizeFilename, forEach await
- users.js: Replace fibers/future pattern with async/await using
  insertAsync/updateAsync for template container creation
- fileStoreStrategy.js: Remove duplicate sanitizeFilename declaration
- wekanCreator.js: Convert attachments.forEach to for...of loop
  so await works inside the iteration body
2026-03-22 18:24:07 +02:00
Harry Adel e68cb5f32f Meteor 3.4 migration: Flip release to METEOR@3.4
Update .meteor/release from METEOR@2.16 to METEOR@3.4.
This activates Node 22, Express 5, async collection APIs,
MongoDB driver 6.x, and the Rspack bundler integration.
2026-03-22 17:58:34 +02:00
Harry Adel a157ae5d1c Meteor 3.4 migration: Module restructuring + Rspack setup
Create explicit entry points required by Meteor 3.4's Rspack bundler:

- client/main.js: imports all client code (models, components, jade
  templates, libraries, config, startup)
- server/main.js: imports all server code (models, methods, publications,
  routes, migrations, notifications, cron, auth)
- rspack.config.js: configures meteor-jade-loader for .jade files
- package.json: adds meteor.mainModule pointing to entry files
- .gitignore: adds Rspack build artifact directories
2026-03-22 17:58:04 +02:00
Harry Adel 620e5ce9dc Meteor 3.4 migration: Convert remaining sync methods to async
Convert all remaining Meteor.methods with sync collection calls:
- users.js: deleteWorkspace → async with findOneAsync/updateAsync
- userPositionHistory.js: 3 methods → async with insertAsync/fetchAsync
- attachmentStorageSettings.js: 4 methods → findOneAsync/insertAsync/upsertAsync
- org.js, team.js, translation.js: insert → insertAsync
- boards.js: acceptInvite + collection hooks → updateAsync
- settings.js: sendInvitation → updateAsync/insertAsync
- import.js: importBoard/cloneBoard → async with await creator.create()
- attachments.js: onAfterUpload → async with findOneAsync/updateAsync
2026-03-22 17:50:31 +02:00
Harry Adel 2e3fbf25db Meteor 3.4 migration: Convert allow/deny callbacks to async
Meteor 3.x supports async allow/deny callbacks. Convert all 25 sync
allow/deny callbacks across 23 model files to use async/await with
findOneAsync/fetchAsync instead of sync findOne/find().fetch().

Also converts helper functions in server/lib/utils.js:
- allowIsBoardMemberWithWriteAccessByCard -> async
- allowIsBoardMemberByCard -> async
- canUpdateCard (cards.js) -> async
- permissionHelper.allow (integrations.js) -> async
2026-03-22 17:44:29 +02:00
Harry Adel e8fee653c3 Meteor 3.4 migration: Phases 0-3
Phase 0 - Pre-migration cleanup:
- Remove msavin:usercache (unused), es5-shim, http@2.0.0, fibers
- Replace Meteor.wrapAsync in counters.js, exporter.js, attachmentUrlValidation.js
- Replace meteorhacks:subs-manager with Meteor.subscribe in 6 client files

Phase 1 - Create meteor-jade-loader:
- Custom Rspack/webpack loader for .jade templates
- Extracts mquandalle:jade-compiler pipeline (lexer, parser, transpiler)
- Bundles Meteor vendor packages (htmljs, html-tools, blaze-tools, spacebars-compiler)

Phase 2 - Package version bumps & replacements:
- Remove mquandalle:jade, dburles:collection-helpers, aldeed:simple-schema
- Remove communitypackages:picker (Meteor 3 bundles Express)
- Replace simple:json-routes -> communitypackages:json-routes
- Replace simple:rest-accounts-password -> communitypackages:rest-accounts-password
- Add collection-helpers shim at imports/collectionHelpers.js
- Convert Picker.route() to WebApp.handlers.get() in export files + sandstorm.js
- Add api.versionsFrom(['2.16', '3.0']) to all 9 custom packages
- Remove version pins from .meteor/packages

Phase 3 - Express 5 migration:
- Rename WebApp.connectHandlers -> WebApp.handlers (20 occurrences)
- Rename WebApp.rawConnectHandlers -> WebApp.rawHandlers (5 occurrences)
- Convert regexp route patterns to Express 5 named params in attachmentApi.js, avatarServer.js
2026-03-22 17:17:48 +02:00
Harry Adel 0d3f3474d1 Merge remote-tracking branch 'fork/use-email-sendAsync' into meteor-3-migration 2026-03-22 15:07:38 +02:00
Harry Adel 6ecb6bdaa7 Merge remote-tracking branch 'fork/remove-meteorhacks-aggregate' into meteor-3-migration 2026-03-22 15:07:33 +02:00
Harry Adel 738655d380 Use Email.sendAsync in SMTP test and update docs
Replace Email.send with await Email.sendAsync in the SMTP test email
method and update the documentation example to match.
2026-03-22 03:21:11 +02:00
Harry Adel 3f0eb227ec Await email calls in notifications and rules
Add missing await to EmailLocalization.sendEmail() calls and replace
fallback Email.send with Email.sendAsync in the notification email
handler and rules helper. The notification handler runs inside an
async setTimeout callback where the missing await meant email errors
were unhandled promise rejections instead of being caught.
2026-03-22 03:20:51 +02:00
Harry Adel 409a624e33 Await email calls in invitation code
Add missing await to EmailLocalization.sendEmail() calls and replace
fallback Email.send with Email.sendAsync in board invitation and
registration invitation flows. Without await, promise rejections from
failed email sends were silently swallowed instead of being caught
by the surrounding try/catch blocks.
2026-03-22 03:20:26 +02:00
Harry Adel 1b02184482 Use Email.sendAsync in email localization wrapper
Email.sendAsync is available since Meteor 2.16 (email@2.2.5).
The synchronous Email.send is just a wrapper around it via
Promise.await. Switch to the async version directly since
sendEmail() is already an async function.
2026-03-22 03:19:54 +02:00
Harry AdelandClaude Opus 4.6 f88e6671a2 Remove unused meteorhacks:aggregate package for Meteor 3.0 migration
The only aggregation usage (metrics.js) already calls rawCollection().aggregate()
directly — the native MongoDB driver method — not the monkey-patched method from
this package. Convert to async/await (replacing Promise.await) for Meteor 3.0
Fibers removal compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 03:02:09 +02:00
Harry Adel 073c297490 Remove unused meteorhacks:aggregate package for Meteor 3.0 migration
The only aggregation usage (metrics.js) already calls rawCollection().aggregate()
directly — the native MongoDB driver method — not the monkey-patched method from
this package. Convert to async/await (replacing Promise.await) for Meteor 3.0
Fibers removal compatibility.
2026-03-22 03:02:09 +02:00
Harry Adel e92578d41d Remove dead useraccounts-core reference from stacksmith build
The sed command targeting packages/meteor-useraccounts-core/package.js
referenced a local package directory that no longer exists.
2026-03-22 02:43:16 +02:00
Harry Adel 0644aeb70e Replace useraccounts packages with communitypackages alternatives
Migrate from the unmaintained useraccounts:* packages to the
community-maintained communitypackages:* drop-in replacements:

- useraccounts:core -> communitypackages:core@2.0.3
- useraccounts:unstyled -> communitypackages:unstyled@2.0.1
- useraccounts:flow-routing-extra -> communitypackages:flow-routing@2.1.0

Also clean up .meteor/packages:
- Remove unused coffeescript and spacebars packages
- Unpin ecmascript and standard-minifier-js versions
2026-03-22 02:43:10 +02:00
Lauri Ojansivu 456bda1169 v8.40 2026-03-21 21:28:34 +02:00
Lauri Ojansivu 4b83a3f861 Updated translations 2026-03-21 21:23:49 +02:00
Lauri Ojansivu 7e18526ed1 Updated translations 2026-03-21 21:14:28 +02:00
Lauri Ojansivu ffef908c8d Updated ChangeLog. 2026-03-21 21:02:03 +02:00
Lauri Ojansivu be5bdbe314 Export Card to Excel and related fixes. Part 1.
Thanks to xet7 !

Fixes #5324
2026-03-21 20:57:07 +02:00
Lauri Ojansivu 6d0f869309 v8.39 2026-03-21 15:35:20 +02:00
Lauri Ojansivu bfd0f9d0e4 Updated translations 2026-03-21 15:32:05 +02:00
Lauri Ojansivu ee9b5a0d25 Updated ChangeLog. 2026-03-21 15:25:48 +02:00
Lauri Ojansivu 445746fd7f Updated ChangeLog. 2026-03-21 15:23:28 +02:00
Lauri Ojansivu ac33d8012b Delete workspace. Part 2.
Thanks to xet7 !

Fixes #6157
2026-03-21 15:18:02 +02:00
Lauri Ojansivu 8513b2e456 Delete workspace etc. In Progress.
Thanks to xet7 !

Related #6157
2026-03-21 11:32:56 +02:00
Lauri Ojansivu 26d393f8ee Fix open lock from all button position at Admin Panel/People/People.
Thanks to xet7 !
2026-03-21 10:33:06 +02:00
Lauri Ojansivu bee322cb2e v8.38 2026-03-21 02:28:01 +02:00
Lauri Ojansivu bf209bfdd7 Updated ChangeLog. 2026-03-21 00:12:24 +02:00
Lauri Ojansivu 0e410b6d7a Fix List of users is empty, some translations not visible, count of orgs/teams/people, theme of some buttons, New org/team to be leftmost column.
Thanks to xet7 !

Fixes #6194
2026-03-21 00:11:08 +02:00
Lauri Ojansivu 2e4e3b1a06 Updated ChangeLog. 2026-03-20 18:15:56 +02:00
Lauri Ojansivu 7ed26f437d Updated CONTRIBUTING.md 2026-03-20 18:07:33 +02:00
Lauri Ojansivu e50b9f211d Updated CONTRIBUTING.md 2026-03-20 18:05:05 +02:00
Lauri Ojansivu e705df6634 Updated CONTRIBUTING.md 2026-03-20 18:01:38 +02:00
Lauri Ojansivu e669a9fe64 Updated CONTRIBUTING.md 2026-03-20 17:57:41 +02:00
Lauri Ojansivu 6249af041c Updated CONTRIBUTING.md 2026-03-20 17:53:23 +02:00
Lauri Ojansivu a4a4fc1cf2 Merge branch 'main' of github.com:wekan/wekan 2026-03-20 17:51:45 +02:00
Lauri Ojansivu 6acc7c95ff Updated CONTRIBUTING.md and SECURITY.md 2026-03-20 17:50:38 +02:00
Lauri Ojansivu 3d75e905d1 v8.37 2026-03-20 15:03:36 +02:00
Lauri Ojansivu 55bc0ed411 Updated ChangeLog. 2026-03-20 15:00:07 +02:00
Lauri Ojansivu 37142f3bb9 Fix card save does not work.
Thanks to comunidadplayasdetijuana, Janfy and xet7 !

Fixes #6192
2026-03-20 14:56:59 +02:00
Lauri Ojansivu c11aa2a194 Updated translations. 2026-03-20 07:47:40 +02:00
Lauri Ojansivu 9da7adad95 v8.36 2026-03-20 00:40:50 +02:00
Lauri Ojansivu 69277d4b24 Updates 2026-03-20 00:36:05 +02:00
Lauri Ojansivu 2e157501c3 Updated ChangeLog. 2026-03-20 00:12:40 +02:00
Lauri Ojansivu 610849ab0b Add remaining features of Copy and Move of Swimlanes, Lists and Cards.
Thanks to xet7 !
2026-03-20 00:10:13 +02:00
Lauri Ojansivu b966607070 Updated ChangeLog. 2026-03-19 22:51:46 +02:00
Lauri Ojansivu d0c44bded8 Fix Copy Card and Move Card.
Thanks to BlobbyBob and xet7 !

Fixes #6169
2026-03-19 22:49:22 +02:00
Lauri Ojansivu c79f5be07a Updated translations. 2026-03-19 22:48:10 +02:00
Lauri Ojansivu baab46a3fb Updated ChangeLog. 2026-03-19 22:08:18 +02:00
Lauri Ojansivu 92beaa3137 Fix RebindBleed of IntegrationBleed.
Thanks to Rodolphe GHIO and xet7 !
2026-03-19 22:07:07 +02:00
Lauri Ojansivu f03d0ddf30 Updated ChangeLog. 2026-03-19 21:32:35 +02:00
Lauri Ojansivu 4dea702ff2 Fixed adding label and select color causes color not checkmarked and wrong color choosed.
Thanks to xet7 !
2026-03-19 21:29:09 +02:00
Lauri Ojansivu 7e55b83a7b Updated translations. 2026-03-19 21:22:54 +02:00
Lauri Ojansivu 259f3776d2 Updated ChangeLog. 2026-03-19 21:18:41 +02:00
Lauri Ojansivu 2f07eb7097 Fixed add remove checklist and checklist item.
Thanks to xet7 !
2026-03-19 21:17:14 +02:00
Lauri Ojansivu 6d6247d449 Updated ChangeLog. 2026-03-19 20:24:02 +02:00
Lauri Ojansivu c3f66cce55 Updated flatted.
Thanks to developers of flatted !
2026-03-19 20:22:51 +02:00
Lauri Ojansivu 8ee178dd89 Updated ChangeLog. 2026-03-19 20:16:05 +02:00
Lauri Ojansivu d01d7fbcc9 Merge pull request #6191 from harryadel/remove-raix-handlebar-helpers
Remove raix:handlebar-helpers, replace with native Blaze helpers
2026-03-19 20:14:57 +02:00
Harry Adel 252b95f661 Remove raix:handlebar-helpers, replace with native Blaze helpers
Replace the raix:handlebar-helpers package with inline Blaze helper
registrations for operator helpers ($eq, $neq, $gt, $lt, $gte, $lte,
$and, $or, $not, $in, $nin, $). This removes an external dependency
while preserving the same template helper API.

Also removes the unused ui@1.0.13 package pin.
2026-03-19 18:08:02 +02:00
Lauri Ojansivu 89f262feae Updated translations 2026-03-19 17:19:23 +02:00
Lauri Ojansivu 7167f53270 Updated ChangeLog. 2026-03-19 17:16:52 +02:00
Lauri Ojansivu c989c21d4f Fixed view, restore and delete of Archived Cards, Lists and Swimlanes. If delete list, it does not delete card of that list that was archived before archive list.
Thanks to xet7 !
2026-03-19 17:12:44 +02:00
Lauri Ojansivu 5d83d4c239 Updated translations. 2026-03-19 09:52:35 +02:00
Lauri Ojansivu 94089f9505 Updated ChangeLog. 2026-03-19 09:42:59 +02:00
Lauri Ojansivu 872dd578c8 Fixed Add after list to add after selected list. Added Confirm to Archive List and Archive Swimlane.
Thanks to xet7 !
2026-03-19 09:40:47 +02:00
Lauri Ojansivu fba00535fa Updated ChangeLog. 2026-03-19 08:10:15 +02:00
Lauri Ojansivu ccd23f6cde Fixed crash at sign-up page, when registering new user, happened after some PR.
Thanks to xet7 !
2026-03-19 08:07:42 +02:00
Lauri Ojansivu 7a1db4e040 Updated ChangeLog. 2026-03-19 07:56:48 +02:00
Lauri Ojansivu 7d0a422714 Removed not needed input fields from GitHub Bug report issue template.
Thanks to xet7 !
2026-03-19 07:55:01 +02:00
Lauri Ojansivu e411a4fd8e Updated ChangeLog. 2026-03-19 07:49:19 +02:00
Lauri Ojansivu 77b57d8233 Merge pull request #6190 from harryadel/remove-underscore
Replace underscore with native JavaScript utilities
2026-03-19 07:48:09 +02:00
Lauri Ojansivu d14005a183 Updated ChangeLog. 2026-03-19 07:47:44 +02:00
Lauri Ojansivu 0c29fa3cbe Fixed crash when IFTTT Rules archived card, but cards etc are not visible at archive yet. Fixed "Add After List" to show lists only from current Swimlane.
Thanks to xet7 !
2026-03-19 07:44:34 +02:00
Harry Adel 60bc1d55f4 Remove underscore from .meteor/packages
All application code now uses native JavaScript equivalents.
underscore remains as a transitive dependency of
raix:handlebar-helpers in .meteor/versions.
2026-03-18 22:39:34 +02:00
Harry Adel bcadc517c3 Remove underscore dependency from wekan-ldap package
- Remove import _ from 'underscore'
- _.reduce -> .reduce()
- _.map on objects -> Object.entries().forEach() (argument order swap)
- _.isEmpty -> manual null/length check
- _.isObject -> typeof check
- _.find -> .includes()
- _.last -> .at(-1)
- _.size -> Object.keys().length
- _.debounce -> inline debounce function
- Remove api.use('underscore') from package.js
2026-03-18 22:39:29 +02:00
Harry Adel eb4cb341c6 Remove underscore dependency from wekan-oidc package
- _.pick -> Object.fromEntries/Object.entries filter
- _.extend -> Object.assign (including on Error objects)
- _.pluck -> .map()
- Remove api.use('underscore') from package.js
2026-03-18 22:39:20 +02:00
Harry Adel 331bc2d1b9 Replace underscore with native JS in server and shared files
- server/publications: _.findWhere -> findWhere() helper
- server/notifications: _.clone -> spread operator
- server/rulesHelper: _.min/_.max -> Math.min/Math.max
- server/lib/utils: _.some/_.contains -> .some()/.includes()
- imports/reactiveCache: _.groupBy/_.indexBy -> helpers
- config/router: _.each on object -> Object.entries().forEach()
- sandstorm: _.pluck/_.values -> .map()/Object.values()
2026-03-18 22:39:14 +02:00
Harry Adel b5782c7b86 Replace underscore with native JS in models
Largest changes in cards.js (~76 calls), boards.js (~38 calls),
and activities.js (~31 calls). Key patterns:

- _.contains -> .includes() with null guards where needed
- _.findWhere/_.where -> findWhere()/where() helpers
- _.pluck -> .map()
- _.union -> [...new Set()]
- _.extend -> Object.assign()
- _.each on objects -> Object.entries().forEach() (argument order swap)
- _.debounce -> debounce() helper
- _.groupBy/_.indexBy -> groupBy()/indexBy() helpers
- _.clone -> spread operator
- _.sortBy -> [...arr].sort()
- Type checks -> native typeof
2026-03-18 22:39:06 +02:00
Harry Adel 01bb5a930d Replace underscore with native JS in client components
- _.contains -> .includes()
- _.filter -> .filter()
- _.pluck -> .map()
- _.findWhere -> findWhere() helper
- _.union -> [...new Set()]
- _.every/.some -> .every()/.some()
- _.once -> once() helper
- _.uniq with property -> uniqBy() helper
- _.sortBy -> [...arr].sort()
- _.chain().groupBy() -> groupBy() helper
- _.each -> .forEach()
- _.isNumber -> typeof x === 'number'
2026-03-18 22:38:56 +02:00
Harry Adel 1bb657b073 Replace underscore with native JS in client utility files
- _.has -> Object.hasOwn
- _.isString -> typeof x === 'string'
- _.isUndefined -> x === undefined
- _.sortBy -> [...arr].sort()
- _.any -> .some()
- _.range -> Array.from()
- _.times -> for loop
2026-03-18 22:38:44 +02:00
Harry Adel 740f39e17c Add native JS collection helpers to replace underscore utilities
Add findWhere, where, uniqBy, groupBy, indexBy, debounce, and once
helper functions in imports/lib/collectionHelpers.js. These provide
null-safe replacements for underscore methods that have no clean
native one-liner equivalent. Expose them on the Utils global for
client-side usage.
2026-03-18 22:38:37 +02:00
Lauri Ojansivu e34e4b8404 Updated ChangeLog. 2026-03-18 22:07:29 +02:00
Lauri Ojansivu e6fbfa87f0 Fix more Rules buttons styling.
Thanks to xet7 !
2026-03-18 22:06:25 +02:00
Lauri Ojansivu a31de9e1a0 Updated ChangeLog. 2026-03-18 22:02:13 +02:00
Lauri Ojansivu a3ac219503 Changed colors of Add Rule and Back buttons from black to theme color.
Thanks to xet7 !
2026-03-18 22:00:10 +02:00
Lauri Ojansivu 5af4a5395b Updated ChangeLog. 2026-03-18 21:42:29 +02:00
Lauri Ojansivu d2168aca3d Removed empty space at top of list header.
Thanks to xet7 !
2026-03-18 21:41:08 +02:00
Lauri Ojansivu 6148d96b9a Updated ChangeLog. 2026-03-18 21:20:41 +02:00
Lauri Ojansivu b6a6180dc3 Fix can not close opened card.
Thanks to xet7 !
2026-03-18 21:18:58 +02:00
Lauri Ojansivu 6edd3c77ec Updated ChangeLog. 2026-03-18 20:57:38 +02:00
Lauri Ojansivu 36741d7ae5 Fix Error reading default file .well-known/assetlinks.json.default.
Thanks to gre3x and xet7 !

Fixes #6189
2026-03-18 20:55:44 +02:00
Lauri Ojansivu 507b196101 Updated ChangeLog. 2026-03-18 20:24:15 +02:00
Lauri Ojansivu 6fc1cb9127 Fix List drag handle missing.
Thanks to FK-PATZ3 and xet7 !

Fixes #6185
2026-03-18 20:22:25 +02:00
Lauri Ojansivu b3ae3871f2 Updated ChangeLog. 2026-03-18 18:38:00 +02:00
Lauri Ojansivu 1f6b5ed0e3 Fix version not visible.
Thanks to FK-PATZ3 and xet7 !

Related #6185
2026-03-18 18:36:12 +02:00
Lauri Ojansivu ab5fe6411a Updated ChangeLog. 2026-03-18 18:07:10 +02:00
Lauri Ojansivu 5dd3209ee8 Updated ChangeLog. 2026-03-18 18:04:50 +02:00
Lauri Ojansivu fccefcaa6c Updated to MongoDB 7.0.31 at Snap Candidate.
Thanks to MongoDB developers !
2026-03-18 18:03:11 +02:00
Lauri Ojansivu 98249dda8e Updated translations. 2026-03-18 18:02:47 +02:00
Lauri Ojansivu 26732d4f6e Updated ChangeLog. 2026-03-16 03:07:22 +02:00
Lauri Ojansivu 9a6319772e Merge pull request #6187 from KhaoulaMaleh/bugfix/6165-card-search-results
Fix #6165: Improve global search relevance and add exact-field operators
2026-03-16 02:58:27 +02:00
Lauri Ojansivu 2547967113 Merge pull request #6188 from harryadel/fix/list-add-card-and-sort-persistence
Fix list add-card actions and sort persistence
2026-03-16 02:56:04 +02:00
Harry Adel f436525f20 Fix list add-card actions and sort persistence 2026-03-16 01:49:53 +02:00
KhaoulaMaleh 7b3f7b8e6b fix search relevance 2026-03-15 04:56:25 -11:00
Lauri Ojansivu a3dece58fb Updated ChangeLog. 2026-03-14 18:58:41 +02:00
Lauri Ojansivu a159856628 Updated flatted.
Thanks to developers of flatted !
2026-03-14 18:57:43 +02:00
Lauri Ojansivu 7fb6e2a5d9 Added docs about If you get "Board not Found" after archieving Template Board.
Thanks to marcuskki and xet7 !

Related #4632
2026-03-13 20:53:17 +02:00
Lauri Ojansivu 0cdae9def1 Updated ChangeLog. 2026-03-13 20:52:55 +02:00
Lauri Ojansivu 63df6cb197 Updated dependencies.
Thanks to developers of dependencies !
2026-03-13 20:51:44 +02:00
Lauri Ojansivu 55d2d0843a Updated ChangeLog. 2026-03-12 04:49:38 +02:00
Lauri Ojansivu 7e378be1d8 Fix Add List popup to not open after adding new board or there being no lists at all.
Thanks to xet7 !
2026-03-12 04:39:51 +02:00
Lauri Ojansivu 89f86caf69 Replaced incompatible file-type with mime-type.
Thanks to xet7 !
2026-03-12 04:29:49 +02:00
Lauri Ojansivu 9604598a79 Updates 2026-03-12 04:12:21 +02:00
Lauri Ojansivu 90d9805abf Updated ChangeLog. 2026-03-12 04:12:21 +02:00
Lauri Ojansivu 0deb7ab641 Updated file-type.
Thanks to developers of file-type.
2026-03-12 04:12:21 +02:00
Lauri Ojansivu 230f954571 Updated ChangeLog. 2026-03-12 04:12:21 +02:00
dependabot[bot] e3f739e5d9 Bump docker/login-action from 3.7.0 to 4.0.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.7.0 to 4.0.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/c94ce9fb468520275223c153574b00df6fe4bcc9...b45d80f862d83dbcd57f89517bcf500b2ab88fb2)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-12 04:12:21 +02:00
dependabot[bot] 6b75bfcb0c Bump docker/metadata-action from 5.10.0 to 6.0.0
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.10.0 to 6.0.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/c299e40c65443455700f0fdfc63efafe5b349051...030e881283bb7a6894de51c315a6bfe6a94e05cf)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-12 04:12:21 +02:00
dependabot[bot] db47ed5430 Bump docker/build-push-action from 6.19.2 to 7.0.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.19.2 to 7.0.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/10e90e3645eae34f1e60eeb005ba3a3d33f178e8...d08e5c354a6adb9ed34480a06d141179aa583294)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-12 04:12:21 +02:00
KhaoulaMaleh 48361b2047 fixed linked card swimlane routing 2026-03-12 04:12:21 +02:00
Harry Adel 6953cc0670 Update PDF export models
Clean up exportPDF and ExporterCardPDF server-side code to align
with the BlazeComponent removal changes.
2026-03-08 11:13:17 +02:00
Harry Adel 477e1c89e5 Migrate rules, activities, and remaining components to Template
Convert all remaining BlazeComponent-based components to native Meteor
Template pattern: activities, comments, all rules actions/triggers,
swimlanes, users, gantt, import, and main utility components.
2026-03-08 11:13:17 +02:00
Harry Adel bae23f9ed8 Migrate sidebar and settings components from BlazeComponent to Template
Convert sidebar, sidebarArchives, sidebarCustomFields, sidebarFilters,
sidebarSearches, and all settings panel components to use native Meteor
Template.onCreated/helpers/events pattern.
2026-03-08 11:04:53 +02:00
Harry Adel d3625db755 Migrate card components from BlazeComponent to Template
Convert cardDetails, cardCustomFields, cardDate, cardTime,
cardDescription, attachments, checklists, labels, minicard,
resultCard, and subtasks to use native Meteor Template pattern.
2026-03-08 11:04:53 +02:00
Harry Adel d9e2e8f97e Migrate board and list components from BlazeComponent to Template
Convert boardBody, boardHeader, boardsList, boardArchive,
originalPositionsView, list, listBody, and listHeader to use
native Meteor Template.onCreated/helpers/events pattern.
2026-03-08 11:04:53 +02:00
Harry Adel f1625ad1f5 Migrate client library code from BlazeComponent to Template pattern
Convert popup, inlinedform, multiSelection, spinner, cardSearch,
datepicker, and dialog helper libraries to use native Meteor
Template.onCreated/helpers/events instead of BlazeComponent.
Update reactiveCache to remove BlazeComponent dependency.
2026-03-08 11:04:53 +02:00
Harry Adel 012947c076 Convert HTML templates to Jade and update template markup
Replace Spacebars .html templates with Jade equivalents for
originalPositionsView and originalPosition. Add new Jade template
for inlinedCardDescription. Update existing Jade templates to
reflect the BlazeComponent-to-Template migration changes.
2026-03-08 11:04:53 +02:00
Harry Adel 3b6f6fa80a Replace BlazeComponent mixins with standalone utility modules
Delete the old Mixins global and BlazeComponent-based InfiniteScrolling
mixin. Add standalone replacements:
- client/lib/infiniteScrolling.js: plain scroll-based pagination helper
- client/lib/currentCard.js: centralized current card resolution
- client/components/forms/datepicker.js: extracted date picker logic
2026-03-08 11:04:53 +02:00
Harry Adel 2848b1a38a Remove peerlibrary:blaze-components package and clean up config
Remove the blaze-components Meteor package and its peer dependencies
from .meteor/packages and .meteor/versions. Clean up related ESLint
globals (BlazeComponent, Mixins, InlinedForm, ReactiveTabs).
2026-03-08 11:04:53 +02:00
Lauri Ojansivu 53a0beffee Updates 2026-03-06 17:21:52 +02:00
Lauri Ojansivu 4cb99c6014 Updates 2026-03-06 17:16:50 +02:00
Lauri Ojansivu e485168169 Updates 2026-03-06 17:03:44 +02:00
Lauri Ojansivu 44a083e095 Updates 2026-03-06 17:00:40 +02:00
Lauri Ojansivu 1bd3363708 Updated ChangeLog. 2026-03-06 01:04:05 +02:00
973 changed files with 97740 additions and 53109 deletions

No files matched your search

+1 -1
View File
@@ -251,7 +251,7 @@ gosu wekan:wekan /home/wekan/.meteor/meteor -- help
# Build app (Development)
cd /home/wekan/app
gosu wekan:wekan /home/wekan/.meteor/meteor add standard-minifier-js
gosu wekan:wekan /home/wekan/.meteor/meteor npm install
gosu wekan:wekan /home/wekan/.meteor/meteor npm install --no-audit
# Put back the original tar
mv $(which tar)~ $(which tar)
+1 -7
View File
@@ -93,10 +93,8 @@
"Template": false,
"$": false,
"_": false,
"autosize": false,
"Avatar": true,
"Avatars": true,
"BlazeComponent": false,
"CollectionHooks": false,
"ESSearchResults": false,
@@ -112,8 +110,7 @@
"Ps": true,
"Restivus": false,
"SimpleSchema": false,
"SubsManager": false,
"T9n": false,
"T9n": false,
"TAPi18n": false,
"AccountsTemplates": true,
"Activities": true,
@@ -130,13 +127,11 @@
"CSSEvents": true,
"EscapeActions": true,
"Filter": true,
"Mixins": true,
"Modal": true,
"MultiSelection": true,
"Popup": true,
"Sidebar": true,
"Utils": true,
"InlinedForm": true,
"UnsavedEdits": true,
"Notifications": true,
"allowIsBoardAdmin": true,
@@ -149,7 +144,6 @@
"Checklists": true,
"Settings": true,
"InvitationCodes": true,
"JsonRoutes": true,
"Authentication": true,
"Integrations": true,
"HTTP": true,
-27
View File
@@ -13,32 +13,6 @@ body:
description: A clear and concise description of what the bug is. Mention versions of WeKan, Node.js, database name and version, frontend webserver version like Caddy etc.
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform / Installation Method
options:
- Snap Stable
- Snap Candidate
- Docker
- Sandstorm
- Source (Meteor)
- Windows
- Mac
- Other
validations:
required: true
- type: dropdown
id: CPU
attributes:
label: CPU
options:
- amd64
- arm64
- s390x
- Other
validations:
required: true
- type: textarea
id: reproduction
attributes:
@@ -66,4 +40,3 @@ body:
attributes:
label: Additional Context
description: Add any other context, anonymized screenshots or GIF video about the bug, and screenshot mockups about how it should work.
+3 -3
View File
@@ -38,7 +38,7 @@ jobs:
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -48,14 +48,14 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
+481
View File
@@ -0,0 +1,481 @@
name: Release All Platforms
# Triggered when maintainer pushes a version tag, e.g.: git tag v8.43 && git push --tags
# Does not work:
# Required GitHub Secrets:
# QUAY_USERNAME - Quay.io username
# QUAY_PASSWORD - Quay.io password or token
# DOCKERHUB_USERNAME - Docker Hub username
# DOCKERHUB_TOKEN - Docker Hub access token
# SNAP_STORE_LOGIN - Snapcraft store credentials: run 'snapcraft export-login --snaps wekan --channels stable -' and base64-encode the output
on:
push:
tags:
- 'v*.*'
permissions:
contents: write
packages: write
jobs:
# ─── 1. Extract version and release notes ───────────────────────────────────
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
changelog: ${{ steps.changelog.outputs.content }}
steps:
- uses: actions/checkout@v6
- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
- name: Extract release notes from CHANGELOG.md
id: changelog
run: |
VERSION="${{ steps.version.outputs.version }}"
# Try to find the version-specific section first, then fall back to Upcoming
NOTES=$(python3 - <<PYEOF
import re, sys
version = "$VERSION"
with open("CHANGELOG.md", encoding="utf-8") as f:
content = f.read()
# Pattern: section heading through (but not including) the next heading
patterns = [
rf"(# v{re.escape(version)} .*?)(?=\n# v[0-9]|\Z)",
r"(# Upcoming WeKan.*?)(?=\n# v[0-9]|\Z)",
]
for pat in patterns:
m = re.search(pat, content, re.DOTALL)
if m:
print(m.group(1).strip())
sys.exit(0)
print(f"WeKan v{version}")
PYEOF
)
echo "content<<GHEOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "GHEOF" >> $GITHUB_OUTPUT
# ─── 2. Build amd64 bundle (full Meteor 3 build) ────────────────────────────
# Meteor 3.5-beta.7 installer supports only: Linux x86_64, Linux aarch64, macOS, Windows.
# It explicitly rejects armhf, s390x, and ppc64le at install time.
# Therefore the full Meteor build always runs here on x86_64.
# All other Linux architectures then rebuild only the native npm modules
# from this bundle (much faster than a full Meteor build).
# Node.js version must match what Meteor 3.5-beta.7 bundles: 24.14.1 (ABI 143).
build-amd64:
needs: prepare
runs-on: ubuntu-24.04
timeout-minutes: 180
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- uses: actions/checkout@v6
- name: Cache Meteor installation
uses: actions/cache@v5
with:
path: ~/.meteor
key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
restore-keys: meteor-
- name: Cache npm packages
uses: actions/cache@v5
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential g++ make python3 curl wget zip unzip
# Pin to Node 24.14.1 — the exact version bundled by Meteor 3.5-beta.7.
# Using the same version for build and npm rebuild ensures consistent native module ABI.
- uses: actions/setup-node@v6
with:
node-version: '24.14.1'
- name: Install Meteor
run: |
if [ ! -f ~/.meteor/meteor ]; then
curl https://install.meteor.com/ | sh
fi
echo "$HOME/.meteor" >> $GITHUB_PATH
- name: Build WeKan bundle
run: |
rm -rf .build node_modules .meteor/local/bundler-cache
meteor npm install
meteor build .build --directory
- name: Install dependencies in bundle
run: |
cd .build/bundle/programs/server
meteor npm install
- name: Create amd64 zip
run: |
cd .build
zip -r wekan-${VERSION}-amd64.zip bundle
- uses: actions/upload-artifact@v7
with:
name: bundle-amd64
path: .build/wekan-${{ needs.prepare.outputs.version }}-amd64.zip
retention-days: 1
# ─── 3. Build arm64 bundle (rebuild native modules natively) ─────────────────
# Meteor 3.5-beta.7 does support Linux aarch64, but running a full Meteor build a
# second time would double the build time for the same result. npm rebuild on
# the native arm64 GitHub Actions runner is much faster and produces the same
# platform-specific native modules.
build-arm64:
needs: [prepare, build-amd64]
runs-on: ubuntu-24.04-arm
timeout-minutes: 60
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential g++ make python3 curl unzip zip
- uses: actions/setup-node@v6
with:
node-version: '24.14.1'
- name: Download amd64 bundle
uses: actions/download-artifact@v8
with:
name: bundle-amd64
- name: Rebuild native modules for arm64
run: |
unzip wekan-${VERSION}-amd64.zip
cd bundle/programs/server
rm -rf node_modules
npm install
cd ../..
- name: Clean up and create arm64 zip
run: |
zip -r wekan-${VERSION}-arm64.zip bundle
- uses: actions/upload-artifact@v7
with:
name: bundle-arm64
path: wekan-${{ needs.prepare.outputs.version }}-arm64.zip
retention-days: 1
# ─── 4. Build s390x bundle (rebuild native modules via QEMU container) ───────
# Meteor 3.5-beta.7 does NOT support s390x (installer exits with error on that arch).
# GitHub Actions has no native s390x runners.
# Solution: QEMU emulation + node:24.14.1-slim (s390x variant) for npm rebuild.
# node:24.14.1-slim is pinned to match Meteor 3.5-beta.7's bundled Node.js version exactly.
build-s390x:
needs: [prepare, build-amd64]
runs-on: ubuntu-latest
timeout-minutes: 90
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- name: Set up QEMU for s390x
uses: docker/setup-qemu-action@v4
with:
platforms: s390x
- name: Install unzip/zip
run: sudo apt-get update && sudo apt-get install -y unzip zip
- name: Download amd64 bundle
uses: actions/download-artifact@v8
with:
name: bundle-amd64
- name: Rebuild native modules for s390x in container
run: |
unzip wekan-${VERSION}-amd64.zip
rm -rf bundle/programs/server/node_modules
# node:24.14.1-slim s390x — same Node.js version as Meteor 3.5-beta.7 bundled
docker run --rm --platform linux/s390x \
-v "$(pwd)/bundle:/bundle" \
-w /bundle/programs/server \
node:24.14.1-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
- name: Clean up and create s390x zip
run: |
zip -r wekan-${VERSION}-s390x.zip bundle
- uses: actions/upload-artifact@v7
with:
name: bundle-s390x
path: wekan-${{ needs.prepare.outputs.version }}-s390x.zip
retention-days: 1
# ─── 4b. Build ppc64le bundle (rebuild native modules via QEMU container) ────
# Meteor 3.5-beta.7 does NOT support ppc64le (installer exits with error on that arch).
# GitHub Actions has no native ppc64le runners.
# Solution: QEMU emulation + node:24.14.1-slim (ppc64le variant) for npm rebuild.
build-ppc64le:
needs: [prepare, build-amd64]
runs-on: ubuntu-latest
timeout-minutes: 90
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- name: Set up QEMU for ppc64le
uses: docker/setup-qemu-action@v4
with:
platforms: ppc64le
- name: Install unzip/zip
run: sudo apt-get update && sudo apt-get install -y unzip zip
- name: Download amd64 bundle
uses: actions/download-artifact@v8
with:
name: bundle-amd64
- name: Rebuild native modules for ppc64le in container
run: |
unzip wekan-${VERSION}-amd64.zip
rm -rf bundle/programs/server/node_modules
# node:24.14.1-slim ppc64le — same Node.js version as Meteor 3.5-beta.7 bundled
docker run --rm --platform linux/ppc64le \
-v "$(pwd)/bundle:/bundle" \
-w /bundle/programs/server \
node:24.14.1-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
- name: Clean up and create ppc64le zip
run: |
zip -r wekan-${VERSION}-ppc64le.zip bundle
- uses: actions/upload-artifact@v7
with:
name: bundle-ppc64le
path: wekan-${{ needs.prepare.outputs.version }}-ppc64le.zip
retention-days: 1
# ─── 4c. Build armhf bundle (rebuild native modules via QEMU container) ──────
# Meteor 3.5-beta.7 does NOT support arm/v7 (armhf). Installer explicitly rejects it.
# GitHub Actions has no native arm/v7 runners.
# Solution: QEMU emulation + node:24.14.1-slim (arm/v7 variant) for npm rebuild.
# arm/v7 under QEMU is slowest — timeout set to 120 min.
build-armhf:
needs: [prepare, build-amd64]
runs-on: ubuntu-latest
timeout-minutes: 120
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- name: Set up QEMU for arm/v7
uses: docker/setup-qemu-action@v4
with:
platforms: arm
- name: Install unzip/zip
run: sudo apt-get update && sudo apt-get install -y unzip zip
- name: Download amd64 bundle
uses: actions/download-artifact@v8
with:
name: bundle-amd64
- name: Rebuild native modules for armhf in container
run: |
unzip wekan-${VERSION}-amd64.zip
rm -rf bundle/programs/server/node_modules
# node:24.14.1-slim arm/v7 — same Node.js version as Meteor 3.5-beta.7 bundled
docker run --rm --platform linux/arm/v7 \
-v "$(pwd)/bundle:/bundle" \
-w /bundle/programs/server \
node:24.14.1-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
- name: Clean up and create armhf zip
run: |
zip -r wekan-${VERSION}-armhf.zip bundle
- uses: actions/upload-artifact@v7
with:
name: bundle-armhf
path: wekan-${{ needs.prepare.outputs.version }}-armhf.zip
retention-days: 1
# ─── 5. Build win64 bundle (rebuild native modules on Windows) ───────────────
build-win64:
needs: [prepare, build-amd64]
runs-on: windows-latest
timeout-minutes: 60
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- uses: actions/setup-node@v6
with:
node-version: '24.14.1'
## With Node 24 these are already preinstalled at GitHub Windows runners
#- name: Install node-gyp and build tools
# run: |
# npm install -g windows-build-tools --vs2019 2>$null; exit 0
- name: Install 7-zip
run: choco install -y 7zip
- name: Download amd64 bundle
uses: actions/download-artifact@v8
with:
name: bundle-amd64
- name: Rebuild native modules for Windows
shell: cmd
run: |
7z x wekan-%VERSION%-amd64.zip
cd bundle\programs\server
rmdir /s /q node_modules
npm install
cd ..\..\..
- name: Create win64 zip
shell: cmd
run: 7z a wekan-%VERSION%-win64.zip bundle
- uses: actions/upload-artifact@v7
with:
name: bundle-win64
path: wekan-${{ needs.prepare.outputs.version }}-win64.zip
retention-days: 1
# ─── 6. Create GitHub Release and upload all bundles ────────────────────────
release:
needs: [prepare, build-amd64, build-arm64, build-s390x, build-ppc64le, build-armhf, build-win64]
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- name: Download all bundles
uses: actions/download-artifact@v8
with:
pattern: bundle-*
merge-multiple: true
- name: Create GitHub Release with all bundles
uses: softprops/action-gh-release@v2
with:
name: v${{ needs.prepare.outputs.version }}
body: ${{ needs.prepare.outputs.changelog }}
files: |
wekan-${{ needs.prepare.outputs.version }}-amd64.zip
wekan-${{ needs.prepare.outputs.version }}-arm64.zip
wekan-${{ needs.prepare.outputs.version }}-s390x.zip
wekan-${{ needs.prepare.outputs.version }}-ppc64le.zip
wekan-${{ needs.prepare.outputs.version }}-armhf.zip
wekan-${{ needs.prepare.outputs.version }}-win64.zip
# ─── 7. Build and push multi-platform Docker image ──────────────────────────
#docker:
# needs: [prepare, release]
# runs-on: ubuntu-latest
# env:
# VERSION: ${{ needs.prepare.outputs.version }}
# steps:
# - uses: actions/checkout@v6
#
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v4
#
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v3
#
# - name: Log in to Docker Hub
# uses: docker/login-action@v4
# with:
# username: ${{ secrets.DOCKERHUB_USERNAME }}
# password: ${{ secrets.DOCKERHUB_TOKEN }}
#
# - name: Log in to Quay.io
# uses: docker/login-action@v4
# with:
# registry: quay.io
# username: ${{ secrets.QUAY_USERNAME }}
# password: ${{ secrets.QUAY_PASSWORD }}
#
# - name: Log in to GitHub Container Registry
# uses: docker/login-action@v4
# with:
# registry: ghcr.io
# username: ${{ github.actor }}
# password: ${{ secrets.GITHUB_TOKEN }}
#
# - name: Build and push Docker image to all registries
# uses: docker/build-push-action@v7
# with:
# context: .
# platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/ppc64le,linux/s390x
# push: true
# build-args: VERSION=${{ needs.prepare.outputs.version }}
# tags: |
# #wekanteam/wekan:v${{ needs.prepare.outputs.version }}
# #wekanteam/wekan:latest
# quay.io/wekan/wekan:v${{ needs.prepare.outputs.version }}
# quay.io/wekan/wekan:latest
# ghcr.io/wekan/wekan:v${{ needs.prepare.outputs.version }}
# ghcr.io/wekan/wekan:latest
# cache-from: type=gha
# cache-to: type=gha,mode=max
# ─── 8. Build and publish snap packages (all architectures via Launchpad) ────
# Uses snapcraft remote-build on Launchpad's native builders (more reliable
# than QEMU for snap builds). Each architecture bundles its own database:
# amd64, arm64 — MongoDB 7.x prebuilt native binary
# armhf, s390x, ppc64el — MongoDB 7.x amd64 binary run via qemu-x86_64-static
# (no prebuilt MongoDB binaries exist for these arches)
#snap:
# needs: [prepare, release]
# runs-on: ubuntu-latest
# env:
# VERSION: ${{ needs.prepare.outputs.version }}
# steps:
# - uses: actions/checkout@v6
#
# - name: Install snapcraft
# run: sudo snap install snapcraft --classic
#
# - name: Remote-build snap for all architectures via Launchpad
# env:
# SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_STORE_LOGIN }}
# run: |
# snapcraft remote-build \
# --build-for=amd64,arm64,armhf,s390x,ppc64el \
# --launchpad-accept-public-upload
#
# - name: Upload all snap files as artifacts
# uses: actions/upload-artifact@v7
# with:
# name: snaps-all-arches
# path: "*.snap"
# retention-days: 30
#
# # Publish to edge — NOT stable.
# # Stable channel holds the old WeKan version until database migrations are ready.
# # Promote to stable manually via the Snap Store dashboard when migrations exist.
# - name: Publish all snaps to Snap Store (candidate, beta, edge)
# env:
# SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_STORE_LOGIN }}
# run: |
# for snap_file in *.snap; do
# echo "Publishing $snap_file ..."
# snapcraft upload --release=edge "$snap_file"
# done
#
# - name: Upload all snaps to GitHub Release
# uses: softprops/action-gh-release@v2
# with:
# files: "*.snap"
+7 -2
View File
@@ -124,13 +124,18 @@ jobs:
restore-keys: |
v1-meteor_build_cache-
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24.14.1'
- name: Setup meteor
uses: meteorengineer/setup-meteor@v3
with:
meteor-release: '2.2'
meteor-release: '3.5-beta.7'
- name: Install NPM Dependencies
run: meteor npm ci
run: meteor npm install
- name: Run Tests
run: sh ./test-wekan.sh -cv
+14
View File
@@ -35,7 +35,21 @@ ehthumbs.db
.devcontainer/volumes*/
.coverage
# Rspack build artifacts
_build/
public/build-assets/
public/build-chunks/
private/build-assets/
private/build-chunks/
public/client-rspack.js.map
# Helm chart
# Chart dependencies
/helm/wekan/**/*.tgz
/helm/wekan/charts
# Meteor Modern-Tools build context directories
_build
*/build-assets
*/build-chunks
.rsdoctor
+38 -51
View File
@@ -3,28 +3,36 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
meteor-base@1.5.1
meteor-base@1.5.2
# Build system
ecmascript@0.16.8
standard-minifier-js@2.8.1
mquandalle:jade
coffeescript@2.4.1!
ecmascript@0.17.0
standard-minifier-js@3.2.0
rspack@1.0.0
# Polyfills
es5-shim@4.8.0
blaze@3.0.2
# Collections
aldeed:collection2
aldeed:schema-index@4.0.0-beta.5
reywood:publish-composite
dburles:collection-helpers
mongo@1.16.8
mongo@2.3.0-beta350.7
# The holy trifecta - this is a replacement for subs-manager
# for IndexedDB persistence and auto-sync
jam:offline
# for optimistic methods with offline queuing
jam:method
# global subscription caching
jam:pub-sub
# Account system
accounts-password@2.4.0
useraccounts:core
useraccounts:unstyled
simple:rest-accounts-password
accounts-password@3.2.3-beta350.7
communitypackages:core
communitypackages:unstyled
communitypackages:flow-routing
# Routing
ostrio:flow-router-extra
wekan-ldap
wekan-accounts-cas
wekan-accounts-sandstorm
@@ -33,50 +41,29 @@ wekan-oidc
wekan-accounts-oidc
# Utilities
check@1.3.2
jquery@3.0.0!
random@1.2.1
reactive-dict@1.3.1
session@1.2.1
tracker@1.3.3
underscore@1.0.13
audit-argument-checks@1.0.7
raix:handlebar-helpers
http@2.0.0! # force new http package
# Datepicker (disabled - using native HTML inputs)
# wekan-bootstrap-datepicker
check@1.5.0
random@1.2.2
reactive-dict@1.3.2
session@1.2.2
tracker@1.3.4
audit-argument-checks@1.0.8
# UI components
ostrio:i18n
reactive-var@1.0.12
meteor-autosize
shell-server@0.5.0
email@2.2.5
dynamic-import@0.7.3
msavin:usercache
# Keep stylus in 1.1.0, because building v2 takes extra 52 minutes.
meteorhacks:subs-manager
meteorhacks:aggregate@1.3.0
reactive-var@1.0.13
shell-server@0.7.0
email@3.2.0-beta350.7
dynamic-import@0.7.4
wekan-markdown
quave:synced-cron
ostrio:cookies
ostrio:files@2.3.0
lmieulet:meteor-coverage
meteortesting:mocha@2.0.3
aldeed:simple-schema
ostrio:files
# lmieulet:meteor-coverage
meteortesting:mocha
matb33:collection-hooks
simple:json-routes
spacebars
service-configuration@1.3.2
communitypackages:picker
minifier-css@1.6.4
blaze
peerlibrary:blaze-components
ejson@1.1.3
logging@1.3.3
service-configuration@1.3.5
minifier-css@2.0.1
ejson@1.2.0-beta350.7
logging@1.3.6
wekan-fullcalendar
wekan-fontawesome
useraccounts:flow-routing-extra
ostrio:flow-router-extra
+1 -1
View File
@@ -1 +1 @@
METEOR@2.16
METEOR@3.5-beta.7
+105 -130
View File
@@ -1,139 +1,114 @@
accounts-base@2.2.11
accounts-oauth@1.4.4
accounts-password@2.4.0
aldeed:collection2@2.10.0
aldeed:collection2-core@1.2.0
aldeed:schema-deny@1.1.0
aldeed:schema-index@1.1.1
aldeed:simple-schema@1.5.4
allow-deny@1.1.1
audit-argument-checks@1.0.7
autoupdate@1.8.0
babel-compiler@7.10.5
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
blaze@2.7.1
blaze-tools@1.1.3
boilerplate-generator@1.7.2
caching-compiler@1.2.2
caching-html-compiler@1.2.1
callback-hook@1.5.1
check@1.4.1
coffeescript@2.7.0
coffeescript-compiler@2.4.1
communitypackages:picker@1.1.1
dburles:collection-helpers@1.1.0
ddp@1.4.1
ddp-client@2.6.2
ddp-common@1.4.1
ddp-rate-limiter@1.2.1
ddp-server@2.7.1
deps@1.0.12
diff-sequence@1.1.2
dynamic-import@0.7.3
ecmascript@0.16.8
ecmascript-runtime@0.8.1
ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0
ejson@1.1.3
email@2.2.6
es5-shim@4.8.0
fetch@0.1.4
geojson-utils@1.0.11
hot-code-push@1.0.4
html-tools@1.1.3
htmljs@1.1.1
http@2.0.0
id-map@1.1.1
inter-process-messaging@0.1.1
jquery@3.0.0
lmieulet:meteor-coverage@1.1.4
localstorage@1.2.0
logging@1.3.4
matb33:collection-hooks@1.3.0
mdg:validation-error@0.5.1
meteor@1.11.5
meteor-autosize@5.0.1
meteor-base@1.5.1
meteorhacks:aggregate@1.3.0
meteorhacks:collection-utils@1.2.0
meteorhacks:picker@1.0.3
meteorhacks:subs-manager@1.6.4
accounts-base@3.3.0-beta350.7
accounts-oauth@1.4.6
accounts-password@3.2.3-beta350.7
aldeed:collection2@4.1.5
aldeed:schema-index@4.0.0
aldeed:simple-schema@1.13.1
allow-deny@2.1.0
audit-argument-checks@1.0.8
autoupdate@2.0.1
babel-compiler@7.13.0
babel-runtime@1.5.2
base64@1.0.13
binary-heap@1.0.12
blaze@3.0.2
blaze-tools@2.0.0
boilerplate-generator@2.1.0
caching-compiler@2.0.1
caching-html-compiler@2.0.0
callback-hook@1.6.1
check@1.5.0
communitypackages:core@2.0.3
communitypackages:flow-routing@2.1.0
communitypackages:unstyled@2.0.1
core-runtime@1.0.0
ddp@1.4.2
ddp-client@3.2.0-beta350.7
ddp-common@1.4.4
ddp-rate-limiter@1.3.0-beta350.7
ddp-server@3.2.0-beta350.7
diff-sequence@1.1.3
dynamic-import@0.7.4
ecmascript@0.17.0
ecmascript-runtime@0.8.3
ecmascript-runtime-client@0.12.3
ecmascript-runtime-server@0.11.1
ejson@1.2.0-beta350.7
email@3.2.0-beta350.7
es5-shim@4.8.1
facts-base@1.0.2
fetch@0.1.6
geojson-utils@1.0.12
hot-code-push@1.0.5
html-tools@2.0.0
htmljs@2.0.1
http@1.4.4
id-map@1.2.0
inter-process-messaging@0.1.2
jam:method@1.9.1
jam:offline@0.4.1
jam:pub-sub@0.5.1
jquery@3.0.2
lai:collection-extensions@1.0.0
localstorage@1.2.1
logging@1.3.6
matb33:collection-hooks@2.1.0
meteor@2.2.0
meteor-base@1.5.2
meteortesting:browser-tests@1.4.2
meteortesting:mocha@2.1.0
meteortesting:mocha-core@8.0.1
minifier-css@1.6.4
minifier-js@2.8.0
minifiers@1.1.8-faster-rebuild.0
minimongo@1.9.4
modern-browsers@0.1.10
modules@0.20.0
modules-runtime@0.13.1
mongo@1.16.10
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
mongo-livedata@1.0.12
mquandalle:jade@0.4.9
mquandalle:jade-compiler@0.4.5
msavin:usercache@1.8.0
npm-mongo@4.17.2
oauth@2.2.1
oauth2@1.3.2
observe-sequence@1.0.21
ordered-dict@1.1.0
ostrio:cookies@2.7.2
minifier-css@2.0.1
minifier-js@3.1.0
minimongo@2.1.0-beta350.7
modern-browsers@0.2.3
modules@0.20.3
modules-runtime@0.13.2
mongo@2.3.0-beta350.7
mongo-decimal@0.2.0
mongo-dev-server@1.1.1
mongo-id@1.0.9
npm-mongo@6.16.1
oauth@3.0.2
oauth2@1.3.3
observe-sequence@2.0.0
ordered-dict@1.2.0
ostrio:cookies@2.9.1
ostrio:cstorage@4.0.1
ostrio:files@2.3.3
ostrio:flow-router-extra@3.10.1
ostrio:files@3.0.1
ostrio:flow-router-extra@3.12.1
ostrio:i18n@3.2.1
peerlibrary:assert@0.3.0
peerlibrary:base-component@0.17.1
peerlibrary:blaze-components@0.23.0
peerlibrary:computed-field@0.10.0
peerlibrary:data-lookup@0.3.0
peerlibrary:reactive-field@0.6.0
promise@0.12.2
quave:synced-cron@2.2.1
raix:eventemitter@0.1.3
raix:handlebar-helpers@0.2.5
random@1.2.1
rate-limit@1.1.1
react-fast-refresh@0.2.8
reactive-dict@1.3.1
reactive-var@1.0.12
reload@1.3.1
retry@1.1.0
promise@1.0.0
quave:synced-cron@2.4.0
raix:eventemitter@1.0.0
random@1.2.2
rate-limit@1.2.0-beta350.7
react-fast-refresh@0.3.0
reactive-dict@1.3.2
reactive-var@1.0.13
reload@1.3.2
retry@1.1.1
reywood:publish-composite@1.9.0
routepolicy@1.1.1
service-configuration@1.3.4
session@1.2.1
sha@1.0.9
shell-server@0.5.0
simple:authenticate-user-by-token@1.2.1
simple:json-routes@2.3.1
simple:rest-accounts-password@1.2.2
simple:rest-bearer-token-parser@1.1.1
simple:rest-json-error-handler@1.1.1
socket-stream-client@0.5.2
spacebars@1.4.1
spacebars-compiler@1.3.1
standard-minifier-js@2.8.1
templating@1.4.1
templating-compiler@1.4.1
templating-runtime@1.5.0
templating-tools@1.2.2
tracker@1.3.3
typescript@4.9.5
ui@1.0.13
underscore@1.6.1
url@1.3.2
useraccounts:core@1.16.2
useraccounts:flow-routing-extra@1.1.0
useraccounts:unstyled@1.14.2
webapp@1.13.8
webapp-hashing@1.1.1
routepolicy@1.1.2
rspack@1.0.0
service-configuration@1.3.5
session@1.2.2
sha@1.0.10
shell-server@0.7.0
socket-stream-client@0.7.0-beta350.7
spacebars@2.0.0
spacebars-compiler@2.0.0
standard-minifier-js@3.2.0
templating@1.4.4
templating-compiler@2.0.0
templating-runtime@2.0.1
templating-tools@2.0.0
tools-core@1.0.0
tracker@1.3.4
typescript@5.9.3
url@1.3.5
webapp@2.2.0-beta350.7
webapp-hashing@1.1.2
wekan-accounts-cas@0.2.0
wekan-accounts-lockout@1.1.0
wekan-accounts-oidc@1.0.10
+688 -34
View File
@@ -2,6 +2,7 @@
Newest WeKan at these platforms:
- [Upgrade WeKan](https://wekan.fi/upgrade/)
- [bundle zip at releases](https://github.com/wekan/wekan/releases), docs at
- [Linux amd64, arm64, s390x](https://github.com/wekan/wekan/blob/main/docs/Platforms/FOSS/RaspberryPi/Raspberry-Pi.md)
- [Windows amd64, works also at Windows arm64](https://github.com/wekan/wekan/blob/main/docs/Platforms/Propietary/Windows/Offline.md)
@@ -11,30 +12,683 @@ Newest WeKan at these platforms:
- Docker amd64/arm64/s390x
- Kubernetes Docker amd64
- Bitnami MongoDB Docker images do not exist anymore. [MongoDump/MongoRestore to groundhog2k MongoDB images](https://github.com/wekan/charts/issues/45)
- Fixing other platforms In Progress.
Fixing other platforms In Progress.
Versions:
- [Node.js 14.21.4](https://github.com/wekan/node-v14-esm/releases/tag/v14.21.4) or [Node.js 14.21.3](https://nodejs.org/dist/latest-v14.x/)
- MongoDB 6.x and 7.x, or [FerretDB2/PostgreSQL](https://github.com/wekan/wekan/blob/main/docs/Databases/FerretDB2-PostgreSQL.md)
- WeKan 8.75 and newer uses Meteor 3.5-beta.7, [Node.js 24.x](https://nodejs.org/dist/latest-v24.x/),
MongoDB 7.x (or 6.x), or [FerretDB2/PostgreSQL](https://github.com/wekan/wekan/blob/main/docs/Databases/FerretDB2-PostgreSQL.md)
- WeKan 8.43 upgraded to Meteor 3.x, huge thanks to harryadel:
- https://harryadel.com/dev-diary-24/
- https://harryadel.com/dev-diary-25/
- https://harryadel.com/dev-diary-26/
- WeKan 8.00-8.24 used Colorful Unicode Emoji Icons, versions before and after use mostly Font Awesome 4.7 icons.
- WeKan 8.00-8.06 had wrong raw database directory setting /var/snap/wekan/common/wekan and some cards were not visible,
it was fixed at WeKan 8.07 where database directory is back to /var/snap/wekan/common and all cards are visible.
[Upgrade WeKan](https://wekan.fi/upgrade/)
# v8.78 2026-04-09 WeKan ® release
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.
This release adds the the following updates:
WeKan 8.00-8.24 used Colorful Unicode Emoji Icons, versions before and after use mostly Font Awesome 4.7 icons.
- Upgraded to Meteor 3.5-beta.7 and Node.js 24.14.1. Now prefers MongoDB Change Streams at Snap Candidate and docker-compose.yml.
[Part 1](https://github.com/wekan/wekan/commit/14f9ac39ad93db4f7c06721f1efd4c18e53cc5f8),
[Part 2](https://github.com/wekan/wekan/commit/b1707b8691d0ba5f355e78e250765c8a7657c3d7).
Thanks to Meteor developers.
- [Enhance translations with new key:value pairs](https://github.com/wekan/wekan/pull/6259).
Thanks to KhaoulaMaleh.
- [Refine translations](https://github.com/wekan/wekan/pull/6261).
Thanks to KhaoulaMaleh.
- [Updated to Node.js 24.x at release scripts](https://github.com/wekan/wekan/commit/bdb92744854cc211866631746c63d748d4a93cfa).
Thanks to xet7.
- [Fix GitHub Actions builds](https://github.com/wekan/wekan/commit/bc88c0dfc2742154e94861201d239bc4b3c51cef).
Thanks to xet7.
Upgrading to Meteor 3 progress:
and fixes the following bugs:
- https://harryadel.com/dev-diary-24/
- https://www.linkedin.com/posts/harryadel_dev-diary-24-wekan-migration-journey-activity-7426683776547811328-jS9h/
- https://forums.meteor.com/t/anyone-still-using-meteor-2/64402/4
- [Fixed Card settings checkboxes](https://github.com/wekan/wekan/pull/6260).
Thanks to KhaoulaMaleh.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.74 2026-04-08 WeKan ® release
This release fixes the following bugs:
- [Fix can not save Card Data. Part 2](https://github.com/wekan/wekan/commit/0328e70d5f4a277722635e3fd9ea78312d1359cf).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.73 2026-04-07 WeKan ® release
This release fixes the following bugs:
- [Fix Link to Windows release doesn't work](https://github.com/wekan/wekan/commit/9c695302565073b971a3521c0a94a380641cc436).
Thanks to hmeunier95 and xet7.
- [Fix 8.72 Bug: Can not save Card Date](https://github.com/wekan/wekan/commit/8da0f34ee6ca66884ab8a2dcdf1a724a0c9fcc81).
Thanks to xet7.
- [Removed exclude-archs from rebuild-wekan.sh, it is not necessary anymore](https://github.com/wekan/wekan/commit/028b404377651d34e334b048ecc960d3ff5b2eab).
Thanks to xet7.
- [Fix 8.72 Bug: LDAP login does not work. ReactiveCache is not defined](https://github.com/wekan/wekan/commit/bcbf4771ea88fb617ddc44650c8ac139b0cfc998).
Thanks to poc-sm and xet7.
- [Fix Error in choosing member or assignee in a card](https://github.com/wekan/wekan/commit/7f0c42cca555dd2dca04d3b6413c0080ea75e96e).
Thanks to hmeunier95 and xet7.
- [Fix Search All Boards result opened card missing Copy Card Link etc that can be used to open correct card](https://github.com/wekan/wekan/commit/304a007d078c04dac7de5074a856c4f0cc2c0aa7).
Thanks bcook-konza and xet7.
- [Fix Checklists add and Subtasks reorder. Fix Board Activity part 1](https://github.com/wekan/wekan/commit/08ebad0fe4a2b8a7b93d75a9a9c2317ce9374fea).
Thanks bcook-konza and xet7.
- [Fix Change list inside or card](https://github.com/wekan/wekan/commit/aea8c285c246b490ac259ceafe7d9f733681c20f).
Thanks to mimZD and xet7.
- [Fix board and card activities. Part 2](https://github.com/wekan/wekan/commit/c7408ad8d147f36cfdbb71089af688bef91c3ecf).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.72 2026-04-07 WeKan ® release
This release adds the following new features and fixes:
- [Fix Impersonate User. Show/Edit Avatar at Admin Panel/People/People](https://github.com/wekan/wekan/commit/3ac7c9057e1e53da9f285149ce832ad430b3bc30).
Thanks to ahlgrimma, DnT1mmer, robbagithub, norrig and xet7.
and adds the following updates:
- [Bump actions/download-artifact from 4 to 8](https://github.com/wekan/wekan/pull/6247).
Thanks to dependabot.
- [Bump actions/upload-artifact from 4 to 7](https://github.com/wekan/wekan/pull/6248).
Thanks to dependabot.
- [Bump actions/cache from 4 to 5](https://github.com/wekan/wekan/pull/6249).
Thanks to dependabot.
- [Bump docker/login-action from 4.0.0 to 4.1.0](https://github.com/wekan/wekan/pull/6250).
Thanks to dependabot.
- [Updated release scripts](https://github.com/wekan/wekan/commit/87f592c67ffb1962ec8acb0d1676b8df6ac42a5d).
Thanks to xet7.
and fixes the following bugs:
- [Fix Admin Panel / Version not showing name of MongoDB storage engine and status of MongoDB Oplog enabled](https://github.com/wekan/wekan/commit/3b98f862bcda3e8a9b0d0131b91cdfa59986c2d8).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.71 2026-04-06 WeKan ® release
This release fixes the following bugs:
- [Fix: Added missing null check to notification](https://github.com/wekan/wekan/commit/5df924f416083a50795443687e1c28f402dc8132).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.70 2026-04-06 WeKan ® release
This release fixes the following bugs:
- [Convert upsert calls to async in authentication.js](https://github.com/wekan/wekan/commit/dfcaaf0ae719e8d8b918ac632100293a6e401371).
Thanks to snowsky and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.69 2026-04-06 WeKan ® release
This release fixes the following bugs:
- [Try to fix Snap to start MongoDB OpLog correctly](https://github.com/wekan/wekan/commit/fbdfcead50fb599d4fafecc2f5847c46c73adf72).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.68 2026-04-06 WeKan ® release
This release fixes the following bugs:
- [Try to fix Snap to start MongoDB OpLog correctly](https://github.com/wekan/wekan/commit/9e316662800fc3b5e38b1176e70a50b18d73d2a6).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.67 2026-04-05 WeKan ® release
This release fixes the following bugs:
- [Try to fix GitHub Actions release bundle builds](https://github.com/wekan/wekan/commit/7707a5b7fc21f981f26c512cd3fea6171a57bd46).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.65 2026-04-05 WeKan ® release
This release fixes the following bugs:
- [Fix Docker Actions Test suite](https://github.com/wekan/wekan/commit/c0661506c42ac412ae9e8fc5392d160efbc446ea).
Thanks to xet7.
- [Try to fix GitHub Actions release bundle builds](https://github.com/wekan/wekan/commit/d3155d24c64fe1104afe454b756d2a6324fab5c2).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.64 2026-04-05 WeKan ® release
This release fixes the following bugs:
- [Try to fix GitHub Actions release bundle builds](https://github.com/wekan/wekan/commit/7a7909b49feaaa938b5dbfc2083b0c719187859d).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.63 2026-04-05 WeKan ® release
This release fixes the following bugs:
- [Try to fix GitHub Actions release bundle builds](https://github.com/wekan/wekan/commit/6a78a178e2fcff9ff280ba44c123403e2792bb7b).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.62 2026-04-05 WeKan ® release
This release fixes the following bugs:
- [Try to fix GitHub Actions release bundle builds](https://github.com/wekan/wekan/commit/cd55c0e12ed80ddcb07895f2d787be95c440f991).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.61 2026-04-05 WeKan ® release
This release adds the following updates:
- [Updated Docker build script to check directories and repos exist](https://github.com/wekan/wekan/commit/81319fc48bbd2a91553090428455bc11aff24fbc).
Thanks to xet7.
- [Updated release scripts: Delete all GitHub Actions Caches, so that old stuff does not break builds](https://github.com/wekan/wekan/commit/f3a97b993842425e4f13bb78e5939a069d64887e).
Thanks to xet7.
and fixes the following bugs:
- [Fix: Server crash during comment deletion](https://github.com/wekan/wekan/pull/6246).
Thanks to KhaoulaMaleh.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.60 2026-04-05 WeKan ® release
This release adds the following updates:
- [Updated Docker build script](https://github.com/wekan/wekan/commit/0b59f87887c215dc34093409ccd12da9acd3ef02).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.59 2026-04-05 WeKan ® release
This release fixes the following bugs:
- [Fix docker-compose.yml to use OpLog](https://github.com/wekan/wekan/commit/30a1792c92de9d485c0b9ed3970a1187ca974576).
Thanks to xet7.
- [Fix Snap to use OpLog](https://github.com/wekan/wekan/commit/1fa72fd34ab4547e97e7e0c201bc3d50d478ef5e).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.58 2026-04-05 WeKan ® release
This release adds the following updates:
- [Updated build docs](https://github.com/wekan/wekan/commit/b3d14fb5e87545635006958686f0340028711820).
Thanks to xet7.
and fixes the following bugs:
- [Try to fix .zip Bundle, Docker and Snap](https://github.com/wekan/wekan/commit/59956ca720d2103baaa9ecf2befff2ec76430feb).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.57 2026-04-05 WeKan ® release
This release fixes the following bugs:
- [Try to fix Snap](https://github.com/wekan/wekan/commit/9f549dfcd2efa241a2ad3bdf5b0d2d942064d4e3).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.56 2026-04-05 WeKan ® release
This release fixes the following bugs:
- [Try to fix Snap](https://github.com/wekan/wekan/commit/56f26418ea9d1321fc5109c4b39104ff98d65429).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.55 2026-04-05 WeKan ® release
This release adds the following updates:
- Docs: IMPORTANT: Do NOT `npm audit fix --force`, it breaks builds.
[Part 1](https://github.com/wekan/wekan/commit/2bad17010cd445c004a228b732b17c68e18eb316),
[Part 2](https://github.com/wekan/wekan/commit/9fa6574c80589218569dc3c49782a1a8036aa542).
Thanks to xet7.
- [Updated dependencies](https://github.com/wekan/wekan/commit/73d69f4631216dfe504d9f05230adadc2850c4f3).
Thanks to developers of dependencies.
and fixes the following bugs:
- [Fix Cards Reports is not working](https://github.com/wekan/wekan/commit/2b02e7712ee6a8ae8af5d416ce2d288a25386fdf).
Thanks to titver968 and xet7.
- [Fix Search All Boards](https://github.com/wekan/wekan/commit/faf8453c6d10613edf32a734e549961860fcdd32).
Thanks to YosserDerbali and xet7.
- [Try to fix Snap](https://github.com/wekan/wekan/commit/8443e3511d61d8c1bb419ed5bca2e28483c5d133).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.54 2026-04-04 WeKan ® release
This release fixes the following bugs:
- [Revert v8.53 Refactor upsert calls to use async method](https://github.com/wekan/wekan/commit/811b79b641c765461f3d696ed00b5459a4747867).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.53 2026-04-04 WeKan ® release
This release fixes the following bugs:
- [Refactor upsert calls to use async method](https://github.com/wekan/wekan/pull/6243).
Thanks to snowsky.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.52 2026-04-03 WeKan ® release
This release fixes the following bugs:
- [Try to fix Docker and Snap](https://github.com/wekan/wekan/commit/03e0cfdc15c56ac0219f5e435034a1c0ad5006ae).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.51 2026-04-03 WeKan ® release
This release adds the following updates:
- [Updated release scripts](https://github.com/wekan/wekan/commit/4594711be41d1e9cf15b1f8bb070fcb36bf736ca).
Thanks to xet7.
and fixes the following bugs:
- [Fix Bug: WeKan v8.50 WRITABLE_PATH](https://github.com/wekan/wekan/commit/edc6b18b5f77320e966e56c888575bcb764096dc).
Thanks to snowsky and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.50 2026-04-03 WeKan ® release
This release adds the following updates:
- [Uncommented build steps. Not yet tested are required dependencies included](https://github.com/wekan/wekan/commit/145a17fee5dc9430de7a60100657566a4a69bc7b).
Thanks to xet7.
- [Updated npm package @wekanteam/exceljs](https://github.com/wekan/wekan/commit/6e8589b1f96b5f0c2ddec0831b238096ce610b1e).
Thanks to xet7.
- [Updated dependencies](https://github.com/wekan/wekan/commit/bf02c9a453e27edba1c08830eb4783b9dfc7bbc2).
Thanks to developers of dependencies.
NOTE: Do not `npm audit fix --force`, it downgrades @meteorjs/rspack and breaks builds.
`npm audit` shows false info, it does not recognize that elliptic is
already updated to 6.6.1 with override at package.json like you can see with
`npm list elliptic`.
and fixes the following bugs:
- [Fix indentation for release-all.yml workflow](https://github.com/wekan/wekan/pull/6240).
Thanks to snowsky.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.49 2026-03-31 WeKan ® release
This release fixes the following bugs:
- [Fix: Missing module @meteorjs/reify/lib/runtime](https://github.com/wekan/wekan/pull/6237).
Thanks to mzch and kovacs-andras.
- [Fix docker-compose.yml](https://github.com/wekan/wekan/commit/d07fcceb6a884cf1ff0c9a2d36691af0304f36e7).
Thanks to xet7.
- [Try to fix Snap](https://github.com/wekan/wekan/commit/8b4676175caa4af36bf11567676f3c3f522f9512).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.48 2026-03-31 WeKan ® release
This release adds the following updates:
- Updated release scripts.
[Part 1](https://github.com/wekan/wekan/commit/43601c5e3e95d6026f2e558f261c02a9578dcecd),
[Part 2](https://github.com/wekan/wekan/commit/56377aaf4c5e92c1a03dc996b6c649ce17553280).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.47 2026-03-31 WeKan ® release
This release adds the following updates:
- [Updated attachments and avatar path to be at WRITABLE_PATH/files/attachments etc](https://github.com/wekan/wekan/commit/e58e99c2852dd81a040dcffc0f1f2798c4b8cf15).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.46 2026-03-31 WeKan ® release
This release adds the following updates:
- Updated release scripts.
[Part 1](https://github.com/wekan/wekan/commit/6bc68ba57bf15339b80011d131a7da65124e071d),
[Part 2](https://github.com/wekan/wekan/commit/ce52b8a0ab29805c562a2af0b79e30d970594e17).
Thanks to xet7.
- [Updated docker-compose.yml to change from FerretDB/PostgreSQL to MongoDB](https://github.com/wekan/wekan/commit/02b75824e5dc24c968959d5a64f846efe33dee4f).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.45 2026-03-31 WeKan ® release
This release adds the following updates:
- Updated release scripts.
[Part 1](https://github.com/wekan/wekan/commit/4223c2a79f4c276ab1c9734a94314f1d1d1ce033),
[Part 2](https://github.com/wekan/wekan/commit/6125822cf7244242832d1fe02f99150abb7350d0),
[Part 3](https://github.com/wekan/wekan/commit/e90b2e384c168fbb4cee389d8af9b54a5bc57734),
[Part 4](https://github.com/wekan/wekan/commit/29550cbd9335fc95e4f4ac399fe754ece8573450),
[Part 5](https://github.com/wekan/wekan/commit/8349fb6b63097970d06af73af388b030761967bd),
[Part 6](https://github.com/wekan/wekan/commit/f43b76c323c1f09cf1885251792b29229ef77d2b),
[Part 7](https://github.com/wekan/wekan/commit/e90b2e384c168fbb4cee389d8af9b54a5bc57734).
Thanks to xet7.
- Updated GitHub Actions.
[Part 1](https://github.com/wekan/wekan/commit/4b8cc0683e9dfdffb7ff7896f622afe8f908a7f5),
[Part 2](https://github.com/wekan/wekan/commit/df067f01e894f43305ad36cebdf48a1bf55ff33a),
[Part 3](https://github.com/wekan/wekan/commit/ec1e3835586bdc09d93d5049794e8657b460acef),
[Part 4](https://github.com/wekan/wekan/commit/79853a2fb7efb738f2f10b6c2e9fef69def88c48),
[Part 5](https://github.com/wekan/wekan/commit/9ae2842fa4bedbd4c132943703ef462cc353ab1a),
[Part 6](https://github.com/wekan/wekan/commit/1a5e23617de98e6b65f78dfb25c233440f818d11),
[Part 7](https://github.com/wekan/wekan/commit/c38fa1c272c32323d8a164dfcf34b898127e6b1d),
[Part 8](https://github.com/wekan/wekan/commit/d8a2e06a95713e33e5676df38951dfd3f991ecb3),
[Part 9](https://github.com/wekan/wekan/commit/e6ccbfd3b88299c52fbaad9f85fc3b438e3cdc89),
[Part 10](https://github.com/wekan/wekan/commit/7666c6c0e1660b5aba300f4e465877bcf73d8066).
Thanks to dependabot.
and fixes the following bugs:
- Try to fix Snap.
[Part 1](https://github.com/wekan/wekan/commit/e5c551f0092ccb71fafa3433bfd99911f9a2e9c0),
[Part 2](https://github.com/wekan/wekan/commit/55ec0b5ff5594c10ebbc8cba74339de2ad0557a6),
[Part 3](https://github.com/wekan/wekan/commit/fd58bb9e4dd0f825237a50043a171fc223d44101),
[Part 4](https://github.com/wekan/wekan/commit/cdf1d3305bd738fe5357a5f3a9df2f7e6858d1d0),
[Part 5](https://github.com/wekan/wekan/commit/2aa8ecfc39a6f971134784817b46ba9867bb402d),
[Part 6](https://github.com/wekan/wekan/commit/ed718f083aa0c9f82e368a5badfe73393753a617),
[Part 7](https://github.com/wekan/wekan/commit/5c4130311a063340bd89dff571fbf685cda7a3ba),
[Part 8](https://github.com/wekan/wekan/commit/33267fa2e462fcc6cd2243ae1b5b39d59b03988f),
[Part 9](https://github.com/wekan/wekan/commit/87916c20bf579e16e21fa3bd9ac7687019b277eb),
[Part 10](https://github.com/wekan/wekan/commit/12c223d2260c9bd9547ae35a394869ef90e8bc83).
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.44 2026-03-30 WeKan ® release
This release adds the following updates:
- [From WeKan v8.43 FerretDB/PostgreSQL back to MongoDB 7.x](https://github.com/wekan/wekan/commit/cf2b3e4fc95ce4619c1ba13b63c3ad9d90bf59f6).
Thanks to xet7.
- [Updated dependencies](https://github.com/wekan/wekan/commit/82f1de078f16e87028ffd101e3f80550a4b18afa).
Thanks to developers of dependencies.
- Updated release scripts.
[Part 1](https://github.com/wekan/wekan/commit/a702596bee6aa812cb69a6673ad34f9b2875c729),
[Part 2](https://github.com/wekan/wekan/commit/29550cbd9335fc95e4f4ac399fe754ece8573450).
Thanks to xet7.
and fixes the following bugs:
- [Always show all data without migrations, like Shared Lists that were before Per-Swimlanes lists, Orphaned Swimlanes/Lists/Cards etc](https://github.com/wekan/wekan/commit/ef75b9ecac8210ff000439aca92765b53a1551a4).
Thanks to xet7.
- [Fix globalSearch](https://github.com/wekan/wekan/commit/b788d398b9b2cb0caad9c28f79acdceb5b577f25).
Thanks to snowsky.
- [Fix myCards crash](https://github.com/wekan/wekan/commit/d5c9e402ce4ebe0318bc29d5b83784771cbe8ac3).
Thanks to cristomoya.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.43 2026-03-27 WeKan ® release
This release adds the following updates:
- Updated to Meteor 3.4, Node.js 22.22, MongoDB 7.x.
[Part 1](https://github.com/wekan/wekan/pull/6205),
[Part 2](https://github.com/wekan/wekan/commit/7584afda6d28b75ae7bfa5388f71e8bcb5ab50a7).
Thanks to harryadel and xet7.
- [Updated dependencies of npm package @wekanteam/exceljs](https://github.com/wekan/wekan/commit/95ff8ef1f2f119c713ffba72122d51b6569b9d42).
Thanks to xet7.
- [Updated @meteorjs/rspack](https://github.com/wekan/wekan/commit/21ab640686d868ee46e16e812b36a1c3820952a5).
Thanks to Meteor developers.
- [Updated rebuild-wekan.sh for macOS 26.4](https://github.com/wekan/wekan/commit/f1e71247c70043634e040bd12f7d065b30ed6fc2).
Thanks to xet7.
- [Moved Meteor 3 migration docs to docs/Upgrade/](https://github.com/wekan/wekan/commit/517d1483876926cdbf6ce4774c8c52540eaec593).
Thanks to xet7.
- Updated dependencies.
[Part 1](https://github.com/wekan/wekan/commit/acfb081b907703c6846b8ccf71a9e2912b41713f),
[Part 2](https://github.com/wekan/wekan/commit/55660e0e69b7a283ab04ad408674d33eb71bb279).
Thanks to developers of dependencies.
- Updated release scripts.
[Part 1](https://github.com/wekan/wekan/commit/0fdf45e8476c0a619cd36891568c67d9092bd65b),
[Part 2](https://github.com/wekan/wekan/commit/07eb062474a5593683c1ed32f2637339fab72d48),
[Part 3](https://github.com/wekan/wekan/commit/5cf3bba68fdfc57a7a9358bd914c11af7bdd37d4),
[Part 4](https://github.com/wekan/wekan/commit/0e59033ea60f504d09d7eddb22a33ed113ea1326).
Thanks to xet7.
and fixes the following bugs:
- [Fixed rebuild-wekan.sh to build WeKan correctly](https://github.com/wekan/wekan/commit/4a8293f8812276f08d8a77a2c8be56441d7e87c1).
Thanks to xet7.
- [Export Card to Excel. Part 3. Also, fixed uploading attachments](https://github.com/wekan/wekan/commit/5c5ed102cabd07e1be00d3fdb01ea599da0a5517).
Thanks to xet7.
- [Removed temporary debug logs](https://github.com/wekan/wekan/commit/c1e3ce64a796fc8b921d1a30705b4aaf66c9c259).
Thanks to xet7.
- [Fixed typo](https://github.com/wekan/wekan/commit/23697d2a33ca527b29bcdadfe432fd9d1a180f9c).
Thanks to YosserDerbali and xet7.
- [Fix Set Color of Swimlane, List and Card](https://github.com/wekan/wekan/commit/0630d62ad0a02d80ab77436ce9797e4390c5cae2).
Thanks to SVANNER, brlin-tw and xet7.
- [Fix workspace settings modal overflow](https://github.com/wekan/wekan/pull/6218).
Thanks to AymenHassini19 and aminsaidane.
- [Fix attachment rename fails for clipboard uploads and keep extension metadata in sync](https://github.com/wekan/wekan/commit/38c670e08700f7b0ff185aadda40985bc71b1ac4).
Thanks to snowsky, YosserDerbali and xet7.
- [Fix card actions popup overflow](https://github.com/wekan/wekan/pull/6220).
Thanks to aminsaidane.
- [Fix Search: Guard Async methods in ReactiveCache and search publications](https://github.com/wekan/wekan/pull/6222).
Thanks to KhaoulaMaleh.
- [Fix/list actions popup overflow scroll](https://github.com/wekan/wekan/pull/6223).
Thanks to YosserDerbali.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.42 2026-03-23 WeKan ® release
This release fixes the following bugs:
- [Fix SSO login is broken](https://github.com/wekan/wekan/commit/2dce7a84cd0ce011972d377a05f5b26ff12daba7).
Thanks to sbaecker, vladimirdulov and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.41 2026-03-23 WeKan ® release
This release adds the following new features:
- [Export Card to Excel. Part 2](https://github.com/wekan/wekan/commit/7de1b04ebc51269e37840e74ba8fa72eed59950e).
Thanks to xet7.
and adds the following updates:
- [Replace useraccounts packages with communitypackages alternatives](https://github.com/wekan/wekan/pull/6195).
Thanks to harryadel.
- [Remove unused meteorhacks:aggregate for Meteor 3.0 migration](https://github.com/wekan/wekan/pull/6196).
Thanks to harryadel.
- [Replace Email.send with Email.sendAsync](https://github.com/wekan/wekan/pull/6197).
Thanks to harryadel.
and fixes the following bugs:
- [Fixed 2nd new card should not have text of 1st card, it should be empty](https://github.com/wekan/wekan/commit/7b2128b3565ecf793a780980c71e5ab9cde0a3b2).
Thanks to xet7.
- [Fix: prevent cards from disappearing after dragging in List view](https://github.com/wekan/wekan/pull/6199).
Thanks to KhaoulaMaleh.
- [Fixed Board lost its member list or permissions](https://github.com/wekan/wekan/commit/3d6797a3058875eb531aa006935ab69825975afd).
Thanks to marylinereumont, AymenHassini19, muabila and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.40 2026-03-21 WeKan ® release
This release adds the following new features:
- [Export Card to Excel and related fixes. Part 1](https://github.com/wekan/wekan/commit/be5bdbe3142f54f571fcef79cac064cec6da17d2).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.39 2026-03-21 WeKan ® release
This release adds the following new features:
- Delete workspace. Workspace Settings.
[Part 1](https://github.com/wekan/wekan/commit/8513b2e456ce27c54b9316a8fb52a87a94ab0e7f),
[Part 2](https://github.com/wekan/wekan/commit/ac33d8012b79ce8c4e92a472f8f2ff5aef488022).
Thanks to mimZD and xet7.
and fixes the following bugs:
- [Fix open lock from all button position at Admin Panel/People/People](https://github.com/wekan/wekan/commit/26d393f8ee6d836d830b05e2ef64bc0d6b84c1fa).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.38 2026-03-21 WeKan ® release
This release adds the following updates:
- [Updated CONTRIBUTING.md and SECURITY.md](https://github.com/wekan/wekan/commit/6acc7c95ff53d90918eb98259e1e93fbd616212c).
Thanks to xet7.
- Updated CONTRIBUTING.md
[Part 1](https://github.com/wekan/wekan/commit/a4a4fc1cf218089e4a2aab8b4941f0d90e8771b1),
[Part 2](https://github.com/wekan/wekan/commit/6249af041c77091d7aa83d6f11f35ccccb8c3b24),
[Part 3](https://github.com/wekan/wekan/commit/e669a9fe64a2c2c709c118cdb9128125c039193b),
[Part 4](https://github.com/wekan/wekan/commit/e705df663487b7d394d327168628402ad9b93ba2),
[Part 5](https://github.com/wekan/wekan/commit/e50b9f211de7d6b7b9c7ec1e5027015e136093a6),
[Part 6](https://github.com/wekan/wekan/commit/7ed26f437d07ff520eb4a8d0fd8f205ea2d1e49e).
and fixes the following bugs:
- [Fix List of users is empty, some translations not visible, count of orgs/teams/people, theme of some buttons, New org/team to be leftmost column](https://github.com/wekan/wekan/commit/803eb452e96945383f480a8cd532233e17263d38).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.37 2026-03-20 WeKan ® release
This release fixes the following bugs:
- [Fix card save does not work](https://github.com/wekan/wekan/commit/37142f3bb9f4f10a3049a9837456c0753e6e329a).
Thanks to comunidadplayasdetijuana, Janfy and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.36 2026-03-20 WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUES of [IntegrationBleed](https://wekan.fi/hall-of-fame/integrationBleed/):
- [Fix RebindBleed of IntegrationBleed](https://github.com/wekan/wekan/commit/92beaa313706bc26fbea7e1cc8cfaf836609e038).
Thanks to Rodolphe GHIO and xet7.
and adds the following new features:
- [Add remaining features of Copy and Move of Swimlanes, Lists and Cards](https://github.com/wekan/wekan/commit/610849ab0bcfb72c92cf9cf512c51b8cede55dad).
Thanks to xet7.
and adds the following updates:
- [Bump docker/build-push-action from 6.19.2 to 7.0.0](https://github.com/wekan/wekan/pull/6181).
Thanks to dependabot.
- [Bump docker/metadata-action from 5.10.0 to 6.0.0](https://github.com/wekan/wekan/pull/6182).
Thanks to dependabot.
- [Bump docker/login-action from 3.7.0 to 4.0.0](https://github.com/wekan/wekan/pull/6183).
Thanks to dependabot.
- [Remove peerlibrary:blaze-components](https://github.com/wekan/wekan/pull/6178).
Thanks to harryadel.
- [Updated dependencies](https://github.com/wekan/wekan/commit/63df6cb197ff497cf8c97dad46c080105f00b2d1).
Thanks to developers of dependencies.
- [Updated flatted](https://github.com/wekan/wekan/commit/a159856628ae9b6454cbdb0a7beca190a785e8af).
Thanks to developers of flatted.
- [Updated to MongoDB 7.0.31 at Snap Candidate](https://github.com/wekan/wekan/commit/fccefcaa6ce0b3408f3917e442229b6c92f877ac).
Thanks to MongoDB developers.
- [Helm Chart: Updated to MongoDB 7.0.31](https://github.com/wekan/charts/commit/6818b7fa5e7371b3777f49a7460102a1e1685746).
Thanks to MongoDB developers.
- [Replace underscore with native JavaScript utilities](https://github.com/wekan/wekan/pull/6190).
Thanks to harryadel.
- [Removed not needed input fields from GitHub Bug report issue template](https://github.com/wekan/wekan/commit/7d0a42271467cbf203adc6ca15fd5b02a1cd0053).
Thanks to xet7.
- [Remove raix:handlebar-helpers, replace with native Blaze helpers](https://github.com/wekan/wekan/pull/6191).
Thanks to harryadel.
- [Updated flatted](https://github.com/wekan/wekan/commit/c3f66cce55dec0d4154f41aa869eed75f9278f43).
Thanks to developers of flatted.
and fixes the following bugs:
- [Fixed linked card swimlane routing](https://github.com/wekan/wekan/pull/6179).
Thanks to KhaoulaMaleh.
- [Replaced incompatible file-type with mime-type](https://github.com/wekan/wekan/commit/89f86caf69db0600a207aee075361f8a6801253b).
Thanks to xet7.
- [Fix Add List popup to not open after adding new board or there being no lists at all](https://github.com/wekan/wekan/commit/7e378be1d87280b8fb3f63eea3c0374e12054984).
Thanks to xet7.
- [Improve global search relevance and add exact-field operators](https://github.com/wekan/wekan/pull/6187).
Thanks to KhaoulaMaleh.
- [Fix list add-card actions and sort persistence](https://github.com/wekan/wekan/pull/6188).
Thanks to harryadel.
- [Fix version not visible](https://github.com/wekan/wekan/commit/1f6b5ed0e387dee0b5a90bcf10dd425e098bba6a).
Thanks to FK-PATZ3 and xet7.
- [Fix List drag handle missing](https://github.com/wekan/wekan/commit/6fc1cb91279d322cfb4a7dfe3deec18c91509184).
Thanks to FK-PATZ3 and xet7.
- [Fix Error reading default file .well-known/assetlinks.json.default](https://github.com/wekan/wekan/commit/36741d7ae5018693f51215b01924a8f7cc709477).
Thanks to gre3x and xet7.
- [Fix can not close opened card](https://github.com/wekan/wekan/commit/b6a6180dc31c82115418e363057a4cd5ba7b7e4e).
Thanks to xet7.
- [Removed empty space at top of list header](https://github.com/wekan/wekan/commit/d2168aca3d09caf698128efa2eb9aebad35d4929).
Thanks to xet7.
- [Changed colors of Add Rule and Back buttons from black to theme color](https://github.com/wekan/wekan/commit/a3ac2195037c182367fb744ba6d3ef9ed1863792).
Thanks to xet7.
- [Fix more Rules buttons styling](https://github.com/wekan/wekan/commit/e6fbfa87f036d81ca63c71216a9804bf93979657).
Thanks to xet7.
- [Fixed crash when IFTTT Rules archived card, but cards etc are not visible at archive yet. Fixed "Add After List" to show lists only from current Swimlane](https://github.com/wekan/wekan/commit/0c29fa3cbee49f054591ce421d790d364f4f28af).
Thanks to xet7.
- [Fixed crash at sign-up page, when registering new user, happened after some PR](https://github.com/wekan/wekan/commit/ccd23f6cdea8b012e492674491969271cbbad842).
Thanks to xet7.
- [Fixed Add after list to add after selected list. Added Confirm to Archive List and Archive Swimlane](https://github.com/wekan/wekan/commit/872dd578c84d629e804566802524100983095b4a).
Thanks to xet7.
- [Fixed view, restore and delete of Archived Cards, Lists and Swimlanes. If delete list, it does not delete card of that list that was archived before archive list](https://github.com/wekan/wekan/commit/c989c21d4fcee6f095134176410e5811343d6923).
Thanks to xet7.
- [Fixed add remove checklist and checklist item, and checklist item translation not used](https://github.com/wekan/wekan/commit/2f07eb70974b40a463ae9f1daddc829e1b5ae468).
Thanks to xet7.
- [Fixed adding label and select color causes color not checkmarked and wrong color choosed](https://github.com/wekan/wekan/commit/4dea702ff28ef4a1fe94abc26b96f666eeaa4aad).
Thanks to xet7.
- [Fix Copy Card and Move Card](https://github.com/wekan/wekan/commit/d0c44bded81d07e5bc9a90c89efca797e331d5eb).
Thanks to BlobbyBob and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.35 2026-03-05 WeKan ® release
This release adds the following CRITICAL SECURITY FIXES of [Integrationbleed](https://wekan.fi/hall-of-fame/integrationbleed/):
This release fixes the following CRITICAL SECURITY ISSUES of [IntegrationBleed](https://wekan.fi/hall-of-fame/integrationBleed/):
- [Fix Integrationbleed](https://github.com/wekan/wekan/commit/2cd702f48df2b8aef0e7381685f8e089986a18a4).
- [Fix IntegrationBleed](https://github.com/wekan/wekan/commit/2cd702f48df2b8aef0e7381685f8e089986a18a4).
Thanks to Rodolphe GHIO and xet7.
and adds the following updates:
@@ -69,15 +723,15 @@ Thanks to above GitHub users for their contributions and translators for their t
# v8.34 2026-02-20 WeKan ® release
This release adds the following CRITICAL SECURITY FIXES of [Anchorbleed](https://wekan.fi/hall-of-fame/anchorbleed/):
This release fixes the following CRITICAL SECURITY ISSUES of [AnchorBleed](https://wekan.fi/hall-of-fame/anchorBleed/):
- [Fix GHSL-2026-035_Wekan CursorBleed of Anchorbleed](https://github.com/wekan/wekan/commit/1c8667eae8b28739e43569b612ffdb2693c6b1ce).
- [Fix GHSL-2026-035_Wekan CursorBleed of AnchorBleed](https://github.com/wekan/wekan/commit/1c8667eae8b28739e43569b612ffdb2693c6b1ce).
Thanks to GHSL and xet7.
- [Fix GHSL-2026-036_Wekan WatchBleed of Anchorbleed](https://github.com/wekan/wekan/commit/8c00adc6b865653bd717a946dd646eb54ac78c9c).
- [Fix GHSL-2026-036_Wekan WatchBleed of AnchorBleed](https://github.com/wekan/wekan/commit/8c00adc6b865653bd717a946dd646eb54ac78c9c).
Thanks to GHSL and xet7.
- [Fix GHSL-2026-037_Wekan GlobalBleed of Anchorbleed](https://github.com/wekan/wekan/commit/1ee9b2e917104f54c035f6426169a28fedecbdb6).
- [Fix GHSL-2026-037_Wekan GlobalBleed of AnchorBleed](https://github.com/wekan/wekan/commit/1ee9b2e917104f54c035f6426169a28fedecbdb6).
Thanks to GHSL and xet7.
- [Fix GHSL-2026-044_Wekan CustomFieldBleed of Anchorbleed](https://github.com/wekan/wekan/commit/73eb98c57afd3d72377a1f7160a52450ab0eeb8b).
- [Fix GHSL-2026-044_Wekan CustomFieldBleed of AnchorBleed](https://github.com/wekan/wekan/commit/73eb98c57afd3d72377a1f7160a52450ab0eeb8b).
Thanks to GHSL and xet7.
- [Fix GHSL-2026-045_Wekan ImportBleed of AnchorBleed](https://github.com/wekan/wekan/commit/62216e36c15f55d4ef6cb97313db3aa54fc77fe0).
Thanks to GHSL and xet7.
@@ -258,9 +912,9 @@ Thanks to above GitHub users for their contributions and translators for their t
# v8.25 2026-01-28 WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUES of [Floppybleed](https://wekan.fi/hall-of-fame/floppybleed/):
This release fixes the following CRITICAL SECURITY ISSUES of [FloppyBleed](https://wekan.fi/hall-of-fame/floppyBleed/):
- [Fix Filebleed of Floppybleed](https://github.com/wekan/wekan/commit/a419d831a408f251c798f5410375b20afd98c04b).
- [Fix FileBleed of FloppyBleed](https://github.com/wekan/wekan/commit/a419d831a408f251c798f5410375b20afd98c04b).
Thanks to Luke Hebenstreit Twitter lheben_ and xet7.
and adds the following updates:
@@ -358,7 +1012,7 @@ Thanks to above GitHub users for their contributions and translators for their t
# v8.21 2026-01-18 WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUES of [Snowbleed](https://wekan.fi/hall-of-fame/snowbleed/):
This release fixes the following CRITICAL SECURITY ISSUES of [SnowBleed](https://wekan.fi/hall-of-fame/snowBleed/):
- [Security Fix 2: OrgsTeamsBleed](https://github.com/wekan/wekan/commit/cabfeed9a68e21c469bf206d8655941444b9912c).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
@@ -420,7 +1074,7 @@ Thanks to above GitHub users for their contributions and translators for their t
# v8.20 2026-01-16 WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUES of [Snowbleed](https://wekan.fi/hall-of-fame/snowbleed/):
This release fixes the following CRITICAL SECURITY ISSUES of [SnowBleed](https://wekan.fi/hall-of-fame/snowBleed/):
- [Security Fix 1: MigrationsBleed](https://github.com/wekan/wekan/commit/cbb1cd78de3e40264a5e047ace0ce27f8635b4e6).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
@@ -480,7 +1134,7 @@ Thanks to above GitHub users for their contributions and translators for their t
# v8.19 2025-12-29 WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUES of [Megableed](https://wekan.fi/hall-of-fame/megableed/):
This release fixes the following CRITICAL SECURITY ISSUES of [MegaBleed](https://wekan.fi/hall-of-fame/megaBleed/):
- [Security Fix 1: IDOR in setCreateTranslation. Non-admin could change Custom Translation](https://github.com/wekan/wekan/commit/f244a43771f6ebf40218b83b9f46dba6b940d7de).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
@@ -536,9 +1190,9 @@ Thanks to above GitHub users for their contributions and translators for their t
# v8.18 2025-12-28 WeKan ® release
This release adds the following CRITICAL SECURITY FIXES:
This release fixes the following CRITICAL SECURITY ISSUES:
- [Upgraded MongoDB to 7.0.28 to fix mongobleed at Snap Candidate](https://github.com/wekan/wekan/commit/e210c9973be55a4fa4e7dd15aefc24e06dbc3e7f).
- [Upgraded MongoDB to 7.0.28 to fix MongoBleed at Snap Candidate](https://github.com/wekan/wekan/commit/e210c9973be55a4fa4e7dd15aefc24e06dbc3e7f).
Thanks to developers of MongoDB.
and adds the following new features:
@@ -638,7 +1292,7 @@ Thanks to above GitHub users for their contributions and translators for their t
# v8.16 2025-11-02 WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUES of [Spacebleed](https://wekan.fi/hall-of-fame/spacebleed/):
This release fixes the following CRITICAL SECURITY ISSUES of [SpaceBleed](https://wekan.fi/hall-of-fame/spaceBleed/):
- [Fix SECURITY ISSUE 1: File Attachments enables stored XSS (High)](https://github.com/wekan/wekan/commit/e9a727301d7b4f1689a703503df668c0f4f4cab8).
Thanks to Siam Thanat Hack (STH) and xet7.
@@ -974,7 +1628,7 @@ Thanks to above GitHub users for their contributions and translators for their t
# v7.98 2025-10-11 WeKan ® release
This release adds the following CRITICAL SECURITY FIXES:
This release fixes the following CRITICAL SECURITY ISSUES:
- [Security Fix: IDOR CWE-639 that affected WeKan 7.80-7.93](https://github.com/wekan/wekan/commit/b87cff1289d2c98ef84d69e171f112ecedb4d843).
Thanks to Romain Korpas at apitech.fr and xet7.
@@ -3641,7 +4295,7 @@ Thanks to above GitHub users for their contributions and translators for their t
# v6.86 2023-04-26 WeKan ® release
This release fixes the following CRITICAL SECURITY FIXES:
This release fixes the following CRITICAL SECURITY ISSUES:
- [Security fix to InvisibleBleed in WeKan. Escape HTML comment tags so that HTML comments are visible](https://github.com/wekan/wekan/commit/167863d95711249e69bb3511175d73b34acbbdb3).
Thanks to xet7 for fixing.
@@ -3677,7 +4331,7 @@ Thanks to above GitHub users for their contributions and translators for their t
# v6.85 2023-04-18 WeKan ® release
This release adds the following CRITICAL SECURITY FIXES:
This release fixes the following CRITICAL SECURITY ISSUES:
- [Security fix to ReactionBleed in WeKan. It is XSS in feature "Reaction to comment"](https://github.com/wekan/wekan/commit/47ac33d6c234359c31d9b5eae49ed3e793907279).
Thanks to Alexander Starikov at Jet Infosystems (https://jetinfosystems.com/) for reporting and fixing.
@@ -3857,7 +4511,7 @@ Thanks to above GitHub users for their contributions and translators for their t
This release fixes the following CRITICAL SECURITY ISSUES:
- Security Fix of Filebleed in WeKan. That is XSS in filename.
- Security Fix of FileBleed in WeKan. That is XSS in filename.
[Part 1](https://github.com/wekan/wekan/commit/ff993e7c917b5650a790238e95c78001e4f0e039),
[Part 2](https://github.com/wekan/wekan/commit/382168a5b428a7124d368c4fcb37e7e140e7ec8b).
Thanks to responsible security disclosure contributors and xet7.
@@ -5539,7 +6193,7 @@ Thanks to above GitHub users for their contributions and translators for their t
# v5.88 2021-12-22 WeKan ® release
This release adds to following CRITICAL SECURITY FIXES:
This release fixes to following CRITICAL SECURITY ISSUES:
- [User now should only see archived cards belonging to boards to which he has permission](https://github.com/wekan/wekan/pull/4252).
Thanks to jrsupplee.
@@ -7227,7 +7881,7 @@ Thanks to above GitHub users for their contributions and translators for their t
# v4.98 2021-02-24 Wekan release
This release adds the following CRITICAL SECURITY FIXES:
This release fixes the following CRITICAL SECURITY ISSUES:
- [Updated Node.js to v12.21.0](https://github.com/wekan/wekan/commit/fde6a6593379277d601408ec83f6f5a4347afef0).
Thanks to Node.js developers.
@@ -7765,7 +8419,7 @@ Thanks to above GitHub users for their contributions and translators for their t
# v4.75 2021-01-11 Wekan release
This release adds the following CRITICAL SECURITY FIXES:
This release fixes the following CRITICAL SECURITY ISSUES:
- [Due Cards and Broken Cards: In All Users view, fixed to show cards only from other users Public Boards. Not anymore from private
boards](https://github.com/wekan/wekan/commit/801d0aacf00eace05ec70d6f0229f2a752f119cd).
@@ -7847,7 +8501,7 @@ Thanks to above GitHub users for their contributions and translators for their t
# v4.70 2021-01-04 Wekan release
This release adds the following CRITICAL SECURITY FIXES:
This release fixes the following CRITICAL SECURITY ISSUES:
- [Upgrade to Node.js 12.20.1](https://github.com/wekan/wekan/commit/4bfe017b08f573991fd1f9229ae53573798f475e).
Thanks to Node developers.
+16 -15
View File
@@ -6,24 +6,25 @@ Not paid:
- Security reports, see [SECURITY.md](SECURITY.md)
- Pull requests
- xet7 checking pull requests
- Public Community Support
- This is because full-featured WeKan FLOSS kanban with MIT license is free to use
also for Commercial Use without any payment, with full source code at
https://github.com/wekan/wekan for all platforms at https://wekan.fi/install/ .
There is nothing proprietary. WeKan is NOT Big Tech.
WeKan is maintained by https://github.com/xet7 at Finland https://wekan.fi .
- Public FLOSS Developer Community Support:
- https://github.com/wekan/wekan/issues
Paid by customers of WeKan Team:
Paid by customers of WeKan to WeKan:
- Commercial Support at https://wekan.fi/commercial-support/
- Support
- Private Chat
- Features
- Fixes
- Hosting
- Optional Private Paid Commercial Support at https://wekan.fi/commercial-support/
- Optional Paid Support/Features/Fixes/PrivateChat . All Paid Features and Fixes are added directly to FLOSS WeKan https://github.com/wekan/wekan
- Optional Paid Official SaaS Hosting https://wekan.fi/saas/ , if you need full-featured hosting, that is more than free app https://wekan.fi/app/
## Contributing Security related
For responsible security disclosure, please follow this process:
https://github.com/wekan/wekan/blob/main/SECURITY.md
For responsible security disclosure, please follow this process: [SECURITY.md](SECURITY.md)
CVE Hall of Fame is at https://wekan.github.io/hall-of-fame/
CVE Hall of Fame is at https://wekan.fi/hall-of-fame/
## Contributing to Documentation Wiki
@@ -33,7 +34,7 @@ and click send PR.
## Contributing code
[Building WeKan and sending PR](https://github.com/wekan/wekan/wiki/Emoji).
[Building WeKan and sending PR](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md).
WeKan code contributors Hall of Fame is at ChangeLog, where
GitHub usernames are mentioned with changes added:
@@ -44,9 +45,9 @@ Changes can be like typo fixes, bugfixes, features, or anything else
like for example at open GitHub issues https://github.com/wekan/wekan/issues .
Closed issues are already fixed or implemented.
Also see other docs at wiki, for example:
Also see other docs:
https://github.com/wekan/wekan/wiki/Developer-Documentation
https://github.com/wekan/wekan/tree/main/docs/DeveloperDocs
Do not use code formatting or linting like eslist or prettier.
@@ -64,7 +65,7 @@ so that WeKan code will still work OK.
## Contributing translations
Non-English translations are contributed only at
https://transifex.com/wekan/wekan
https://app.transifex.com/wekan/wekan
When adding new features, in your PR to
https://github.com/wekan/wekan/pulls
+22 -18
View File
@@ -4,20 +4,20 @@ LABEL org.opencontainers.image.ref.name="ubuntu"
LABEL org.opencontainers.image.version="24.04"
LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
# TARGETARCH is automatically provided by Docker Buildx
# TARGETARCH and TARGETVARIANT are automatically provided by Docker Buildx
ARG TARGETARCH
ARG TARGETVARIANT
ARG VERSION=8.78
ARG DEBIAN_FRONTEND=noninteractive
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
ENV \
DEBUG=false \
NODE_VERSION=v14.21.4 \
METEOR_RELEASE=METEOR@2.16 \
NODE_VERSION=v24.14.1 \
METEOR_RELEASE=METEOR@3.5-beta.7 \
USE_EDGE=false \
METEOR_EDGE=1.5-beta.17 \
NPM_VERSION=6.14.17 \
FIBERS_VERSION=4.0.1 \
NPM_VERSION=11.11.0 \
SRC_PATH=./ \
WITH_API=true \
MONGO_OPLOG_URL="" \
@@ -169,21 +169,24 @@ apt-get update --assume-yes
apt-get upgrade --assume-yes
apt-get install --assume-yes --no-install-recommends ${BUILD_DEPS}
# Multi-arch mapping logic
# Multi-arch mapping: Docker TARGETARCH -> Node.js arch name + WeKan bundle name
# arm/v7 uses TARGETARCH=arm; armhf has no MongoDB Community -> uses FerretDB
case "${TARGETARCH}" in
"amd64") NODE_ARCH="x64" WEKAN_ARCH="amd64" ;;
"arm64") NODE_ARCH="arm64" WEKAN_ARCH="arm64" ;;
"s390x") NODE_ARCH="s390x" WEKAN_ARCH="s390x" ;;
"amd64") NODE_ARCH="x64" WEKAN_ARCH="amd64" ;;
"arm64") NODE_ARCH="arm64" WEKAN_ARCH="arm64" ;;
"arm") NODE_ARCH="armv7l" WEKAN_ARCH="armhf" ;;
"ppc64le") NODE_ARCH="ppc64le" WEKAN_ARCH="ppc64le" ;;
"s390x") NODE_ARCH="s390x" WEKAN_ARCH="s390x" ;;
*) echo "Unsupported architecture: ${TARGETARCH}"; exit 1 ;;
esac
# Node.js Installation
# Node.js Installation (official nodejs.org builds for Node 22)
cd /tmp
wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz"
wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt"
grep "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt | shasum -a 256 -c -
wget "https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz"
wget "https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc"
grep "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt.asc | shasum -a 256 -c -
tar xzf "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" -C /usr/local --strip-components=1 --no-same-owner
rm -f "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt
rm -f "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt.asc
ln -s "/usr/local/bin/node" "/usr/local/bin/nodejs"
# NPM configuration
@@ -197,9 +200,10 @@ ln -sf $(which bsdtar) $(which tar)
# WeKan Bundle Installation
mkdir -p /home/wekan/app
cd /home/wekan/app
wget "https://github.com/wekan/wekan/releases/download/v8.35/wekan-8.35-${WEKAN_ARCH}.zip"
unzip "wekan-8.35-${WEKAN_ARCH}.zip"
rm "wekan-8.35-${WEKAN_ARCH}.zip"
wget "https://github.com/wekan/wekan/releases/download/v${VERSION}/wekan-${VERSION}-${WEKAN_ARCH}.zip"
unzip "wekan-${VERSION}-${WEKAN_ARCH}.zip"
rm "wekan-${VERSION}-${WEKAN_ARCH}.zip"
npm install --production --no-audit --prefix ./bundle/programs/server
mv /home/wekan/app/bundle /build
# Restore original tar
-98
View File
@@ -1,98 +0,0 @@
FROM arm64v8/ubuntu:23.04 AS builder
#FROM amd64/alpine:latest AS builder
# Set the environment variables for builder
ENV QEMU_VERSION=v7.2.0-1 \
QEMU_ARCHITECTURE=aarch64 \
NODE_ARCHITECTURE=linux-arm64 \
NODE_VERSION=v14.21.4 \
WEKAN_VERSION=latest \
WEKAN_ARCHITECTURE=arm64
# Install dependencies
#RUN apk update && apk add ca-certificates outils-sha1 && \
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
RUN apt update && apt install ca-certificates wget unzip -y && \
\
# Download qemu static for our architecture
wget https://github.com/multiarch/qemu-user-static/releases/download/${QEMU_VERSION}/qemu-${QEMU_ARCHITECTURE}-static.tar.gz -O - | tar -xz && \
\
# Download wekan and shasum
wget https://releases.wekan.team/raspi3/wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
wget https://releases.wekan.team/raspi3/SHA256SUMS.txt && \
# Verify wekan
grep wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip SHA256SUMS.txt | sha256sum -c - && \
\
# Unzip wekan
unzip wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
\
# Download node and shasums
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt && \
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
\
# Verify nodejs authenticity
grep node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz SHASUMS256.txt | sha256sum -c - && \
\
# Extract node and remove tar.gz
tar xvzf node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz
# Build wekan dockerfile
FROM --platform=linux/arm64 arm64v8/ubuntu:23.04
LABEL maintainer="wekan"
# Set the environment variables (defaults where required)
ENV QEMU_ARCHITECTURE=aarch64 \
NODE_ARCHITECTURE=linux-arm64 \
NODE_VERSION=v14.21.4 \
NODE_ENV=production \
NPM_VERSION=latest \
WITH_API=true \
PORT=8080 \
ROOT_URL=http://localhost \
MONGO_URL=mongodb://127.0.0.1:27017/wekan \
LDAP_AUTHENTIFICATION_PASSWORD_FILE="" \
OAUTH2_SECRET_FILE="" \
MAIL_SERVICE_PASSWORD_FILE="" \
MONGO_PASSWORD_FILE="" \
S3_SECRET_FILE=""
# Copy qemu-static to image
COPY --from=builder qemu-${QEMU_ARCHITECTURE}-static /usr/bin
# Copy the app to the image
COPY --from=builder bundle /home/wekan/bundle
# Copy
COPY --from=builder node-${NODE_VERSION}-${NODE_ARCHITECTURE} /opt/nodejs
RUN \
set -o xtrace && \
# Add non-root user wekan
useradd --user-group --system --home-dir /home/wekan wekan && \
\
# Install Node
ln -s /opt/nodejs/bin/node /usr/bin/node && \
ln -s /opt/nodejs/bin/npm /usr/bin/npm && \
mkdir -p /opt/nodejs/lib/node_modules/fibers/.node-gyp /root/.node-gyp/8.16.1 /home/wekan/.config && \
chown wekan --recursive /home/wekan/.config
# \
# # Install Node dependencies
# #npm install -g npm@${NPM_VERSION} && \
# \
# # Install Health Check dependencies
# #apk add curl
#
#HEALTHCHECK --start-period=30s --interval=30s --timeout=10s --retries=3 \
# CMD curl --fail "http://localhost:$PORT" || exit 1
EXPOSE $PORT
USER wekan
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 --max-old-space-size=8192 /home/wekan/bundle/main.js"]
CMD ["bash", "-c", "ulimit -s 65500; exec node /home/wekan/bundle/main.js"]
-99
View File
@@ -1,99 +0,0 @@
FROM arm64v8/ubuntu:23.04 AS builder
#FROM --platform=linux/amd64 amd64/ubuntu:23.04 AS builder
#FROM --platform=linux/amd64 ghcr.io/wekan/wekan:main AS builder
#FROM arm64v8/ubuntu:23.04 AS builder
#FROM amd64/alpine:latest AS builder
# Set the environment variables for builder
ENV QEMU_VERSION=v7.2.0-1 \
QEMU_ARCHITECTURE=s390x \
NODE_ARCHITECTURE=linux-s390x \
NODE_VERSION=v14.21.4 \
WEKAN_VERSION=latest \
WEKAN_ARCHITECTURE=s390x
# Install dependencies
#RUN apk update && apk add ca-certificates outils-sha1 && \
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
RUN apt update && apt install ca-certificates wget unzip -y && \
\
# Download qemu static for our architecture
wget https://github.com/multiarch/qemu-user-static/releases/download/${QEMU_VERSION}/qemu-${QEMU_ARCHITECTURE}-static.tar.gz -O - | tar -xz && \
\
# Download wekan and shasum
wget https://releases.wekan.team/${WEKAN_ARCHITECTURE}/wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
wget https://releases.wekan.team/${WEKAN_ARCHITECTURE}/SHA256SUMS.txt && \
# Verify wekan
grep wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip SHA256SUMS.txt | sha256sum -c - && \
\
# Unzip wekan
unzip wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
\
# Download node and shasums
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt && \
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
\
# Verify nodejs authenticity
grep node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz SHASUMS256.txt | sha256sum -c - && \
\
# Extract node and remove tar.gz
tar xvzf node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz
# Build wekan dockerfile
FROM --platform=linux/s390x s390x/ubuntu:23.04
LABEL maintainer="wekan"
# Set the environment variables (defaults where required)
ENV QEMU_ARCHITECTURE=s390x \
NODE_ARCHITECTURE=linux-s390x \
NODE_VERSION=v14.21.4 \
NODE_ENV=production \
NPM_VERSION=latest \
WITH_API=true \
PORT=8080 \
ROOT_URL=http://localhost \
MONGO_URL=mongodb://127.0.0.1:27017/wekan \
LDAP_AUTHENTIFICATION_PASSWORD_FILE="" \
OAUTH2_SECRET_FILE="" \
MAIL_SERVICE_PASSWORD_FILE="" \
MONGO_PASSWORD_FILE="" \
S3_SECRET_FILE=""
# Copy qemu-static to image
COPY --from=builder qemu-${QEMU_ARCHITECTURE}-static /usr/bin
# Copy the app to the image
COPY --from=builder bundle /home/wekan/bundle
# Copy
COPY --from=builder node-${NODE_VERSION}-${NODE_ARCHITECTURE} /opt/nodejs
RUN \
set -o xtrace && \
# Add non-root user wekan
useradd --user-group --system --home-dir /home/wekan wekan && \
\
# Install Node
ln -s /opt/nodejs/bin/node /usr/bin/node && \
ln -s /opt/nodejs/bin/npm /usr/bin/npm && \
mkdir -p /opt/nodejs/lib/node_modules/fibers/.node-gyp /root/.node-gyp/8.16.1 /home/wekan/.config && \
chown wekan --recursive /home/wekan/.config
# \
# # Install Node dependencies
# #npm install -g npm@${NPM_VERSION} && \
# \
# # Install Health Check dependencies
# #apk add curl
#
#HEALTHCHECK --start-period=30s --interval=30s --timeout=10s --retries=3 \
# CMD curl --fail "http://localhost:$PORT" || exit 1
EXPOSE $PORT
USER wekan
CMD ["bash", "-c", "ulimit -s 65500; exec node /home/wekan/bundle/main.js"]
+20 -4
View File
@@ -1,10 +1,26 @@
## About money
See [CONTRIBUTING](CONTRIBUTING.md)
## Responsible Security Disclosure
- To send email, use [ProtonMail](https://proton.me) email address or use PGP key [security-at-wekan.fi.asc](security-at-wekan.fi.asc)
- Send info about security issue ONLY to security@wekan.fi (that is Protomail email address). NOT TO ANYWHERE ELSE. NO CC, NO BCC.
- Wait for new WeKan release that fixes security issue
- If you approve, we thank you by adding you to Hall of Fame: https://wekan.fi/hall-of-fame/
1. To send email, if possible, use PGP key [security-at-wekan.fi.asc](security-at-wekan.fi.asc)
2. Send info about security issue ONLY to security@wekan.fi . NOT TO ANYWHERE ELSE. NO CC, NO BCC.
3. Wait for new WeKan release that fixes security issue to appear to top of
https://github.com/wekan/wekan/blob/main/CHANGELOG.md
4. We will thank you by adding you to Hall of Fame: https://wekan.fi/hall-of-fame/
5. All vulnerability details will be private to security@wekan.fi ,
unless you help all WeKan platforms to have a way to upgrade, like sending
database migrations code to security@wekan.fi or PRs to https://github.com/wekan/wekan/pulls .
There is no benefit to Wordwide Security Community to have more details about vulnerabilities,
if Worldwide Security Community does not help to make upgrades possible.
6. If there some day becomes available a way to upgrade all WeKan platforms,
this page will be updated to add permission for security researchers
to request new GHSA or CVE ID and publish your vulnerability details at your blog, talks, etc,
and send that info also to security@wekan.fi to be added to
Hall of Fame: https://wekan.fi/hall-of-fame/ to get Upgrade Bonus Point Stars.
In that case, it will become possible for security@wekan.fi to publish all
remaining private security details, and publicly thank Worldwide Security Community.
## Bonus Points
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v8.35.0"
appVersion: "v8.78.0"
files:
userUploads:
- README.md
+7 -2
View File
@@ -5,9 +5,13 @@ if ('serviceWorker' in navigator) {
});
}
// Import Blaze helpers (replaces removed raix:handlebar-helpers package)
import '/client/config/blazeHelpers';
// Import board converter for on-demand conversion
import '/client/lib/boardConverter';
import '/client/components/boardConversionProgress';
import '/client/features/boardConversion';
import { Utils } from '/client/lib/utils';
// Import migration manager and progress UI - COMMENTED OUT
// import '/client/lib/attachmentMigrationManager';
@@ -62,6 +66,7 @@ Meteor.startup(() => {
syncCookie();
}
});
});
// Subscribe to per-user small publications
@@ -72,7 +77,7 @@ Meteor.startup(() => {
Meteor.subscribe('userDesktopDragHandles');
}
});
// Initialize mobile mode on startup for iOS devices
// This ensures mobile mode is applied correctly on page load
Tracker.afterFlush(() => {
+2 -2
View File
@@ -9,11 +9,11 @@ template(name="activities")
+cardActivities
template(name="boardActivities")
each activityData in currentBoard.activities
each activityData in boardActivitiesList
+activity(activity=activityData card=card mode=mode)
template(name="cardActivities")
each activityData in activities
each activityData in cardActivitiesList
+activity(activity=activityData card=card mode=mode)
template(name="activity")
+140 -118
View File
@@ -1,168 +1,192 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import DOMPurify from 'dompurify';
import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
import { TAPi18n } from '/imports/i18n';
import { Utils } from '/client/lib/utils';
import { getSidebarInstance } from '/client/features/sidebar/service';
const activitiesPerPage = 500;
BlazeComponent.extendComponent({
onCreated() {
// XXX Should we use ReactiveNumber?
this.page = new ReactiveVar(1);
this.loadNextPageLocked = false;
// TODO is sidebar always available? E.g. on small screens/mobile devices
const sidebar = Sidebar;
sidebar && sidebar.callFirstWith(null, 'resetNextPeak');
this.autorun(() => {
let mode = this.data()?.mode;
if (mode) {
const capitalizedMode = Utils.capitalize(mode);
let searchId;
const showActivities = this.showActivities();
if (mode === 'linkedcard' || mode === 'linkedboard') {
const currentCard = Utils.getCurrentCard();
searchId = currentCard.linkedId;
mode = mode.replace('linked', '');
} else if (mode === 'card') {
searchId = Utils.getCurrentCardId();
} else {
searchId = Session.get(`current${capitalizedMode}`);
}
const limit = this.page.get() * activitiesPerPage;
if (searchId === null) return;
Template.activities.onCreated(function () {
// Register with sidebar so it can call loadNextPage on us
const Sidebar = getSidebarInstance();
if (Sidebar) {
Sidebar.activitiesInstance = this;
}
this.subscribe('activities', mode, searchId, limit, showActivities, () => {
this.loadNextPageLocked = false;
// TODO the guard can be removed as soon as the TODO above is resolved
if (!sidebar) return;
// If the sibear peak hasn't increased, that mean that there are no more
// activities, and we can stop calling new subscriptions.
// XXX This is hacky! We need to know excatly and reactively how many
// activities there are, we probably want to denormalize this number
// dirrectly into card and board documents.
const nextPeakBefore = sidebar.callFirstWith(null, 'getNextPeak');
sidebar.calculateNextPeak();
const nextPeakAfter = sidebar.callFirstWith(null, 'getNextPeak');
if (nextPeakBefore === nextPeakAfter) {
sidebar.callFirstWith(null, 'resetNextPeak');
}
});
}
});
},
loadNextPage() {
// XXX Should we use ReactiveNumber?
this.page = new ReactiveVar(1);
this.loadNextPageLocked = false;
this.loadNextPage = () => {
if (this.loadNextPageLocked === false) {
this.page.set(this.page.get() + 1);
this.loadNextPageLocked = true;
}
},
showActivities() {
let ret = false;
let mode = this.data()?.mode;
};
// TODO is sidebar always available? E.g. on small screens/mobile devices
const sidebar = getSidebarInstance();
if (sidebar && sidebar.infiniteScrolling) {
sidebar.infiniteScrolling.resetNextPeak();
}
this.autorun(() => {
const data = Template.currentData();
let mode = data?.mode;
if (mode) {
const capitalizedMode = Utils.capitalize(mode);
let searchId;
const showActivities = _showActivities(data);
if (mode === 'linkedcard' || mode === 'linkedboard') {
const currentCard = Utils.getCurrentCard();
ret = currentCard.showActivities ?? false;
searchId = currentCard.linkedId;
mode = mode.replace('linked', '');
} else if (mode === 'card') {
ret = this.data()?.card?.showActivities ?? false;
searchId = Utils.getCurrentCardId();
} else {
ret = Utils.getCurrentBoard().showActivities ?? false;
searchId = Session.get(`current${capitalizedMode}`);
}
}
return ret;
},
activities() {
const ret = this.data().card.activities();
return ret;
},
}).register('activities');
const limit = this.page.get() * activitiesPerPage;
if (searchId === null) return;
BlazeComponent.extendComponent({
this.subscribe('activities', mode, searchId, limit, showActivities, () => {
this.loadNextPageLocked = false;
// TODO the guard can be removed as soon as the TODO above is resolved
if (!sidebar || !sidebar.infiniteScrolling) return;
// If the sidebar peak hasn't increased, that means that there are no more
// activities, and we can stop calling new subscriptions.
const nextPeakBefore = sidebar.infiniteScrolling.getNextPeak();
sidebar.calculateNextPeak();
const nextPeakAfter = sidebar.infiniteScrolling.getNextPeak();
if (nextPeakBefore === nextPeakAfter) {
sidebar.infiniteScrolling.resetNextPeak();
}
});
}
});
});
function _showActivities(data) {
let ret = false;
let mode = data?.mode;
if (mode) {
if (mode === 'linkedcard' || mode === 'linkedboard') {
const currentCard = Utils.getCurrentCard();
ret = currentCard.showActivities ?? false;
} else if (mode === 'card') {
// For card mode, get showActivities from data context or fallback to current card
const card = data?.card || Utils.getCurrentCard();
ret = card?.showActivities ?? false;
} else {
ret = Utils.getCurrentBoard().showActivities ?? false;
}
}
return ret;
}
Template.activities.helpers({
showActivities() {
const data = Template.currentData();
return _showActivities(data);
},
activities() {
const activities = this.card?.activities?.();
return activities || [];
},
});
Template.boardActivities.helpers({
boardActivitiesList() {
const board = Utils.getCurrentBoard();
const activities = board?.activities?.();
return activities || [];
},
});
Template.cardActivities.helpers({
cardActivitiesList() {
const data = Template.currentData();
const card = data?.card;
const activities = card?.activities?.();
return activities || [];
},
});
Template.activity.helpers({
checkItem() {
const checkItemId = this.currentData().activity.checklistItemId;
const checkItemId = this.activity.checklistItemId;
const checkItem = ReactiveCache.getChecklistItem(checkItemId);
return checkItem && checkItem.title;
},
boardLabelLink() {
const data = this.currentData();
const currentBoardId = Session.get('currentBoard');
if (data.mode !== 'board') {
// data.mode: card, linkedcard, linkedboard
return createBoardLink(data.activity.board(), data.activity.listName ? data.activity.listName : null);
if (this.mode !== 'board') {
return createBoardLink(this.activity.board(), this.activity.listName ? this.activity.listName : null);
}
else if (currentBoardId != data.activity.boardId) {
// data.mode: board
// current activitie is linked
return createBoardLink(data.activity.board(), data.activity.listName ? data.activity.listName : null);
else if (currentBoardId != this.activity.boardId) {
return createBoardLink(this.activity.board(), this.activity.listName ? this.activity.listName : null);
}
return TAPi18n.__('this-board');
},
cardLabelLink() {
const data = this.currentData();
const currentBoardId = Session.get('currentBoard');
if (data.mode == 'card') {
// data.mode: card
if (this.mode == 'card') {
return TAPi18n.__('this-card');
}
else if (data.mode !== 'board') {
// data.mode: linkedcard, linkedboard
return createCardLink(data.activity.card(), null);
else if (this.mode !== 'board') {
return createCardLink(this.activity.card(), null);
}
else if (currentBoardId != data.activity.boardId) {
// data.mode: board
// current activitie is linked
return createCardLink(data.activity.card(), data.activity.board().title);
else if (currentBoardId != this.activity.boardId) {
return createCardLink(this.activity.card(), this.activity.board().title);
}
return createCardLink(this.currentData().activity.card(), null);
return createCardLink(this.activity.card(), null);
},
cardLink() {
const data = this.currentData();
const currentBoardId = Session.get('currentBoard');
if (data.mode !== 'board') {
// data.mode: card, linkedcard, linkedboard
return createCardLink(data.activity.card(), null);
if (this.mode !== 'board') {
return createCardLink(this.activity.card(), null);
}
else if (currentBoardId != data.activity.boardId) {
// data.mode: board
// current activitie is linked
return createCardLink(data.activity.card(), data.activity.board().title);
else if (currentBoardId != this.activity.boardId) {
return createCardLink(this.activity.card(), this.activity.board().title);
}
return createCardLink(this.currentData().activity.card(), null);
return createCardLink(this.activity.card(), null);
},
receivedDate() {
const receivedDate = this.currentData().activity.card();
if (!receivedDate) return null;
return receivedDate.receivedAt;
const card = this.activity.card();
if (!card) return null;
return card.receivedAt;
},
startDate() {
const startDate = this.currentData().activity.card();
if (!startDate) return null;
return startDate.startAt;
const card = this.activity.card();
if (!card) return null;
return card.startAt;
},
dueDate() {
const dueDate = this.currentData().activity.card();
if (!dueDate) return null;
return dueDate.dueAt;
const card = this.activity.card();
if (!card) return null;
return card.dueAt;
},
endDate() {
const endDate = this.currentData().activity.card();
if (!endDate) return null;
return endDate.endAt;
const card = this.activity.card();
if (!card) return null;
return card.endAt;
},
lastLabel() {
const lastLabelId = this.currentData().activity.labelId;
const lastLabelId = this.activity.labelId;
if (!lastLabelId) return null;
const lastLabel = ReactiveCache.getBoard(
this.currentData().activity.boardId,
this.activity.boardId,
).getLabelById(lastLabelId);
if (lastLabel && (lastLabel.name === undefined || lastLabel.name === '')) {
return lastLabel.color;
@@ -175,7 +199,7 @@ BlazeComponent.extendComponent({
lastCustomField() {
const lastCustomField = ReactiveCache.getCustomField(
this.currentData().activity.customFieldId,
this.activity.customFieldId,
);
if (!lastCustomField) return null;
return lastCustomField.name;
@@ -183,16 +207,15 @@ BlazeComponent.extendComponent({
lastCustomFieldValue() {
const lastCustomField = ReactiveCache.getCustomField(
this.currentData().activity.customFieldId,
this.activity.customFieldId,
);
if (!lastCustomField) return null;
const value = this.currentData().activity.value;
const value = this.activity.value;
if (
lastCustomField.settings.dropdownItems &&
lastCustomField.settings.dropdownItems.length > 0
) {
const dropDownValue = _.find(
lastCustomField.settings.dropdownItems,
const dropDownValue = lastCustomField.settings.dropdownItems.find(
item => {
return item._id === value;
},
@@ -203,13 +226,13 @@ BlazeComponent.extendComponent({
},
listLabel() {
const activity = this.currentData().activity;
const activity = this.activity;
const list = activity.list();
return (list && list.title) || activity.title;
},
sourceLink() {
const source = this.currentData().activity.source;
const source = this.activity.source;
if (source) {
if (source.url) {
return Blaze.toHTML(
@@ -229,12 +252,12 @@ BlazeComponent.extendComponent({
memberLink() {
return Blaze.toHTMLWithData(Template.memberName, {
user: this.currentData().activity.member(),
user: this.activity.member(),
});
},
attachmentLink() {
const attachment = this.currentData().activity.attachment();
const attachment = this.activity.attachment();
// trying to display url before file is stored generates js errors
return (
(attachment &&
@@ -248,17 +271,16 @@ BlazeComponent.extendComponent({
sanitizeText(attachment.name),
),
)) ||
sanitizeText(this.currentData().activity.attachmentName)
sanitizeText(this.activity.attachmentName)
);
},
customField() {
const customField = this.currentData().activity.customField();
const customField = this.activity.customField();
if (!customField) return null;
return customField.name;
},
}).register('activity');
});
Template.activity.helpers({
sanitize(value) {
+63 -73
View File
@@ -1,95 +1,85 @@
import { ReactiveCache } from '/imports/reactiveCache';
import CardComments from '/models/cardComments';
import { UnsavedEdits } from '/client/lib/unsavedEdits';
import { EscapeActions } from '/client/lib/escapeActions';
import { Utils } from '/client/lib/utils';
import autosize from 'autosize';
const commentFormIsOpen = new ReactiveVar(false);
BlazeComponent.extendComponent({
onDestroyed() {
commentFormIsOpen.set(false);
$('.note-popover').hide();
},
Template.commentForm.onDestroyed(function () {
commentFormIsOpen.set(false);
$('.note-popover').hide();
});
Template.commentForm.helpers({
commentFormIsOpen() {
return commentFormIsOpen.get();
},
});
getInput() {
return this.$('.js-new-comment-input');
Template.commentForm.events({
'submit .js-new-comment-form'(evt, tpl) {
const input = tpl.$('.js-new-comment-input');
const text = input.val().trim();
const card = Template.currentData();
let boardId = card.boardId;
let cardId = card._id;
if (card.isLinkedCard()) {
boardId = ReactiveCache.getCard(card.linkedId).boardId;
cardId = card.linkedId;
} else if (card.isLinkedBoard()) {
boardId = card.linkedId;
}
if (text) {
CardComments.insert({
text,
boardId,
cardId,
});
resetCommentInput(input);
Tracker.flush();
autosize.update(input);
input.trigger('submitted');
}
evt.preventDefault();
},
events() {
return [
{
'submit .js-new-comment-form'(evt) {
const input = this.getInput();
const text = input.val().trim();
const card = this.currentData();
let boardId = card.boardId;
let cardId = card._id;
if (card.isLinkedCard()) {
boardId = ReactiveCache.getCard(card.linkedId).boardId;
cardId = card.linkedId;
} else if (card.isLinkedBoard()) {
boardId = card.linkedId;
}
if (text) {
CardComments.insert({
text,
boardId,
cardId,
});
resetCommentInput(input);
Tracker.flush();
autosize.update(input);
input.trigger('submitted');
}
evt.preventDefault();
},
// Pressing Ctrl+Enter should submit the form
'keydown form textarea'(evt) {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
this.find('button[type=submit]').click();
}
},
},
];
// Pressing Ctrl+Enter should submit the form
'keydown form textarea'(evt, tpl) {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
tpl.find('button[type=submit]').click();
}
},
}).register('commentForm');
});
BlazeComponent.extendComponent({
Template.comments.helpers({
getComments() {
const data = this.data();
const data = Template.currentData();
if (!data || typeof data.comments !== 'function') return [];
return data.comments();
},
}).register("comments");
});
BlazeComponent.extendComponent({
events() {
return [
{
'click .js-delete-comment': Popup.afterConfirm('deleteComment', () => {
const commentId = this.data()._id;
CardComments.remove(commentId);
Popup.back();
}),
'submit .js-edit-comment'(evt) {
evt.preventDefault();
const commentText = this.currentComponent()
.getValue()
.trim();
const commentId = this.data()._id;
if (commentText) {
CardComments.update(commentId, {
$set: {
text: commentText,
},
});
}
Template.comment.events({
'click .js-delete-comment': Popup.afterConfirm('deleteComment', function () {
const commentId = this._id;
CardComments.remove(commentId);
Popup.back();
}),
'submit .js-edit-comment'(evt, tpl) {
evt.preventDefault();
const textarea = tpl.find('.js-edit-comment textarea,input[type=text]');
const commentText = textarea && textarea.value ? textarea.value.trim() : '';
const commentId = this._id;
if (commentText) {
CardComments.update(commentId, {
$set: {
text: commentText,
},
},
];
});
}
},
}).register("comment");
});
// XXX This should be a static method of the `commentForm` component
function resetCommentInput(input) {
+35 -37
View File
@@ -1,11 +1,13 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import Boards from '/models/boards';
import { Utils } from '/client/lib/utils';
BlazeComponent.extendComponent({
onCreated() {
this.subscribe('archivedBoards');
},
Template.archivedBoards.onCreated(function () {
this.subscribe('archivedBoards');
});
Template.archivedBoards.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
@@ -19,38 +21,34 @@ BlazeComponent.extendComponent({
);
return ret;
},
});
events() {
return [
{
async 'click .js-restore-board'() {
// TODO : Make isSandstorm variable global
const isSandstorm =
Meteor.settings &&
Meteor.settings.public &&
Meteor.settings.public.sandstorm;
if (isSandstorm && Utils.getCurrentBoardId()) {
const currentBoard = Utils.getCurrentBoard();
await currentBoard.archive();
}
const board = this.currentData();
await board.restore();
Utils.goBoardId(board._id);
},
'click .js-delete-board': Popup.afterConfirm('boardDelete', async function() {
Popup.back();
const isSandstorm =
Meteor.settings &&
Meteor.settings.public &&
Meteor.settings.public.sandstorm;
if (isSandstorm && Utils.getCurrentBoardId()) {
const currentBoard = Utils.getCurrentBoard();
await Boards.removeAsync(currentBoard._id);
}
await Boards.removeAsync(this._id);
FlowRouter.go('home');
}),
},
];
Template.archivedBoards.events({
async 'click .js-restore-board'() {
// TODO : Make isSandstorm variable global
const isSandstorm =
Meteor.settings &&
Meteor.settings.public &&
Meteor.settings.public.sandstorm;
if (isSandstorm && Utils.getCurrentBoardId()) {
const currentBoard = Utils.getCurrentBoard();
await currentBoard.archive();
}
const board = this;
await board.restore();
Utils.goBoardId(board._id);
},
}).register('archivedBoards');
'click .js-delete-board': Popup.afterConfirm('boardDelete', async function() {
Popup.back();
const isSandstorm =
Meteor.settings &&
Meteor.settings.public &&
Meteor.settings.public.sandstorm;
if (isSandstorm && Utils.getCurrentBoardId()) {
const currentBoard = Utils.getCurrentBoard();
await Boards.removeAsync(currentBoard._id);
}
await Boards.removeAsync(this._id);
FlowRouter.go('home');
}),
});
+4 -4
View File
@@ -1,8 +1,8 @@
template(name="board")
if isConverting.get
if isConverting
+boardConversionProgress
else if isBoardReady.get
else if isBoardReady
if currentBoard
if onlyShowCurrentCard
+cardDetails(currentCard)
@@ -28,10 +28,10 @@ template(name="boardBody")
class="{{#if hasSwimlanes}}dragscroll{{/if}}"
class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
class="{{#if draggingActive.get}}is-dragging-active{{/if}}"
class="{{#if draggingActive}}is-dragging-active{{/if}}"
class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}"
class="{{#if isMiniScreen}}mobile-view{{/if}}")
if showOverlay.get
if showOverlay
.board-overlay
if currentBoard.isTemplatesBoard
each currentBoard.swimlanes
File diff suppressed because it is too large. Load diff
+35 -6
View File
@@ -233,11 +233,11 @@ template(name="createBoard")
label
| {{_ 'title'}}
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
if visibilityMenuIsOpen.get
if visibilityMenuIsOpen
+boardVisibilityList
else
p.quiet
if $eq visibility.get 'public'
if $eq visibility 'public'
span.fa.fa-globe.colorful
= " "
| {{{_ 'board-public-info'}}}
@@ -262,11 +262,40 @@ template(name="createBoardPopup")
label
| {{_ 'title'}}
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
if visibilityMenuIsOpen.get
if visibilityMenuIsOpen
+boardVisibilityList
else
p.quiet
if $eq visibility.get 'public'
if $eq visibility 'public'
span.fa.fa-globe.colorful
= " "
| {{{_ 'board-public-info'}}}
else
span.fa.fa-lock.colorful
= " "
| {{{_ '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="headerBarCreateBoardPopup")
form
label
| {{_ 'title'}}
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
if visibilityMenuIsOpen
+boardVisibilityList
else
p.quiet
if $eq visibility 'public'
span.fa.fa-globe.colorful
= " "
| {{{_ 'board-public-info'}}}
@@ -292,11 +321,11 @@ template(name="createTemplateContainerPopup")
label
| {{_ 'title'}}
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
if visibilityMenuIsOpen.get
if visibilityMenuIsOpen
+boardVisibilityList
else
p.quiet
if $eq visibility.get 'public'
if $eq visibility 'public'
span.fa.fa-globe.colorful
= " "
| {{{_ 'board-public-info'}}}
+340 -350
View File
@@ -2,6 +2,14 @@ import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import dragscroll from '@wekanteam/dragscroll';
import getSlug from 'limax';
import Boards from '/models/boards';
import Swimlanes from '/models/swimlanes';
import TableVisibilityModeSettings from '/models/tableVisibilityModeSettings';
import { Filter } from '/client/lib/filter';
import { MultiSelection } from '/client/lib/multiSelection';
import { getSidebarInstance } from '/client/features/sidebar/service';
import { Utils } from '/client/lib/utils';
/*
const DOWNCLS = 'fa-sort-down';
@@ -11,6 +19,7 @@ const sortCardsBy = new ReactiveVar('');
Template.boardChangeTitlePopup.events({
async submit(event, templateInstance) {
event.preventDefault();
const newTitle = templateInstance
.$('.js-board-name')
.val()
@@ -20,22 +29,22 @@ Template.boardChangeTitlePopup.events({
.val()
.trim();
if (newTitle) {
await this.rename(newTitle);
await this.setDescription(newDesc);
const board = Utils.getCurrentBoard();
if (board) {
await board.rename(newTitle);
await board.setDescription(newDesc);
}
Popup.back();
}
event.preventDefault();
},
});
BlazeComponent.extendComponent({
Template.boardHeaderBar.helpers({
watchLevel() {
const currentBoard = Utils.getCurrentBoard();
return currentBoard && currentBoard.getWatchLevel(Meteor.userId());
},
isStarred() {
const boardId = Session.get('currentBoard');
const user = ReactiveCache.getCurrentUser();
@@ -47,130 +56,7 @@ BlazeComponent.extendComponent({
const currentBoard = Utils.getCurrentBoard();
return currentBoard && currentBoard.stars >= 2;
},
/*
showSort() {
return ReactiveCache.getCurrentUser().hasSortBy();
},
directionClass() {
return this.currentDirection() === -1 ? DOWNCLS : UPCLS;
},
changeDirection() {
const direction = 0 - this.currentDirection() === -1 ? '-' : '';
Meteor.call('setListSortBy', direction + this.currentListSortBy());
},
currentDirection() {
return ReactiveCache.getCurrentUser().getListSortByDirection();
},
currentListSortBy() {
return ReactiveCache.getCurrentUser().getListSortBy();
},
listSortShortDesc() {
return `list-label-short-${this.currentListSortBy()}`;
},
*/
events() {
return [
{
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
'click .js-star-board'() {
const boardId = Session.get('currentBoard');
if (boardId) {
Meteor.call('toggleBoardStar', boardId);
}
},
'click .js-open-board-menu': Popup.open('boardMenu'),
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
'click .js-watch-board': Popup.open('boardChangeWatch'),
'click .js-open-archived-board'() {
Modal.open('archivedBoards');
},
'click .js-toggle-board-view': Popup.open('boardChangeView'),
'click .js-toggle-sidebar'() {
if (process.env.DEBUG === 'true') {
console.log('Hamburger menu clicked');
}
// Use the same approach as keyboard shortcuts
if (typeof Sidebar !== 'undefined' && Sidebar && typeof Sidebar.toggle === 'function') {
if (process.env.DEBUG === 'true') {
console.log('Using Sidebar.toggle()');
}
Sidebar.toggle();
} else {
if (process.env.DEBUG === 'true') {
console.warn('Sidebar not available, trying alternative approach');
}
// Try to trigger the sidebar through the global Blaze helper
if (typeof Blaze !== 'undefined' && Blaze._globalHelpers && Blaze._globalHelpers.Sidebar) {
const sidebar = Blaze._globalHelpers.Sidebar();
if (sidebar && typeof sidebar.toggle === 'function') {
if (process.env.DEBUG === 'true') {
console.log('Using Blaze helper Sidebar.toggle()');
}
sidebar.toggle();
}
}
}
},
'click .js-open-filter-view'() {
if (Sidebar) {
Sidebar.setView('filter');
} else {
console.warn('Sidebar not available for setView');
}
},
'click .js-sort-cards': Popup.open('cardsSort'),
/*
'click .js-open-sort-view'(evt) {
const target = evt.target;
if (target.tagName === 'I') {
// click on the text, popup choices
this.changeDirection();
} else {
// change the sort order
Popup.open('listsort')(evt);
}
},
*/
'click .js-filter-reset'(event) {
event.stopPropagation();
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'() {
if (Sidebar) {
Sidebar.setView('search');
} else {
console.warn('Sidebar not available for setView');
}
},
'click .js-multiselection-activate'() {
const currentCard = Utils.getCurrentCardId();
MultiSelection.activate();
if (currentCard) {
MultiSelection.add(currentCard);
}
},
'click .js-multiselection-reset'(event) {
event.stopPropagation();
MultiSelection.disable();
},
'click .js-log-in'() {
FlowRouter.go('atSignIn');
},
},
];
},
}).register('boardHeaderBar');
Template.boardHeaderBar.helpers({
boardView() {
return Utils.boardView();
},
@@ -196,6 +82,106 @@ Template.boardHeaderBar.helpers({
},
});
Template.boardHeaderBar.events({
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
'click .js-star-board'() {
const boardId = Session.get('currentBoard');
if (boardId) {
Meteor.call('toggleBoardStar', boardId);
}
},
'click .js-open-board-menu': Popup.open('boardMenu'),
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
'click .js-watch-board': Popup.open('boardChangeWatch'),
'click .js-open-archived-board'() {
Modal.open('archivedBoards');
},
'click .js-toggle-board-view': Popup.open('boardChangeView'),
'click .js-toggle-sidebar'() {
if (process.env.DEBUG === 'true') {
console.log('Hamburger menu clicked');
}
// Use the same approach as keyboard shortcuts
const sidebar = getSidebarInstance();
if (sidebar && typeof sidebar.toggle === 'function') {
if (process.env.DEBUG === 'true') {
console.log('Using Sidebar.toggle()');
}
sidebar.toggle();
} else {
if (process.env.DEBUG === 'true') {
console.warn('Sidebar not available, trying alternative approach');
}
// Try to trigger the sidebar through the global Blaze helper
if (typeof Blaze !== 'undefined' && Blaze._globalHelpers && Blaze._globalHelpers.Sidebar) {
const blazeSidebar = Blaze._globalHelpers.Sidebar();
if (blazeSidebar && typeof blazeSidebar.toggle === 'function') {
if (process.env.DEBUG === 'true') {
console.log('Using Blaze helper Sidebar.toggle()');
}
blazeSidebar.toggle();
}
}
}
},
'click .js-open-filter-view'() {
const sidebar = getSidebarInstance();
if (sidebar) {
sidebar.setView('filter');
} else {
console.warn('Sidebar not available for setView');
}
},
'click .js-sort-cards': Popup.open('cardsSort'),
/*
'click .js-open-sort-view'(evt) {
const target = evt.target;
if (target.tagName === 'I') {
// click on the text, popup choices
this.changeDirection();
} else {
// change the sort order
Popup.open('listsort')(evt);
}
},
*/
'click .js-filter-reset'(event) {
event.stopPropagation();
const sidebar = getSidebarInstance();
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'() {
const sidebar = getSidebarInstance();
if (sidebar) {
sidebar.setView('search');
} else {
console.warn('Sidebar not available for setView');
}
},
'click .js-multiselection-activate'() {
const currentCard = Utils.getCurrentCardId();
MultiSelection.activate();
if (currentCard) {
MultiSelection.add(currentCard);
}
},
'click .js-multiselection-reset'(event) {
event.stopPropagation();
MultiSelection.disable();
},
'click .js-log-in'() {
FlowRouter.go('atSignIn');
},
});
Template.boardChangeViewPopup.events({
'click .js-open-lists-view'() {
Utils.setBoardView('board-view-lists');
@@ -215,219 +201,229 @@ Template.boardChangeViewPopup.events({
},
});
const CreateBoard = BlazeComponent.extendComponent({
template() {
return 'createBoard';
},
// Shared setup for all create board popups
function setupCreateBoardState(tpl) {
tpl.visibilityMenuIsOpen = new ReactiveVar(false);
tpl.visibility = new ReactiveVar('private');
tpl.boardId = new ReactiveVar('');
Meteor.subscribe('tableVisibilityModeSettings');
}
onCreated() {
this.visibilityMenuIsOpen = new ReactiveVar(false);
this.visibility = new ReactiveVar('private');
this.boardId = new ReactiveVar('');
Meteor.subscribe('tableVisibilityModeSettings');
},
function createBoardHelpers() {
return {
visibilityMenuIsOpen() {
return Template.instance().visibilityMenuIsOpen.get();
},
visibility() {
return Template.instance().visibility.get();
},
notAllowPrivateVisibilityOnly() {
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
},
visibilityCheck() {
return Template.currentData() === Template.instance().visibility.get();
},
};
}
notAllowPrivateVisibilityOnly(){
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
},
async function createBoardSubmit(tpl, event) {
event.preventDefault();
const title = tpl.find('.js-new-board-title').value;
const slug = getSlug(title) || 'board';
visibilityCheck() {
return this.currentData() === this.visibility.get();
},
const addTemplateContainer = $('#add-template-container.is-checked').length > 0;
if (addTemplateContainer) {
tpl.boardId.set(
await Meteor.callAsync('createBoardWithInitialSwimlanes', {
title,
slug,
permission: 'private',
type: 'template-container',
migrationVersion: 1,
swimlanes: [
{ title: 'Card Templates', sort: 1, type: 'template-container' },
{ title: 'List Templates', sort: 2, type: 'template-container' },
{ title: 'Board Templates', sort: 3, type: 'template-container' },
],
}),
);
setVisibility(visibility) {
this.visibility.set(visibility);
this.visibilityMenuIsOpen.set(false);
},
// Assign to space if one was selected
const spaceId = Session.get('createBoardInWorkspace');
if (spaceId) {
Meteor.call('assignBoardToWorkspace', tpl.boardId.get(), spaceId, (err) => {
if (err) console.error('Error assigning board to space:', err);
});
Session.set('createBoardInWorkspace', null); // Clear after use
}
toggleVisibilityMenu() {
this.visibilityMenuIsOpen.set(!this.visibilityMenuIsOpen.get());
},
FlowRouter.go('board', { id: tpl.boardId.get(), slug });
toggleAddTemplateContainer() {
} else {
const visibility = tpl.visibility.get();
tpl.boardId.set(
await Meteor.callAsync('createBoardWithInitialSwimlanes', {
title,
slug,
permission: visibility,
migrationVersion: 1,
swimlanes: [{ title: 'Default' }],
}),
);
// Assign to space if one was selected
const spaceId = Session.get('createBoardInWorkspace');
if (spaceId) {
Meteor.call('assignBoardToWorkspace', tpl.boardId.get(), spaceId, (err) => {
if (err) console.error('Error assigning board to space:', err);
});
Session.set('createBoardInWorkspace', null); // Clear after use
}
FlowRouter.go('board', { id: tpl.boardId.get(), slug });
}
}
function createBoardEvents() {
return {
'click .js-select-visibility'(event, tpl) {
tpl.visibility.set(Template.currentData());
tpl.visibilityMenuIsOpen.set(false);
},
'click .js-change-visibility'(event, tpl) {
tpl.visibilityMenuIsOpen.set(!tpl.visibilityMenuIsOpen.get());
},
'click .js-import': Popup.open('boardImportBoard'),
async 'submit'(event, tpl) {
await createBoardSubmit(tpl, event);
},
'click .js-import-board': Popup.open('chooseBoardSource'),
'click .js-board-template': Popup.open('searchElement'),
'click .js-toggle-add-template-container'() {
$('#add-template-container').toggleClass('is-checked');
},
};
}
// createBoard (non-popup version)
Template.createBoard.onCreated(function () {
setupCreateBoardState(this);
});
Template.createBoard.helpers(createBoardHelpers());
Template.createBoard.events(createBoardEvents());
// createBoardPopup
Template.createBoardPopup.onCreated(function () {
setupCreateBoardState(this);
});
Template.createBoardPopup.helpers(createBoardHelpers());
Template.createBoardPopup.events(createBoardEvents());
// createTemplateContainerPopup
Template.createTemplateContainerPopup.onCreated(function () {
setupCreateBoardState(this);
});
Template.createTemplateContainerPopup.onRendered(function () {
// Always pre-check the template container checkbox for this popup
$('#add-template-container').addClass('is-checked');
});
Template.createTemplateContainerPopup.helpers(createBoardHelpers());
Template.createTemplateContainerPopup.events(createBoardEvents());
// headerBarCreateBoardPopup
Template.headerBarCreateBoardPopup.onCreated(function () {
setupCreateBoardState(this);
});
Template.headerBarCreateBoardPopup.helpers(createBoardHelpers());
Template.headerBarCreateBoardPopup.events({
'click .js-select-visibility'(event, tpl) {
tpl.visibility.set(Template.currentData());
tpl.visibilityMenuIsOpen.set(false);
},
'click .js-change-visibility'(event, tpl) {
tpl.visibilityMenuIsOpen.set(!tpl.visibilityMenuIsOpen.get());
},
'click .js-import': Popup.open('boardImportBoard'),
async submit(event, tpl) {
await createBoardSubmit(tpl, event);
// Immediately star boards created with the headerbar popup.
await ReactiveCache.getCurrentUser().toggleBoardStar(tpl.boardId.get());
},
'click .js-import-board': Popup.open('chooseBoardSource'),
'click .js-board-template': Popup.open('searchElement'),
'click .js-toggle-add-template-container'() {
$('#add-template-container').toggleClass('is-checked');
},
});
onSubmit(event) {
event.preventDefault();
const title = this.find('.js-new-board-title').value;
Template.boardChangeVisibilityPopup.onCreated(function () {
Meteor.subscribe('tableVisibilityModeSettings');
});
const addTemplateContainer = $('#add-template-container.is-checked').length > 0;
if (addTemplateContainer) {
//const templateContainerId = Meteor.call('setCreateTemplateContainer');
//Utils.goBoardId(templateContainerId);
//alert('niinku template ' + Meteor.call('setCreateTemplateContainer'));
this.boardId.set(
Boards.insert({
// title: TAPi18n.__('templates'),
title: title,
permission: 'private',
type: 'template-container',
migrationVersion: 1, // Latest version - no migration needed
}),
);
// Insert the card templates swimlane
Swimlanes.insert({
// title: TAPi18n.__('card-templates-swimlane'),
title: 'Card Templates',
boardId: this.boardId.get(),
sort: 1,
type: 'template-container',
}),
// Insert the list templates swimlane
Swimlanes.insert(
{
// title: TAPi18n.__('list-templates-swimlane'),
title: 'List Templates',
boardId: this.boardId.get(),
sort: 2,
type: 'template-container',
},
);
// Insert the board templates swimlane
Swimlanes.insert(
{
//title: TAPi18n.__('board-templates-swimlane'),
title: 'Board Templates',
boardId: this.boardId.get(),
sort: 3,
type: 'template-container',
},
);
// Assign to space if one was selected
const spaceId = Session.get('createBoardInWorkspace');
if (spaceId) {
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
if (err) console.error('Error assigning board to space:', err);
});
Session.set('createBoardInWorkspace', null); // Clear after use
}
Utils.goBoardId(this.boardId.get());
} else {
const visibility = this.visibility.get();
this.boardId.set(
Boards.insert({
title,
permission: visibility,
migrationVersion: 1, // Latest version - no migration needed
}),
);
Swimlanes.insert({
title: 'Default',
boardId: this.boardId.get(),
});
// Assign to space if one was selected
const spaceId = Session.get('createBoardInWorkspace');
if (spaceId) {
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
if (err) console.error('Error assigning board to space:', err);
});
Session.set('createBoardInWorkspace', null); // Clear after use
}
Utils.goBoardId(this.boardId.get());
}
},
events() {
return [
{
'click .js-select-visibility'() {
this.setVisibility(this.currentData());
},
'click .js-change-visibility': this.toggleVisibilityMenu,
'click .js-import': Popup.open('boardImportBoard'),
submit: this.onSubmit,
'click .js-import-board': Popup.open('chooseBoardSource'),
'click .js-board-template': Popup.open('searchElement'),
'click .js-toggle-add-template-container': this.toggleAddTemplateContainer,
},
];
},
}).register('createBoardPopup');
(class CreateTemplateContainerPopup extends CreateBoard {
onRendered() {
// Always pre-check the template container checkbox for this popup
$('#add-template-container').addClass('is-checked');
}
}).register('createTemplateContainerPopup');
(class HeaderBarCreateBoard extends CreateBoard {
async onSubmit(event) {
super.onSubmit(event);
// Immediately star boards crated with the headerbar popup.
await ReactiveCache.getCurrentUser().toggleBoardStar(this.boardId.get());
}
}.register('headerBarCreateBoardPopup'));
BlazeComponent.extendComponent({
Template.boardChangeVisibilityPopup.helpers({
notAllowPrivateVisibilityOnly(){
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
},
visibilityCheck() {
const currentBoard = Utils.getCurrentBoard();
return this.currentData() === currentBoard.permission;
return this === currentBoard.permission;
},
});
selectBoardVisibility() {
Template.boardChangeVisibilityPopup.events({
'click .js-select-visibility'() {
const currentBoard = Utils.getCurrentBoard();
const visibility = this.currentData();
currentBoard.setVisibility(visibility);
Popup.back();
const visibility = Template.currentData();
if (typeof visibility === 'string') {
currentBoard.setVisibility(visibility);
Popup.back();
}
},
});
events() {
return [
{
'click .js-select-visibility': this.selectBoardVisibility,
},
];
},
}).register('boardChangeVisibilityPopup');
BlazeComponent.extendComponent({
Template.boardChangeWatchPopup.helpers({
watchLevel() {
const currentBoard = Utils.getCurrentBoard();
return currentBoard.getWatchLevel(Meteor.userId());
},
watchCheck() {
return this.currentData() === this.watchLevel();
const currentBoard = Utils.getCurrentBoard();
return this === currentBoard.getWatchLevel(Meteor.userId());
},
});
events() {
return [
{
'click .js-select-watch'() {
const level = this.currentData();
Meteor.call(
'watch',
'board',
Session.get('currentBoard'),
level,
(err, ret) => {
if (!err && ret) Popup.back();
},
);
Template.boardChangeWatchPopup.events({
'click .js-select-watch'() {
const level = Template.currentData();
if (typeof level === 'string') {
Meteor.call(
'watch',
'board',
Session.get('currentBoard'),
level,
(err, ret) => {
if (!err && ret) Popup.back();
},
},
];
);
}
},
}).register('boardChangeWatchPopup');
});
/*
BlazeComponent.extendComponent({
// BlazeComponent.extendComponent was removed - this code is unused.
// Original listsortPopup component:
// {
onCreated() {
//this.sortBy = new ReactiveVar();
////this.sortDirection = new ReactiveVar();
@@ -495,46 +491,40 @@ BlazeComponent.extendComponent({
},
];
},
}).register('listsortPopup');
// }.register('listsortPopup');
*/
BlazeComponent.extendComponent({
events() {
return [
{
'click .js-sort-due'() {
const sortBy = {
dueAt: 1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('due-date'));
Popup.back();
},
'click .js-sort-title'() {
const sortBy = {
title: 1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('title'));
Popup.back();
},
'click .js-sort-created-asc'() {
const sortBy = {
createdAt: 1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('date-created-newest-first'));
Popup.back();
},
'click .js-sort-created-desc'() {
const sortBy = {
createdAt: -1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('date-created-oldest-first'));
Popup.back();
},
},
];
Template.cardsSortPopup.events({
'click .js-sort-due'() {
const sortBy = {
dueAt: 1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('due-date'));
Popup.back();
},
}).register('cardsSortPopup');
'click .js-sort-title'() {
const sortBy = {
title: 1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('title'));
Popup.back();
},
'click .js-sort-created-asc'() {
const sortBy = {
createdAt: 1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('date-created-newest-first'));
Popup.back();
},
'click .js-sort-created-desc'() {
const sortBy = {
createdAt: -1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('date-created-oldest-first'));
Popup.back();
},
});
+69 -3
View File
@@ -1,4 +1,4 @@
@import url("../../../css/reset.css") print, screen;
@import url("../../../public/css/reset.css") print, screen;
/* Board List Header with Zoom Controls */
.board-list-header {
@@ -69,12 +69,14 @@
font-weight: bold;
margin-top: 12px;
}
.workspaces-header .js-add-space {
.workspaces-header .js-add-space,
.workspaces-header .js-add-workspace {
text-decoration: none;
font-weight: bold;
border: 1px solid #ccc;
border: none;
padding: 2px 8px;
border-radius: 4px;
margin-right: 10px;
}
.workspace-tree {
@@ -130,8 +132,23 @@
}
.workspace-node .workspace-icon {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 1;
vertical-align: middle;
}
.workspace-node .workspace-icon .workspace-icon-value,
.workspace-node .workspace-icon .workspace-icon-value p {
display: inline;
margin: 0;
margin-top: 12px;
}
.workspace-node .workspace-icon .emoji-icon {
margin-top: 0px;
}
.workspace-node .workspace-name {
@@ -165,6 +182,55 @@
background: #e0e0e0;
}
.workspace-node .js-open-workspace-menu {
padding: 2px 6px;
border-radius: 3px;
cursor: pointer;
text-decoration: none;
font-size: 14px;
opacity: 0.7;
transition: opacity 0.2s;
}
.workspace-node .js-open-workspace-menu:hover {
opacity: 1;
background: #e0e0e0;
}
.workspace-actions-form label,
.workspace-subspace-form label {
display: block;
font-weight: 600;
margin-bottom: 8px;
}
.workspace-actions-form input[type='text'],
.workspace-subspace-form input[type='text'] {
width: 100%;
margin-top: 4px;
}
.workspace-actions-form .workspace-color-label {
margin-bottom: 6px;
}
.workspace-actions-form .workspace-color-list {
margin-bottom: 10px;
}
.workspace-delete-block {
display: flex;
flex-direction: column;
gap: 8px;
}
.workspace-delete-confirm {
display: flex;
align-items: center;
gap: 6px;
font-weight: 600;
}
.workspace-node.active > .workspace-node-content .js-select-space,
.workspace-node > .workspace-node-content:hover .js-select-space {
background: #f0f0f0;
+27 -7
View File
@@ -221,19 +221,39 @@ template(name="workspaceTree")
span.emoji-icon
i.fa.fa-arrows
a.js-select-workspace(data-id="{{id}}")
a.js-select-workspace.js-select-space(data-id="{{id}}")
span.workspace-icon
if icon
+viewer
= icon
span.workspace-icon-value(style="{{#if iconColor}}color: {{iconColor}};{{/if}}")
+viewer
= icon
else
span.emoji-icon
span.emoji-icon(style="{{#if iconColor}}color: {{iconColor}};{{/if}}")
i.fa.fa-folder
span.workspace-name= name
a.js-edit-workspace(data-id="{{id}}" title="{{_ 'allboards.edit-workspace'}}")
a.js-open-workspace-menu(data-id="{{id}}" title="{{_ 'allboards.workspace-menu'}}")
span.emoji-icon
i.fa.fa-pencil-square-o
i.fa.fa-bars
span.workspace-count {{workspaceCount id}}
a.js-add-subworkspace(data-id="{{id}}" title="{{_ 'allboards.add-subworkspace'}}") +
if children
+workspaceTree(nodes=children selectedWorkspaceId=selectedWorkspaceId)
template(name="workspaceActionsPopup")
form.workspace-actions-form.js-workspace-actions-form(data-id="{{id}}")
label
| {{_ 'allboards.edit-workspace-name'}}
input.js-workspace-name(type="text" value="{{workspaceName}}" required)
label
| {{_ 'allboards.edit-workspace-icon'}}
input.js-workspace-icon(type="text" value="{{workspaceIcon}}")
input.primary.wide(type="submit" value="{{_ 'save'}}")
hr
form.workspace-subspace-form.js-workspace-subspace-form(data-id="{{id}}")
label
| {{_ 'allboards.add-subworkspace-prompt'}}
input.js-subworkspace-name(type="text")
button.primary.wide(type="submit") {{_ 'allboards.add-subworkspace'}}
hr
.workspace-delete-block
span.js-workspace-delete-error(style="color: #c00; display: block; margin: 4px 0;")
button.js-delete-workspace.negate.wide(type="button" data-id="{{id}}") {{_ 'delete'}}
File diff suppressed because it is too large. Load diff
@@ -1,82 +0,0 @@
<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,57 @@
template(name="originalPositionsView")
.original-positions-view
.original-positions-header
button.btn.btn-sm.btn-outline-secondary.js-toggle-original-positions
i.fa.fa-history
if isShowingOriginalPositions
| Hide Original Positions
else
| Show Original Positions
if isShowingOriginalPositions
button.btn.btn-sm.btn-outline-primary.js-refresh-history
i.fa.fa-refresh
| Refresh
if isShowingOriginalPositions
.original-positions-content
if isLoading
.original-positions-loading
i.fa.fa-spinner.fa-spin
| Loading original positions...
else
.original-positions-filters
.btn-group.btn-group-sm(role="group")
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'all'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="all")
| All
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'swimlane'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="swimlane")
i.fa.fa-bars
| Swimlanes
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'list'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="list")
i.fa.fa-columns
| Lists
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'card'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="card")
i.fa.fa-sticky-note
| Cards
.original-positions-list
each getFilteredHistory
.original-position-item
.original-position-item-header
i.fa(class="{{getEntityTypeIcon entityType}}")
span.entity-type {{getEntityTypeLabel entityType}}
span.entity-name {{getEntityDisplayName this}}
span.entity-id ({{entityId}})
.original-position-item-details
.original-position-description
| {{getEntityOriginalPositionDescription this}}
if originalTitle
.original-title
strong Original title:
| {{originalTitle}}
.original-position-date
small.text-muted Created: {{formatDate createdAt}}
else
.no-original-positions
i.fa.fa-info-circle
| No original position data available for this board.
@@ -1,94 +1,73 @@
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();
}
Template.originalPositionsView.onCreated(function () {
this.showOriginalPositions = new ReactiveVar(false);
this.boardHistory = new ReactiveVar([]);
this.isLoading = new ReactiveVar(false);
this.filterType = new ReactiveVar('all'); // 'all', 'swimlane', 'list', 'card'
loadBoardHistory() {
const tpl = this;
this.loadBoardHistory = function () {
const boardId = Session.get('currentBoard');
if (!boardId) return;
this.isLoading.set(true);
tpl.isLoading.set(true);
Meteor.call('positionHistory.getBoardHistory', boardId, (error, result) => {
this.isLoading.set(false);
tpl.isLoading.set(false);
if (error) {
console.error('Error loading board history:', error);
this.boardHistory.set([]);
tpl.boardHistory.set([]);
} else {
this.boardHistory.set(result);
tpl.boardHistory.set(result);
}
});
}
};
});
toggleOriginalPositions() {
this.showOriginalPositions.set(!this.showOriginalPositions.get());
}
Template.originalPositionsView.onRendered(function () {
this.loadBoardHistory();
});
Template.originalPositionsView.helpers({
isShowingOriginalPositions() {
return this.showOriginalPositions.get();
}
return Template.instance().showOriginalPositions.get();
},
isLoading() {
return this.isLoading.get();
}
return Template.instance().isLoading.get();
},
getBoardHistory() {
return this.boardHistory.get();
}
return Template.instance().boardHistory.get();
},
getFilteredHistory() {
const history = this.getBoardHistory();
const filterType = this.filterType.get();
const tpl = Template.instance();
const history = tpl.boardHistory.get();
const filterType = tpl.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();
}
isFilterType(type) {
return Template.instance().filterType.get() === type;
},
getEntityDisplayName(entity) {
const position = entity.originalPosition || {};
return position.title || `Entity ${entity.entityId}`;
}
},
getEntityOriginalPositionDescription(entity) {
const position = entity.originalPosition || {};
@@ -106,7 +85,7 @@ class OriginalPositionsViewComponent extends BlazeComponent {
}
return description;
}
},
getEntityTypeIcon(entityType) {
switch (entityType) {
@@ -119,7 +98,7 @@ class OriginalPositionsViewComponent extends BlazeComponent {
default:
return 'fa-question';
}
}
},
getEntityTypeLabel(entityType) {
switch (entityType) {
@@ -132,17 +111,24 @@ class OriginalPositionsViewComponent extends BlazeComponent {
default:
return 'Unknown';
}
}
},
formatDate(date) {
return new Date(date).toLocaleString();
}
},
});
refreshHistory() {
this.loadBoardHistory();
}
}
Template.originalPositionsView.events({
'click .js-toggle-original-positions'(evt, tpl) {
tpl.showOriginalPositions.set(!tpl.showOriginalPositions.get());
},
OriginalPositionsViewComponent.register('originalPositionsView');
'click .js-refresh-history'(evt, tpl) {
tpl.loadBoardHistory();
},
export default OriginalPositionsViewComponent;
'click .js-filter-type'(evt, tpl) {
const type = evt.currentTarget.dataset.filterType;
tpl.filterType.set(type);
},
});
+4
View File
@@ -86,6 +86,10 @@ template(name="attachmentGallery")
b
= name
span.file-size ({{fileSize size}})
if uploaderName
div.attachment-uploader {{uploaderName}}
if uploadedAt
div.attachment-uploaded-at {{uploadedAt}}
.attachment-actions
a.js-download(href="{{link}}?download=true", download="{{name}}", title="{{_ 'download'}}")
i.fa.fa-arrow-down
+101 -88
View File
@@ -1,12 +1,15 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { ObjectID } from 'bson';
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';
import Attachments from '/models/attachments';
import { Utils } from '/client/lib/utils';
import { formatDateTime } from '/imports/lib/dateUtils';
const filesize = require('filesize');
const prettyMilliseconds = require('pretty-ms');
const { filesize } = require('filesize');
import prettyMilliseconds from 'pretty-ms';
// We store current card ID and the ID of currently opened attachment in a
// global var. This is used so that we know what's the next attachment to open
@@ -19,7 +22,7 @@ let touchStartCoords = null;
let touchEndCoords = null;
// Stores link to the attachment for which attachment actions popup was opened
attachmentActionsLink = null;
let attachmentActionsLink = null;
Template.attachmentGallery.events({
'click .open-preview'(event) {
@@ -40,8 +43,8 @@ Template.attachmentGallery.events({
attachmentActionsLink = event.currentTarget.getAttribute("data-attachment-link");
},
'click .js-rename': Popup.open('attachmentRename'),
'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete', function() {
Attachments.remove(this._id);
'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete', async function() {
await Attachments.removeAsync(this._id);
Popup.back();
}),
});
@@ -196,8 +199,6 @@ function processTouch(){
xDist = touchEndCoords.x - touchStartCoords.x;
yDist = touchEndCoords.y - touchStartCoords.y;
console.log("xDist: " + xDist);
// Left swipe
if (Math.abs(xDist) > Math.abs(yDist) && xDist < 0) {
openNextAttachment();
@@ -217,14 +218,12 @@ function processTouch(){
Template.attachmentViewer.events({
'touchstart #viewer-container'(event) {
console.log("touchstart")
touchStartCoords = {
x: event.changedTouches[0].screenX,
y: event.changedTouches[0].screenY
}
},
'touchend #viewer-container'(event) {
console.log("touchend")
touchEndCoords = {
x: event.changedTouches[0].screenX,
y: event.changedTouches[0].screenY
@@ -273,6 +272,26 @@ Template.attachmentGallery.helpers({
sanitize(value) {
return sanitizeHTML(value);
},
uploaderName() {
const uploaderId = this.userId;
if (!uploaderId) return '';
const uploader = ReactiveCache.getUser(uploaderId);
if (!uploader) return '';
return uploader.profile && uploader.profile.fullname
? uploader.profile.fullname
: uploader.username || '';
},
uploadedAt() {
if (this.uploadedAtOstrio) {
return formatDateTime(this.uploadedAtOstrio);
}
// Fall back to ObjectId timestamp (first 4 bytes = Unix seconds)
try {
const ts = parseInt(this._id.substring(0, 8), 16) * 1000;
if (!isNaN(ts)) return formatDateTime(new Date(ts));
} catch (_) {}
return '';
},
});
Template.cardAttachmentsPopup.onCreated(function() {
@@ -294,12 +313,12 @@ Template.cardAttachmentsPopup.helpers({
});
Template.cardAttachmentsPopup.events({
'change .js-attach-file'(event, templateInstance) {
async 'change .js-attach-file'(event, templateInstance) {
const card = this;
const files = event.currentTarget.files;
if (files) {
let uploads = [];
const uploaders = handleFileUpload(card, files);
const uploaders = await handleFileUpload(card, files);
uploaders.forEach(uploader => {
uploader.on('start', function() {
@@ -328,7 +347,7 @@ const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
let pastedResults = null;
// Shared upload logic for drag-and-drop functionality
export function handleFileUpload(card, files) {
export async function handleFileUpload(card, files) {
if (!files || files.length === 0) {
return [];
}
@@ -361,7 +380,7 @@ export function handleFileUpload(card, files) {
continue;
}
const fileId = new ObjectID().toString();
const fileId = new ObjectId().toString();
let fileName = sanitizeText(file.name);
// If sanitized filename is not same as original filename,
@@ -385,7 +404,7 @@ export function handleFileUpload(card, files) {
config.meta.fileId = fileId;
try {
const uploader = Attachments.insert(
const uploader = await Attachments.insertAsync(
config,
false,
);
@@ -460,12 +479,12 @@ Template.previewClipboardImagePopup.onRendered(() => {
});
Template.previewClipboardImagePopup.events({
'click .js-upload-pasted-image'() {
async 'click .js-upload-pasted-image'() {
const card = this;
if (pastedResults && pastedResults.file) {
const file = pastedResults.file;
window.oPasted = pastedResults;
const fileId = new ObjectID().toString();
const fileId = new ObjectId().toString();
const config = {
file,
fileId: fileId,
@@ -474,7 +493,7 @@ Template.previewClipboardImagePopup.events({
chunkSize: 'dynamic',
};
config.meta.fileId = fileId;
const uploader = Attachments.insert(
const uploader = await Attachments.insertAsync(
config,
false,
);
@@ -495,9 +514,9 @@ Template.previewClipboardImagePopup.events({
},
});
BlazeComponent.extendComponent({
Template.attachmentActionsPopup.helpers({
isCover() {
const ret = ReactiveCache.getCard(this.data().meta.cardId).coverId == this.data()._id;
const ret = ReactiveCache.getCard(this.meta.cardId).coverId == this._id;
return ret;
},
isBackgroundImage() {
@@ -505,78 +524,72 @@ BlazeComponent.extendComponent({
//return currentBoard.backgroundImageURL === $(".attachment-thumbnail-img").attr("src");
return false;
},
events() {
return [
{
'click .js-add-cover'() {
ReactiveCache.getCard(this.data().meta.cardId).setCover(this.data()._id);
Popup.back();
},
'click .js-remove-cover'() {
ReactiveCache.getCard(this.data().meta.cardId).unsetCover();
Popup.back();
},
'click .js-add-background-image'() {
const currentBoard = Utils.getCurrentBoard();
currentBoard.setBackgroundImageURL(attachmentActionsLink);
Utils.setBackgroundImage(attachmentActionsLink);
Popup.back();
event.preventDefault();
},
'click .js-remove-background-image'() {
const currentBoard = Utils.getCurrentBoard();
currentBoard.setBackgroundImageURL("");
Utils.setBackgroundImage("");
Popup.back();
Utils.reload();
event.preventDefault();
},
'click .js-move-storage-fs'() {
Meteor.call('moveAttachmentToStorage', this.data()._id, "fs");
Popup.back();
},
'click .js-move-storage-gridfs'() {
Meteor.call('moveAttachmentToStorage', this.data()._id, "gridfs");
Popup.back();
},
'click .js-move-storage-s3'() {
Meteor.call('moveAttachmentToStorage', this.data()._id, "s3");
Popup.back();
},
}
]
}
}).register('attachmentActionsPopup');
});
BlazeComponent.extendComponent({
Template.attachmentActionsPopup.events({
'click .js-add-cover'() {
ReactiveCache.getCard(this.meta.cardId).setCover(this._id);
Popup.back();
},
'click .js-remove-cover'() {
ReactiveCache.getCard(this.meta.cardId).unsetCover();
Popup.back();
},
'click .js-add-background-image'(event) {
const currentBoard = Utils.getCurrentBoard();
currentBoard.setBackgroundImageURL(attachmentActionsLink);
Utils.setBackgroundImage(attachmentActionsLink);
Popup.back();
event.preventDefault();
},
'click .js-remove-background-image'(event) {
const currentBoard = Utils.getCurrentBoard();
currentBoard.setBackgroundImageURL("");
Utils.setBackgroundImage("");
Popup.back();
Utils.reload();
event.preventDefault();
},
'click .js-move-storage-fs'() {
Meteor.call('moveAttachmentToStorage', this._id, "fs");
Popup.back();
},
'click .js-move-storage-gridfs'() {
Meteor.call('moveAttachmentToStorage', this._id, "gridfs");
Popup.back();
},
'click .js-move-storage-s3'() {
Meteor.call('moveAttachmentToStorage', this._id, "s3");
Popup.back();
},
});
Template.attachmentRenamePopup.helpers({
getNameWithoutExtension() {
const ret = this.data().name.replace(new RegExp("\." + this.data().extension + "$"), "");
const ret = this.name.replace(new RegExp("\." + this.extension + "$"), "");
return ret;
},
events() {
return [
{
'keydown input.js-edit-attachment-name'(evt) {
// enter = save
if (evt.keyCode === 13) {
this.find('button[type=submit]').click();
}
},
'click button.js-submit-edit-attachment-name'(event) {
// save button pressed
event.preventDefault();
const name = this.$('.js-edit-attachment-name')[0]
.value
.trim() + this.data().extensionWithDot;
if (name === sanitizeText(name)) {
Meteor.call('renameAttachment', this.data()._id, name);
}
Popup.back();
},
}
]
}
}).register('attachmentRenamePopup');
});
Template.attachmentRenamePopup.events({
'keydown input.js-edit-attachment-name'(evt, tpl) {
// enter = save
if (evt.keyCode === 13) {
tpl.find('button[type=submit]').click();
}
},
'click button.js-submit-edit-attachment-name'(event, tpl) {
// save button pressed
event.preventDefault();
const name = tpl.$('.js-edit-attachment-name')[0]
.value
.trim() + this.extensionWithDot;
if (name === sanitizeText(name)) {
Meteor.call('renameAttachment', this._id, name);
}
Popup.back();
},
});
// Template helpers for attachment migration status
Template.registerHelper('attachmentMigrationStatus', function(attachmentId) {
@@ -99,6 +99,21 @@ template(name="cardCustomField-date")
b
| {{showWeek}}
template(name="cardCustomField-datePopup")
.datepicker-container
form.edit-date
.fields
.left
label(for="date") {{_ 'date'}}
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
.right
label(for="time") {{_ 'time'}}
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'}}
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
template(name="cardCustomField-dropdown")
if canModifyCard
+inlinedForm(classNames="js-card-customfield-dropdown")
+223 -236
View File
@@ -1,5 +1,10 @@
import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker';
import {
setupDatePicker,
datePickerRendered,
datePickerHelpers,
datePickerEvents,
} from '/client/lib/datepicker';
import { ReactiveCache } from '/imports/reactiveCache';
import {
formatDateTime,
@@ -22,12 +27,15 @@ import {
fromNow,
calendar
} from '/imports/lib/dateUtils';
import Cards from '/models/cards';
import { CustomFieldStringTemplate } from '/client/lib/customFields'
import { getCurrentCardFromContext } from '/client/lib/currentCard';
import { EscapeActions } from '/client/lib/escapeActions';
import { getSidebarInstance } from '/client/features/sidebar/service';
Template.cardCustomFieldsPopup.helpers({
hasCustomField() {
const card = Utils.getCurrentCard();
const card = getCurrentCardFromContext();
if (!card) return false;
const customFieldId = this._id;
return card.customFieldIndex(customFieldId) > -1;
},
@@ -35,318 +43,297 @@ Template.cardCustomFieldsPopup.helpers({
Template.cardCustomFieldsPopup.events({
'click .js-select-field'(event) {
const card = Utils.getCurrentCard();
const card = getCurrentCardFromContext();
if (!card) return;
const customFieldId = this._id;
card.toggleCustomField(customFieldId);
event.preventDefault();
},
'click .js-settings'(event) {
EscapeActions.executeUpTo('detailsPane');
Sidebar.setView('customFields');
const sidebar = getSidebarInstance();
if (sidebar) {
sidebar.setView('customFields');
}
event.preventDefault();
},
});
// cardCustomField
const CardCustomField = BlazeComponent.extendComponent({
Template.cardCustomField.helpers({
getTemplate() {
return `cardCustomField-${this.data().definition.type}`;
},
onCreated() {
const self = this;
self.card = Utils.getCurrentCard();
self.customFieldId = this.data()._id;
return `cardCustomField-${this.definition.type}`;
},
});
CardCustomField.register('cardCustomField');
Template.cardCustomField.onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
});
// cardCustomField-text
(class extends CardCustomField {
onCreated() {
super.onCreated();
}
Template['cardCustomField-text'].onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
});
events() {
return [
{
'submit .js-card-customfield-text'(event) {
event.preventDefault();
const value = this.currentComponent().getValue();
this.card.setCustomField(this.customFieldId, value);
},
},
];
}
}.register('cardCustomField-text'));
Template['cardCustomField-text'].events({
'submit .js-card-customfield-text'(event, tpl) {
event.preventDefault();
const value = tpl.currentComponent ? tpl.currentComponent().getValue() : tpl.$('textarea').val();
tpl.card.setCustomField(tpl.customFieldId, value);
},
});
// cardCustomField-number
(class extends CardCustomField {
onCreated() {
super.onCreated();
}
Template['cardCustomField-number'].onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
});
events() {
return [
{
'submit .js-card-customfield-number'(event) {
event.preventDefault();
const value = parseInt(this.find('input').value, 10);
this.card.setCustomField(this.customFieldId, value);
},
},
];
}
}.register('cardCustomField-number'));
Template['cardCustomField-number'].events({
'submit .js-card-customfield-number'(event, tpl) {
event.preventDefault();
const value = parseInt(tpl.find('input').value, 10);
tpl.card.setCustomField(tpl.customFieldId, value);
},
});
// cardCustomField-checkbox
(class extends CardCustomField {
onCreated() {
super.onCreated();
}
Template['cardCustomField-checkbox'].onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
});
toggleItem() {
this.card.setCustomField(this.customFieldId, !this.data().value);
}
events() {
return [
{
'click .js-checklist-item .check-box-unicode': this.toggleItem,
'click .js-checklist-item .check-box-container': this.toggleItem,
},
];
}
}.register('cardCustomField-checkbox'));
Template['cardCustomField-checkbox'].events({
'click .js-checklist-item .check-box-unicode'(event, tpl) {
tpl.card.setCustomField(tpl.customFieldId, !Template.currentData().value);
},
'click .js-checklist-item .check-box-container'(event, tpl) {
tpl.card.setCustomField(tpl.customFieldId, !Template.currentData().value);
},
});
// cardCustomField-currency
(class extends CardCustomField {
onCreated() {
super.onCreated();
this.currencyCode = this.data().definition.settings.currencyCode;
}
Template['cardCustomField-currency'].onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
this.currencyCode = Template.currentData().definition.settings.currencyCode;
});
Template['cardCustomField-currency'].helpers({
formattedValue() {
const locale = TAPi18n.getLanguage();
const tpl = Template.instance();
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: this.currencyCode,
}).format(this.data().value);
}
currency: tpl.currencyCode,
}).format(this.value);
},
});
events() {
return [
{
'submit .js-card-customfield-currency'(event) {
event.preventDefault();
// To allow input separated by comma, the comma is replaced by a period.
const value = Number(this.find('input').value.replace(/,/i, '.'), 10);
this.card.setCustomField(this.customFieldId, value);
},
},
];
}
}.register('cardCustomField-currency'));
Template['cardCustomField-currency'].events({
'submit .js-card-customfield-currency'(event, tpl) {
event.preventDefault();
// To allow input separated by comma, the comma is replaced by a period.
const value = Number(tpl.find('input').value.replace(/,/i, '.'), 10);
tpl.card.setCustomField(tpl.customFieldId, value);
},
});
// cardCustomField-date
(class extends CardCustomField {
onCreated() {
super.onCreated();
const self = this;
self.date = ReactiveVar();
self.now = ReactiveVar(now());
window.setInterval(() => {
self.now.set(now());
}, 60000);
Template['cardCustomField-date'].onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
const self = this;
self.date = ReactiveVar();
self.now = ReactiveVar(now());
window.setInterval(() => {
self.now.set(now());
}, 60000);
self.autorun(() => {
self.date.set(new Date(self.data().value));
});
}
self.autorun(() => {
self.date.set(new Date(Template.currentData().value));
});
});
Template['cardCustomField-date'].helpers({
showWeek() {
return getISOWeek(this.date.get()).toString();
}
return getISOWeek(Template.instance().date.get()).toString();
},
showWeekOfYear() {
const user = ReactiveCache.getCurrentUser();
if (!user) {
// For non-logged-in users, week of year is not shown
return false;
}
return user.isShowWeekOfYear();
}
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
showISODate() {
return this.date.get().toISOString();
}
return Template.instance().date.get().toISOString();
},
classes() {
const tpl = Template.instance();
if (
isBefore(this.date.get(), this.now.get(), 'minute') &&
isBefore(this.now.get(), this.data().value, 'minute')
isBefore(tpl.date.get(), tpl.now.get(), 'minute') &&
isBefore(tpl.now.get(), this.value, 'minute')
) {
return 'current';
}
return '';
}
},
showTitle() {
return `${TAPi18n.__('card-start-on')} ${this.date.get().toLocaleString()}`;
}
return `${TAPi18n.__('card-start-on')} ${Template.instance().date.get().toLocaleString()}`;
},
});
events() {
return [
{
'click .js-edit-date': Popup.open('cardCustomField-date'),
},
];
}
}.register('cardCustomField-date'));
Template['cardCustomField-date'].events({
'click .js-edit-date': Popup.open('cardCustomField-date'),
});
// cardCustomField-datePopup
(class extends DatePicker {
onCreated() {
super.onCreated();
const self = this;
self.card = Utils.getCurrentCard();
self.customFieldId = this.data()._id;
this.data().value && this.date.set(new Date(this.data().value));
}
Template['cardCustomField-datePopup'].onCreated(function () {
const data = Template.currentData();
setupDatePicker(this, {
initialDate: data.value ? data.value : undefined,
});
// Override card and store customFieldId for store/delete callbacks
this.datePicker.card = getCurrentCardFromContext();
this.customFieldId = data._id;
});
_storeDate(date) {
this.card.setCustomField(this.customFieldId, date);
}
Template['cardCustomField-datePopup'].onRendered(function () {
datePickerRendered(this);
});
_deleteDate() {
this.card.setCustomField(this.customFieldId, '');
}
}.register('cardCustomField-datePopup'));
Template['cardCustomField-datePopup'].helpers(datePickerHelpers());
Template['cardCustomField-datePopup'].events(datePickerEvents({
storeDate(date) {
this.datePicker.card.setCustomField(this.customFieldId, date);
},
deleteDate() {
this.datePicker.card.setCustomField(this.customFieldId, '');
},
}));
// cardCustomField-dropdown
(class extends CardCustomField {
onCreated() {
super.onCreated();
this._items = this.data().definition.settings.dropdownItems;
this.items = this._items.slice(0);
this.items.unshift({
_id: '',
name: TAPi18n.__('custom-field-dropdown-none'),
});
}
Template['cardCustomField-dropdown'].onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
this._items = Template.currentData().definition.settings.dropdownItems;
this.items = this._items.slice(0);
this.items.unshift({
_id: '',
name: TAPi18n.__('custom-field-dropdown-none'),
});
});
Template['cardCustomField-dropdown'].helpers({
items() {
return Template.instance().items;
},
selectedItem() {
const selected = this._items.find(item => {
return item._id === this.data().value;
const tpl = Template.instance();
const selected = tpl._items.find(item => {
return item._id === this.value;
});
return selected
? selected.name
: TAPi18n.__('custom-field-dropdown-unknown');
}
},
});
events() {
return [
{
'submit .js-card-customfield-dropdown'(event) {
event.preventDefault();
const value = this.find('select').value;
this.card.setCustomField(this.customFieldId, value);
},
},
];
}
}.register('cardCustomField-dropdown'));
Template['cardCustomField-dropdown'].events({
'submit .js-card-customfield-dropdown'(event, tpl) {
event.preventDefault();
const value = tpl.find('select').value;
tpl.card.setCustomField(tpl.customFieldId, value);
},
});
// cardCustomField-stringtemplate
class CardCustomFieldStringTemplate extends CardCustomField {
onCreated() {
super.onCreated();
this.customField = new CustomFieldStringTemplate(this.data().definition);
this.stringtemplateItems = new ReactiveVar(this.data().value ?? []);
}
Template['cardCustomField-stringtemplate'].onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
this.customField = new CustomFieldStringTemplate(Template.currentData().definition);
this.stringtemplateItems = new ReactiveVar(Template.currentData().value ?? []);
});
Template['cardCustomField-stringtemplate'].helpers({
formattedValue() {
const ret = this.customField.getFormattedValue(this.data().value);
const tpl = Template.instance();
const ret = tpl.customField.getFormattedValue(this.value);
return ret;
}
},
stringtemplateItems() {
return Template.instance().stringtemplateItems.get();
},
});
getItems() {
return Array.from(this.findAll('input'))
.map(input => input.value)
.filter(value => !!value.trim());
}
Template['cardCustomField-stringtemplate'].events({
'submit .js-card-customfield-stringtemplate'(event, tpl) {
event.preventDefault();
const items = tpl.stringtemplateItems.get();
tpl.card.setCustomField(tpl.customFieldId, items);
},
events() {
return [
{
'submit .js-card-customfield-stringtemplate'(event) {
event.preventDefault();
const items = this.stringtemplateItems.get();
this.card.setCustomField(this.customFieldId, items);
},
'keydown .js-card-customfield-stringtemplate-item'(event, tpl) {
if (event.keyCode === 13) {
event.preventDefault();
'keydown .js-card-customfield-stringtemplate-item'(event) {
if (event.keyCode === 13) {
event.preventDefault();
if (event.target.value.trim() || event.metaKey || event.ctrlKey) {
const inputLast = tpl.find('input.last');
if (event.target.value.trim() || event.metaKey || event.ctrlKey) {
const inputLast = this.find('input.last');
let items = Array.from(tpl.findAll('input'))
.map(input => input.value)
.filter(value => !!value.trim());
let items = this.getItems();
if (event.target === inputLast) {
inputLast.value = '';
} else if (event.target.nextSibling === inputLast) {
inputLast.focus();
} else {
event.target.blur();
if (event.target === inputLast) {
inputLast.value = '';
} else if (event.target.nextSibling === inputLast) {
inputLast.focus();
} else {
event.target.blur();
const idx = Array.from(tpl.findAll('input')).indexOf(
event.target,
);
items.splice(idx + 1, 0, '');
const idx = Array.from(this.findAll('input')).indexOf(
event.target,
);
items.splice(idx + 1, 0, '');
Tracker.afterFlush(() => {
const element = tpl.findAll('input')[idx + 1];
element.focus();
element.value = '';
});
}
Tracker.afterFlush(() => {
const element = this.findAll('input')[idx + 1];
element.focus();
element.value = '';
});
}
tpl.stringtemplateItems.set(items);
}
if (event.metaKey || event.ctrlKey) {
tpl.find('button[type=submit]').click();
}
}
},
this.stringtemplateItems.set(items);
}
if (event.metaKey || event.ctrlKey) {
this.find('button[type=submit]').click();
}
}
},
'blur .js-card-customfield-stringtemplate-item'(event, tpl) {
if (
!event.target.value.trim() ||
event.target === tpl.find('input.last')
) {
const items = Array.from(tpl.findAll('input'))
.map(input => input.value)
.filter(value => !!value.trim());
tpl.stringtemplateItems.set(items);
tpl.find('input.last').value = '';
}
},
'blur .js-card-customfield-stringtemplate-item'(event) {
if (
!event.target.value.trim() ||
event.target === this.find('input.last')
) {
const items = this.getItems();
this.stringtemplateItems.set(items);
this.find('input.last').value = '';
}
},
'click .js-close-inlined-form'(event) {
this.stringtemplateItems.set(this.data().value ?? []);
},
},
];
}
}
CardCustomFieldStringTemplate.register('cardCustomField-stringtemplate');
'click .js-close-inlined-form'(event, tpl) {
tpl.stringtemplateItems.set(Template.currentData().value ?? []);
},
});
+134
View File
@@ -14,6 +14,70 @@ template(name="dateBadge")
b
| {{showWeek}}
template(name="cardReceivedDate")
if canModifyCard
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="cardStartDate")
if canModifyCard
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="cardDueDate")
if canModifyCard
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="cardEndDate")
if canModifyCard
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="dateCustomField")
a(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
@@ -22,6 +86,46 @@ template(name="dateCustomField")
b
| {{showWeek}}
template(name="cardCustomFieldDate")
a(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="voteEndDate")
if canModifyCard
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="pokerEndDate")
if canModifyCard
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
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}}")
@@ -153,3 +257,33 @@ template(name="editCardEndDatePopup")
.warning {{_ error.get}}
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
template(name="editVoteEndDatePopup")
.datepicker-container
form.edit-date
.fields
.left
label(for="date") {{_ 'date'}}
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
.right
label(for="time") {{_ 'time'}}
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'}}
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
template(name="editPokerEndDatePopup")
.datepicker-container
form.edit-date
.fields
.left
label(for="date") {{_ 'date'}}
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
.right
label(for="time") {{_ 'time'}}
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'}}
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
+510 -352
View File
@@ -1,5 +1,11 @@
import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker';
import { ReactiveCache } from '/imports/reactiveCache';
import {
setupDatePicker,
datePickerRendered,
datePickerHelpers,
datePickerEvents,
} from '/client/lib/datepicker';
import {
formatDateTime,
formatDate,
@@ -23,128 +29,159 @@ import {
diff
} from '/imports/lib/dateUtils';
// --- DatePicker popups (edit date forms) ---
// editCardReceivedDatePopup
(class extends DatePicker {
onCreated() {
super.onCreated(formatDateTime(now()));
this.data().getReceived() &&
this.date.set(new Date(this.data().getReceived()));
}
_storeDate(date) {
this.card.setReceived(formatDateTime(date));
}
_deleteDate() {
this.card.unsetReceived();
}
}.register('editCardReceivedDatePopup'));
// editCardStartDatePopup
(class extends DatePicker {
onCreated() {
super.onCreated(formatDateTime(now()));
this.data().getStart() && this.date.set(new Date(this.data().getStart()));
}
_storeDate(date) {
this.card.setStart(formatDateTime(date));
}
_deleteDate() {
this.card.unsetStart();
}
}.register('editCardStartDatePopup'));
// editCardDueDatePopup
(class extends DatePicker {
onCreated() {
super.onCreated('1970-01-01 17:00:00');
this.data().getDue() && this.date.set(new Date(this.data().getDue()));
}
_storeDate(date) {
this.card.setDue(formatDateTime(date));
}
_deleteDate() {
this.card.unsetDue();
}
}.register('editCardDueDatePopup'));
// editCardEndDatePopup
(class extends DatePicker {
onCreated() {
super.onCreated(formatDateTime(now()));
this.data().getEnd() && this.date.set(new Date(this.data().getEnd()));
}
_storeDate(date) {
this.card.setEnd(formatDateTime(date));
}
_deleteDate() {
this.card.unsetEnd();
}
}.register('editCardEndDatePopup'));
// Display received, start, due & end dates
const CardDate = BlazeComponent.extendComponent({
template() {
return 'dateBadge';
},
onCreated() {
const self = this;
self.date = ReactiveVar();
self.now = ReactiveVar(now());
window.setInterval(() => {
self.now.set(now());
}, 60000);
},
showWeek() {
return getISOWeek(this.date.get()).toString();
},
showWeekOfYear() {
const user = ReactiveCache.getCurrentUser();
if (!user) {
// For non-logged-in users, week of year is not shown
return false;
}
return user.isShowWeekOfYear();
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
},
showISODate() {
return this.date.get().toISOString();
},
Template.editCardReceivedDatePopup.onCreated(function () {
const card = Template.currentData();
setupDatePicker(this, {
defaultTime: formatDateTime(now()),
initialDate: card.getReceived() ? card.getReceived() : undefined,
});
});
class CardReceivedDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(new Date(self.data().getReceived()));
});
}
Template.editCardReceivedDatePopup.onRendered(function () {
datePickerRendered(this);
});
Template.editCardReceivedDatePopup.helpers(datePickerHelpers());
Template.editCardReceivedDatePopup.events(datePickerEvents({
storeDate(date) {
this.datePicker.card.setReceived(date);
},
deleteDate() {
this.datePicker.card.unsetReceived();
},
}));
// editCardStartDatePopup
Template.editCardStartDatePopup.onCreated(function () {
const card = Template.currentData();
setupDatePicker(this, {
defaultTime: formatDateTime(now()),
initialDate: card.getStart() ? card.getStart() : undefined,
});
});
Template.editCardStartDatePopup.onRendered(function () {
datePickerRendered(this);
});
Template.editCardStartDatePopup.helpers(datePickerHelpers());
Template.editCardStartDatePopup.events(datePickerEvents({
storeDate(date) {
this.datePicker.card.setStart(date);
},
deleteDate() {
this.datePicker.card.unsetStart();
},
}));
// editCardDueDatePopup
Template.editCardDueDatePopup.onCreated(function () {
const card = Template.currentData();
setupDatePicker(this, {
defaultTime: '1970-01-01 17:00:00',
initialDate: card.getDue() ? card.getDue() : undefined,
});
});
Template.editCardDueDatePopup.onRendered(function () {
datePickerRendered(this);
});
Template.editCardDueDatePopup.helpers(datePickerHelpers());
Template.editCardDueDatePopup.events(datePickerEvents({
storeDate(date) {
this.datePicker.card.setDue(date);
},
deleteDate() {
this.datePicker.card.unsetDue();
},
}));
// editCardEndDatePopup
Template.editCardEndDatePopup.onCreated(function () {
const card = Template.currentData();
setupDatePicker(this, {
defaultTime: formatDateTime(now()),
initialDate: card.getEnd() ? card.getEnd() : undefined,
});
});
Template.editCardEndDatePopup.onRendered(function () {
datePickerRendered(this);
});
Template.editCardEndDatePopup.helpers(datePickerHelpers());
Template.editCardEndDatePopup.events(datePickerEvents({
storeDate(date) {
this.datePicker.card.setEnd(date);
},
deleteDate() {
this.datePicker.card.unsetEnd();
},
}));
// --- Card date badge display helpers ---
// Shared onCreated logic for card date badge templates
function cardDateOnCreated(tpl) {
tpl.date = new ReactiveVar();
tpl.now = new ReactiveVar(now());
window.setInterval(() => {
tpl.now.set(now());
}, 60000);
}
// Shared helpers for card date badge templates
function cardDateHelpers(extraHelpers) {
const base = {
showWeek() {
return getISOWeek(Template.instance().date.get()).toString();
},
showWeekOfYear() {
const user = ReactiveCache.getCurrentUser();
if (!user) {
return false;
}
return user.isShowWeekOfYear();
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
showISODate() {
return Template.instance().date.get().toISOString();
},
};
return Object.assign(base, extraHelpers);
}
// cardReceivedDate
Template.cardReceivedDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getReceived()));
});
});
Template.cardReceivedDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'received-date ';
const dueAt = this.data().getDue();
const endAt = this.data().getEnd();
const startAt = this.data().getStart();
const theDate = this.date.get();
const now = this.now.get();
const data = Template.currentData();
const dueAt = data.getDue();
const endAt = data.getEnd();
const startAt = data.getStart();
const theDate = tpl.date.get();
// Received date logic: if received date is after start, due, or end dates, it's overdue
if (
(startAt && isAfter(theDate, startAt)) ||
(endAt && isAfter(theDate, endAt)) ||
@@ -155,332 +192,453 @@ class CardReceivedDate extends CardDate {
classes += 'not-due';
}
return classes;
}
},
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
}
},
}));
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editCardReceivedDate'),
});
}
}
CardReceivedDate.register('cardReceivedDate');
Template.cardReceivedDate.events({
'click .js-edit-date': Popup.open('editCardReceivedDate'),
});
class CardStartDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(new Date(self.data().getStart()));
});
}
// cardStartDate
Template.cardStartDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getStart()));
});
});
Template.cardStartDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'start-date ';
const dueAt = this.data().getDue();
const endAt = this.data().getEnd();
const theDate = this.date.get();
const now = this.now.get();
const data = Template.currentData();
const dueAt = data.getDue();
const endAt = data.getEnd();
const theDate = tpl.date.get();
const nowVal = tpl.now.get();
// Start date logic: if start date is after due or end dates, it's overdue
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
classes += 'overdue';
} else if (isAfter(theDate, now)) {
// Start date is in the future - not due yet
} else if (isAfter(theDate, nowVal)) {
classes += 'not-due';
} else {
// Start date is today or in the past - current/active
classes += 'current';
}
return classes;
}
},
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
}
},
}));
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editCardStartDate'),
});
}
}
CardStartDate.register('cardStartDate');
Template.cardStartDate.events({
'click .js-edit-date': Popup.open('editCardStartDate'),
});
class CardDueDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(new Date(self.data().getDue()));
});
}
// cardDueDate
Template.cardDueDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getDue()));
});
});
Template.cardDueDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'due-date ';
const endAt = this.data().getEnd();
const theDate = this.date.get();
const now = this.now.get();
const data = Template.currentData();
const endAt = data.getEnd();
const theDate = tpl.date.get();
const nowVal = tpl.now.get();
// If there's an end date and it's before the due date, task is completed early
if (endAt && isBefore(endAt, theDate)) {
classes += 'completed-early';
}
// If there's an end date, don't show due date status since task is completed
else if (endAt) {
} else if (endAt) {
classes += 'completed';
}
// Due date logic based on current time
else {
const daysDiff = diff(theDate, now, 'days');
} else {
const daysDiff = diff(theDate, nowVal, 'days');
if (daysDiff < 0) {
// Due date is in the past - overdue
classes += 'overdue';
} else if (daysDiff <= 1) {
// Due today or tomorrow - due soon
classes += 'due-soon';
} else {
// Due date is more than 1 day away - not due yet
classes += 'not-due';
}
}
return classes;
}
},
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${TAPi18n.__('card-due-on')} ${formattedDate}`;
}
},
}));
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editCardDueDate'),
});
}
}
CardDueDate.register('cardDueDate');
Template.cardDueDate.events({
'click .js-edit-date': Popup.open('editCardDueDate'),
});
class CardEndDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(new Date(self.data().getEnd()));
});
}
// cardEndDate
Template.cardEndDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getEnd()));
});
});
Template.cardEndDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'end-date ';
const dueAt = this.data().getDue();
const theDate = this.date.get();
const data = Template.currentData();
const dueAt = data.getDue();
const theDate = tpl.date.get();
if (!dueAt) {
// No due date set - just show as completed
classes += 'completed';
} else if (isBefore(theDate, dueAt)) {
// End date is before due date - completed early
classes += 'completed-early';
} else if (isAfter(theDate, dueAt)) {
// End date is after due date - completed late
classes += 'completed-late';
} else {
// End date equals due date - completed on time
classes += 'completed-on-time';
}
return classes;
}
},
showTitle() {
return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`;
}
const tpl = Template.instance();
return `${TAPi18n.__('card-end-on')} ${format(tpl.date.get(), 'LLLL')}`;
},
}));
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editCardEndDate'),
});
}
}
CardEndDate.register('cardEndDate');
Template.cardEndDate.events({
'click .js-edit-date': Popup.open('editCardEndDate'),
});
class CardCustomFieldDate extends CardDate {
template() {
return 'dateCustomField';
}
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(new Date(self.data().value));
});
}
showWeek() {
return getISOWeek(this.date.get()).toString();
}
showWeekOfYear() {
const user = ReactiveCache.getCurrentUser();
if (!user) {
// For non-logged-in users, week of year is not shown
return false;
}
return user.isShowWeekOfYear();
}
// cardCustomFieldDate
Template.cardCustomFieldDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().value));
});
});
Template.cardCustomFieldDate.helpers(cardDateHelpers({
showDate() {
const tpl = Template.instance();
// 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, {
return tpl.date.get().calendar(null, {
sameElse: 'llll',
});
}
},
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${formattedDate}`;
}
},
classes() {
return 'customfield-date';
}
},
}));
events() {
return [];
}
}
CardCustomFieldDate.register('cardCustomFieldDate');
// --- Minicard date templates ---
(class extends CardReceivedDate {
template() {
return 'minicardReceivedDate';
}
// minicardReceivedDate
Template.minicardReceivedDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getReceived()));
});
});
showDate() {
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() {
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() {
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() {
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() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardCustomFieldDate'));
class VoteEndDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(new Date(self.data().getVoteEnd()));
});
}
Template.minicardReceivedDate.helpers(cardDateHelpers({
classes() {
const classes = 'end-date' + ' ';
const tpl = Template.instance();
let classes = 'received-date ';
const data = Template.currentData();
const dueAt = data.getDue();
const endAt = data.getEnd();
const startAt = data.getStart();
const theDate = tpl.date.get();
if (
(startAt && isAfter(theDate, startAt)) ||
(endAt && isAfter(theDate, endAt)) ||
(dueAt && isAfter(theDate, dueAt))
) {
classes += 'overdue';
} else {
classes += 'not-due';
}
return classes;
}
},
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().toLocaleString()}`;
}
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
}));
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editVoteEndDate'),
});
}
}
VoteEndDate.register('voteEndDate');
Template.minicardReceivedDate.events({
'click .js-edit-date': Popup.open('editCardReceivedDate'),
});
class PokerEndDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(new Date(self.data().getPokerEnd()));
});
}
// minicardStartDate
Template.minicardStartDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getStart()));
});
});
Template.minicardStartDate.helpers(cardDateHelpers({
classes() {
const classes = 'end-date' + ' ';
const tpl = Template.instance();
let classes = 'start-date ';
const data = Template.currentData();
const dueAt = data.getDue();
const endAt = data.getEnd();
const theDate = tpl.date.get();
const nowVal = tpl.now.get();
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
classes += 'overdue';
} else if (isAfter(theDate, nowVal)) {
classes += 'not-due';
} else {
classes += 'current';
}
return classes;
}
},
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
},
showDate() {
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')} ${format(this.date.get(), 'LLLL')}`;
}
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
}));
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editPokerEndDate'),
});
}
}
PokerEndDate.register('pokerEndDate');
Template.minicardStartDate.events({
'click .js-edit-date': Popup.open('editCardStartDate'),
});
// minicardDueDate
Template.minicardDueDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getDue()));
});
});
Template.minicardDueDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'due-date ';
const data = Template.currentData();
const endAt = data.getEnd();
const theDate = tpl.date.get();
const nowVal = tpl.now.get();
if (endAt && isBefore(endAt, theDate)) {
classes += 'completed-early';
} else if (endAt) {
classes += 'completed';
} else {
const daysDiff = diff(theDate, nowVal, 'days');
if (daysDiff < 0) {
classes += 'overdue';
} else if (daysDiff <= 1) {
classes += 'due-soon';
} else {
classes += 'not-due';
}
}
return classes;
},
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${TAPi18n.__('card-due-on')} ${formattedDate}`;
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
}));
Template.minicardDueDate.events({
'click .js-edit-date': Popup.open('editCardDueDate'),
});
// minicardEndDate
Template.minicardEndDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getEnd()));
});
});
Template.minicardEndDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'end-date ';
const data = Template.currentData();
const dueAt = data.getDue();
const theDate = tpl.date.get();
if (!dueAt) {
classes += 'completed';
} else if (isBefore(theDate, dueAt)) {
classes += 'completed-early';
} else if (isAfter(theDate, dueAt)) {
classes += 'completed-late';
} else {
classes += 'completed-on-time';
}
return classes;
},
showTitle() {
const tpl = Template.instance();
return `${TAPi18n.__('card-end-on')} ${format(tpl.date.get(), 'LLLL')}`;
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
}));
Template.minicardEndDate.events({
'click .js-edit-date': Popup.open('editCardEndDate'),
});
// minicardCustomFieldDate
Template.minicardCustomFieldDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().value));
});
});
Template.minicardCustomFieldDate.helpers(cardDateHelpers({
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${formattedDate}`;
},
classes() {
return 'customfield-date';
},
}));
// --- Vote and Poker end date badge templates ---
// voteEndDate
Template.voteEndDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getVoteEnd()));
});
});
Template.voteEndDate.helpers(cardDateHelpers({
classes() {
return 'end-date ';
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
showTitle() {
const tpl = Template.instance();
return `${TAPi18n.__('card-end-on')} ${tpl.date.get().toLocaleString()}`;
},
}));
Template.voteEndDate.events({
'click .js-edit-date': Popup.open('editVoteEndDate'),
});
// pokerEndDate
Template.pokerEndDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getPokerEnd()));
});
});
Template.pokerEndDate.helpers(cardDateHelpers({
classes() {
return 'end-date ';
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
showTitle() {
const tpl = Template.instance();
return `${TAPi18n.__('card-end-on')} ${format(tpl.date.get(), 'LLLL')}`;
},
}));
Template.pokerEndDate.events({
'click .js-edit-date': Popup.open('editPokerEndDate'),
});
+20 -28
View File
@@ -1,37 +1,29 @@
const descriptionFormIsOpen = new ReactiveVar(false);
BlazeComponent.extendComponent({
onDestroyed() {
descriptionFormIsOpen.set(false);
$('.note-popover').hide();
},
Template.descriptionForm.onDestroyed(function () {
descriptionFormIsOpen.set(false);
$('.note-popover').hide();
});
Template.descriptionForm.helpers({
descriptionFormIsOpen() {
return descriptionFormIsOpen.get();
},
});
getInput() {
return this.$('.js-new-description-input');
Template.descriptionForm.events({
async 'submit .js-card-description'(event, tpl) {
event.preventDefault();
const description = tpl.currentComponent ? tpl.currentComponent().getValue() : tpl.$('textarea').val();
await this.setDescription(description);
},
events() {
return [
{
async 'submit .js-card-description'(event) {
event.preventDefault();
const description = this.currentComponent().getValue();
await this.data().setDescription(description);
},
// Pressing Ctrl+Enter should submit the form
'keydown form textarea'(evt) {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
const submitButton = this.find('button[type=submit]');
if (submitButton) {
submitButton.click();
}
}
},
},
];
// Pressing Ctrl+Enter should submit the form
'keydown form textarea'(evt, tpl) {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
const submitButton = tpl.find('button[type=submit]');
if (submitButton) {
submitButton.click();
}
}
},
}).register('descriptionForm');
});
+7 -7
View File
@@ -136,12 +136,12 @@ body.desktop-mode .card-details:not(.card-details-popup) {
max-width: 800px;
flex-basis: auto;
border-radius: 8px;
z-index: 100;
z-index: 2001;
}
/* Default position for first card or when dragged */
body.desktop-mode .card-details:not(.card-details-popup):not([style*="left"]):not([style*="top"]) {
top: 50px;
top: 8px;
left: 20px;
right: 20px;
bottom: 20px;
@@ -149,23 +149,23 @@ body.desktop-mode .card-details:not(.card-details-popup):not([style*="left"]):no
/* Stagger positions for multiple cards using nth-of-type */
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(1) {
top: 50px;
top: 8px;
left: 20px;
}
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(2) {
top: 80px;
top: 38px;
left: 50px;
}
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(3) {
top: 110px;
top: 68px;
left: 80px;
}
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(4) {
top: 140px;
top: 98px;
left: 110px;
}
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(5) {
top: 170px;
top: 128px;
left: 140px;
}
+40 -26
View File
@@ -11,31 +11,30 @@ template(name="cardDetails")
+editCardTitleForm
else
unless isMiniScreen
unless isPopup
span.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
if cardCollapsed
i.fa.fa-caret-right
else
i.fa.fa-caret-down
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
i.fa.fa-times-thin
if cardMaximized
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
span.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
if cardCollapsed
i.fa.fa-caret-right
else
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
a.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
i.fa.fa-bars
a.card-copy-button.js-copy-link(
id="cardURL_copy"
title="{{_ 'copy-card-link-to-clipboard'}}"
href="{{ originRelativeUrl }}"
)
span.emoji-icon
i.fa.fa-link
if canModifyCard
span.card-drag-handle.js-card-drag-handle(title="Drag card")
i.fa.fa-arrows
span.copied-tooltip {{_ 'copied'}}
i.fa.fa-caret-down
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
i.fa.fa-times-thin
if cardMaximized
a.fa.fa-window-minimize.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.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
i.fa.fa-bars
a.card-copy-button.js-copy-link(
id="cardURL_copy"
title="{{_ 'copy-card-link-to-clipboard'}}"
href="{{ originRelativeUrl }}"
)
span.emoji-icon
i.fa.fa-link
if canModifyCard
span.card-drag-handle.js-card-drag-handle(title="Drag card")
i.fa.fa-arrows
span.copied-tooltip {{_ 'copied'}}
else
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
i.fa.fa-times-thin
@@ -844,11 +843,26 @@ template(name="cardDetailsActionsPopup")
template(name="exportCardPopup")
ul.pop-over-list
li
a(href="{{exportUrlCardPDF}}",, download="{{exportFilenameCardPDF}}")
i.fa.fa-upload
a(href="{{exportUrlCardPDF}}", download="{{exportFilenameCardPDF}}")
i.fa.fa-file-pdf-o
| {{_ 'export-card-pdf'}}
hr
p.quiet {{_ 'export-card-excel-fields'}}
ul.pop-over-list
each excelExportFields
li(class="{{#if checked}}active{{/if}}")
a.js-excel-field-toggle(data-field="{{field}}" href="#")
i.fa.fa-check
| {{_ label}}
ul.pop-over-list
li
a(href="{{exportUrlCardExcel}}", download="{{exportFilenameCardExcel}}")
i.fa.fa-file-excel-o
| {{_ 'download'}}
template(name="moveCardPopup")
label(for='move-card-title') {{_ 'title'}}:
input#move-card-title.full-line(type="text" value="{{getTitle}}" autofocus)
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,4 +1,4 @@
template(name="editCardSpentTime")
template(name="editCardSpentTimePopup")
.edit-card-time
form.edit-time
.fields
@@ -13,7 +13,7 @@ template(name="editCardSpentTime")
button.primary.wide.left.js-submit-time(type="submit") {{_ 'save'}}
button.js-delete-time.negate.wide.right {{_ 'delete'}}
template(name="timeBadge")
template(name="cardSpentTime")
if canModifyCard
a.js-edit-time.card-time(title="{{_ 'time'}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
| ⏱️ {{showTime}}
+73 -67
View File
@@ -1,85 +1,91 @@
import { TAPi18n } from '/imports/i18n';
import Cards from '/models/cards';
import { getCurrentCardIdFromContext } from '/client/lib/currentCard';
BlazeComponent.extendComponent({
template() {
return 'editCardSpentTime';
function getCardId() {
return getCurrentCardIdFromContext();
}
Template.editCardSpentTimePopup.onCreated(function () {
this.error = new ReactiveVar('');
this.card = Cards.findOne(getCardId());
});
Template.editCardSpentTimePopup.helpers({
error() {
return Template.instance().error;
},
onCreated() {
this.error = new ReactiveVar('');
this.card = this.data();
card() {
return Cards.findOne(getCardId());
},
toggleOvertime() {
this.card.setIsOvertime(!this.card.getIsOvertime());
getIsOvertime() {
const card = Cards.findOne(getCardId());
return card?.getIsOvertime ? card.getIsOvertime() : false;
},
});
Template.editCardSpentTimePopup.events({
//TODO : need checking this portion
'submit .edit-time'(evt, tpl) {
evt.preventDefault();
const card = Cards.findOne(getCardId());
if (!card) return;
const spentTime = parseFloat(evt.target.time.value);
let isOvertime = false;
if ($('#overtime').attr('class').indexOf('is-checked') >= 0) {
isOvertime = true;
}
if (spentTime >= 0) {
card.setSpentTime(spentTime);
card.setIsOvertime(isOvertime);
Popup.back();
} else {
tpl.error.set('invalid-time');
evt.target.time.focus();
}
},
'click .js-delete-time'(evt) {
evt.preventDefault();
const card = Cards.findOne(getCardId());
if (!card) return;
card.setSpentTime(null);
card.setIsOvertime(false);
Popup.back();
},
'click a.js-toggle-overtime'(evt) {
const card = Cards.findOne(getCardId());
if (!card) return;
card.setIsOvertime(!card.getIsOvertime());
$('#overtime .materialCheckBox').toggleClass('is-checked');
$('#overtime').toggleClass('is-checked');
},
storeTime(spentTime, isOvertime) {
this.card.setSpentTime(spentTime);
this.card.setIsOvertime(isOvertime);
},
deleteTime() {
this.card.setSpentTime(null);
this.card.setIsOvertime(false);
},
events() {
return [
{
//TODO : need checking this portion
'submit .edit-time'(evt) {
evt.preventDefault();
});
const spentTime = parseFloat(evt.target.time.value);
//const isOvertime = this.card.getIsOvertime();
let isOvertime = false;
if ($('#overtime').attr('class').indexOf('is-checked') >= 0) {
isOvertime = true;
}
if (spentTime >= 0) {
this.storeTime(spentTime, isOvertime);
Popup.back();
} else {
this.error.set('invalid-time');
evt.target.time.focus();
}
},
'click .js-delete-time'(evt) {
evt.preventDefault();
this.deleteTime();
Popup.back();
},
'click a.js-toggle-overtime': this.toggleOvertime,
},
];
},
}).register('editCardSpentTimePopup');
BlazeComponent.extendComponent({
template() {
return 'timeBadge';
},
onCreated() {
const self = this;
self.time = ReactiveVar();
},
Template.cardSpentTime.helpers({
showTitle() {
if (this.data().getIsOvertime()) {
const card = Cards.findOne(this._id) || this;
if (card.getIsOvertime && card.getIsOvertime()) {
return `${TAPi18n.__(
'overtime',
)} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
} else {
)} ${card.getSpentTime()} ${TAPi18n.__('hours')}`;
} else if (card.getSpentTime) {
return `${TAPi18n.__(
'card-spent',
)} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
)} ${card.getSpentTime()} ${TAPi18n.__('hours')}`;
}
return '';
},
showTime() {
return this.data().getSpentTime();
const card = Cards.findOne(this._id) || this;
return card.getSpentTime ? card.getSpentTime() : '';
},
events() {
return [
{
'click .js-edit-time': Popup.open('editCardSpentTime'),
},
];
getIsOvertime() {
const card = Cards.findOne(this._id) || this;
return card.getIsOvertime ? card.getIsOvertime() : false;
},
}).register('cardSpentTime');
});
Template.cardSpentTime.events({
'click .js-edit-time': Popup.open('editCardSpentTime'),
});
+4
View File
@@ -63,6 +63,10 @@ template(name="checklistDeletePopup")
p {{_ 'confirm-checklist-delete-popup'}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
template(name="checklistItemDeletePopup")
p {{_ 'confirm-checklist-item-delete-popup'}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
template(name="addChecklistItemForm")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
+283 -242
View File
@@ -2,10 +2,15 @@ import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import Cards from '/models/cards';
import Boards from '/models/boards';
import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard';
import ChecklistItems from '/models/checklistItems';
import Checklists from '/models/checklists';
import { BoardSwimlaneListCardDialog } from '/client/lib/dialogWithBoardSwimlaneListCard';
import { EscapeActions } from '/client/lib/escapeActions';
import { Utils } from '/client/lib/utils';
import autosize from 'autosize';
const subManager = new SubsManager();
const { calculateIndexData, capitalize } = Utils;
// SubsManager removed for Meteor 3 migration
const { calculateIndexData } = Utils;
function initSorting(items) {
items.sortable({
@@ -45,55 +50,66 @@ function initSorting(items) {
});
}
BlazeComponent.extendComponent({
onRendered() {
const self = this;
self.itemsDom = this.$('.js-checklist-items');
initSorting(self.itemsDom);
self.itemsDom.mousedown(function (evt) {
evt.stopPropagation();
});
Template.checklistDetail.onRendered(function () {
const tpl = this;
tpl.itemsDom = this.$('.js-checklist-items');
initSorting(tpl.itemsDom);
tpl.itemsDom.mousedown(function (evt) {
evt.stopPropagation();
});
function userIsMember() {
return ReactiveCache.getCurrentUser()?.isBoardMember();
}
function userIsMember() {
return ReactiveCache.getCurrentUser()?.isBoardMember();
}
// Disable sorting if the current user is not a board member
self.autorun(() => {
const $itemsDom = $(self.itemsDom);
if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
$(self.itemsDom).sortable('option', 'disabled', !userIsMember());
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$(self.itemsDom).sortable({
handle: 'span.fa.checklistitem-handle',
});
}
// Disable sorting if the current user is not a board member
tpl.autorun(() => {
const $itemsDom = $(tpl.itemsDom);
if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
$(tpl.itemsDom).sortable('option', 'disabled', !userIsMember());
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$(tpl.itemsDom).sortable({
handle: 'span.fa.checklistitem-handle',
});
}
});
},
}
});
});
Template.checklistDetail.helpers({
/** returns the finished percent of the checklist */
finishedPercent() {
const ret = this.data().checklist.finishedPercent();
const ret = this.checklist.finishedPercent();
return ret;
},
}).register('checklistDetail');
});
BlazeComponent.extendComponent({
addChecklist(event) {
Template.checklists.helpers({
checklists() {
const card = ReactiveCache.getCard(this.cardId);
if (!card || typeof card.checklists !== 'function') {
return [];
}
const ret = card.checklists();
return ret;
},
});
Template.checklists.events({
'click .js-open-checklist-details-menu': Popup.open('checklistActions'),
'submit .js-add-checklist'(event, tpl) {
event.preventDefault();
const textarea = this.find('textarea.js-add-checklist-item');
const textarea = tpl.find('textarea.js-add-checklist-item');
const title = textarea.value.trim();
let cardId = this.currentData().cardId;
let cardId = Template.currentData().cardId;
const card = ReactiveCache.getCard(cardId);
//if (card.isLinked()) cardId = card.linkedId;
if (card.isLinkedCard()) {
cardId = card.linkedId;
}
let sortIndex;
let checklistItemIndex;
if (this.currentData().position === 'top') {
if (Template.currentData().position === 'top') {
sortIndex = Utils.calculateIndexData(null, card.firstChecklist()).base;
checklistItemIndex = 0;
} else {
@@ -107,35 +123,60 @@ BlazeComponent.extendComponent({
title,
sort: sortIndex,
});
this.closeAllInlinedForms();
tpl.$('.js-close-inlined-form').click();
setTimeout(() => {
this.$('.add-checklist-item')
tpl.$('.add-checklist-item')
.eq(checklistItemIndex)
.click();
}, 100);
}
},
addChecklistItem(event) {
'submit .js-edit-checklist-title'(event, tpl) {
event.preventDefault();
const textarea = this.find('textarea.js-add-checklist-item');
const newlineBecomesNewChecklistItem = this.find('input#toggleNewlineBecomesNewChecklistItem');
const newlineBecomesNewChecklistItemOriginOrder = this.find('input#toggleNewlineBecomesNewChecklistItemOriginOrder');
const textarea = tpl.find('textarea.js-edit-checklist-item');
const title = textarea.value.trim();
const checklist = this.currentData().checklist;
const checklist = Template.currentData().checklist;
checklist.setTitle(title);
},
'submit .js-add-checklist-item'(event, tpl) {
event.preventDefault();
const textarea = tpl.find('textarea.js-add-checklist-item');
if (!textarea) {
return;
}
const newlineBecomesNewChecklistItem = tpl.find('input#toggleNewlineBecomesNewChecklistItem');
const newlineBecomesNewChecklistItemOriginOrder = tpl.find('input#toggleNewlineBecomesNewChecklistItemOriginOrder');
const title = textarea.value.trim();
const currentData = Template.currentData() || {};
let resolvedData = currentData;
let checklist = currentData.checklist;
if (!checklist) {
const form = event.currentTarget?.closest
? event.currentTarget.closest('form')
: $(event.target).closest('form').get(0);
const formData = form ? Blaze.getData(form) : null;
if (formData) {
resolvedData = formData;
}
checklist = formData?.checklist;
}
if (!checklist) {
return;
}
if (title) {
let checklistItems = [title];
if (newlineBecomesNewChecklistItem.checked) {
if (newlineBecomesNewChecklistItem?.checked) {
checklistItems = title.split('\n').map(_value => _value.trim());
if (this.currentData().position === 'top') {
if (newlineBecomesNewChecklistItemOriginOrder.checked === false) {
if (resolvedData.position === 'top') {
if (newlineBecomesNewChecklistItemOriginOrder?.checked === false) {
checklistItems = checklistItems.reverse();
}
}
}
let addIndex;
let sortIndex;
if (this.currentData().position === 'top') {
if (resolvedData.position === 'top') {
sortIndex = Utils.calculateIndexData(null, checklist.firstItem()).base;
addIndex = -1;
} else {
@@ -156,33 +197,33 @@ BlazeComponent.extendComponent({
textarea.value = '';
textarea.focus();
},
async deleteItem() {
const checklist = this.currentData().checklist;
const item = this.currentData().item;
if (checklist && item && item._id) {
ChecklistItems.remove(item._id);
'submit .js-edit-checklist-item'(event, tpl) {
event.preventDefault();
const textarea = tpl.find('textarea.js-edit-checklist-item');
const title = textarea.value.trim();
const formData = Blaze.getData(event.currentTarget) || Blaze.getData(event.target);
const item = formData?.item;
if (item) {
item.setTitle(title);
}
},
editChecklist(event) {
event.preventDefault();
const textarea = this.find('textarea.js-edit-checklist-item');
const title = textarea.value.trim();
const checklist = this.currentData().checklist;
checklist.setTitle(title);
'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
'click .js-delete-checklist-item': Popup.afterConfirm('checklistItemDelete', function () {
Popup.back();
const item = this?.item || this;
if (item && item._id) {
ChecklistItems.remove(item._id);
}
}),
// add and delete checklist / checklist-item
'click .js-open-inlined-form'(event, tpl) {
tpl.$('.js-close-inlined-form').click();
},
editChecklistItem(event) {
'click #toggleHideFinishedChecklist'(event) {
event.preventDefault();
const textarea = this.find('textarea.js-edit-checklist-item');
const title = textarea.value.trim();
const item = this.currentData().item;
item.setTitle(title);
Template.currentData().card.toggleHideFinishedChecklist();
},
pressKey(event) {
'keydown textarea.js-add-checklist-item'(event) {
//If user press enter key inside a form, submit it
//Unless the user is also holding down the 'shift' key
if (event.keyCode === 13 && !event.shiftKey) {
@@ -191,201 +232,201 @@ BlazeComponent.extendComponent({
$form.find('button[type=submit]').click();
}
},
});
focusChecklistItem(event) {
// If a new checklist is created, pre-fill the title and select it.
const checklist = this.currentData().checklist;
if (!checklist) {
const textarea = event.target;
textarea.value = capitalize(TAPi18n.__('r-checklist'));
textarea.select();
}
},
// NOTE: boardsSwimlanesAndLists template was removed from jade but JS was left behind.
// This is dead code — the template no longer exists in any jade file.
/** closes all inlined forms (checklist and checklist-item input fields) */
closeAllInlinedForms() {
this.$('.js-close-inlined-form').click();
},
Template.addChecklistItemForm.onRendered(function () {
autosize(this.$('textarea.js-add-checklist-item'));
});
events() {
return [
{
'click .js-open-checklist-details-menu': Popup.open('checklistActions'),
'submit .js-add-checklist': this.addChecklist,
'submit .js-edit-checklist-title': this.editChecklist,
'submit .js-add-checklist-item': this.addChecklistItem,
'submit .js-edit-checklist-item': this.editChecklistItem,
'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
'click .js-delete-checklist-item': this.deleteItem,
'focus .js-add-checklist-item': this.focusChecklistItem,
// add and delete checklist / checklist-item
'click .js-open-inlined-form': this.closeAllInlinedForms,
'click #toggleHideFinishedChecklist'(event) {
event.preventDefault();
this.data().card.toggleHideFinishedChecklist();
},
keydown: this.pressKey,
},
];
},
}).register('checklists');
Template.addChecklistItemForm.events({
'click a.fa.fa-copy'(event, tpl) {
const $editor = tpl.$('textarea');
const promise = Utils.copyTextToClipboard($editor[0].value);
BlazeComponent.extendComponent({
onCreated() {
subManager.subscribe('board', Session.get('currentBoard'), false);
this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
},
boards() {
const ret = ReactiveCache.getBoards(
{
archived: false,
'members.userId': Meteor.userId(),
_id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() },
},
{
sort: { sort: 1 /* boards default sorting */ },
},
);
return ret;
},
swimlanes() {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
return board.swimlanes();
},
aBoardLists() {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
return board.lists();
},
events() {
return [
{
'change .js-select-boards'(event) {
this.selectedBoardId.set($(event.currentTarget).val());
subManager.subscribe('board', this.selectedBoardId.get(), false);
},
},
];
},
}).register('boardsSwimlanesAndLists');
Template.checklists.helpers({
checklists() {
const card = ReactiveCache.getCard(this.cardId);
const ret = card.checklists();
return ret;
const $tooltip = tpl.$('.copied-tooltip');
Utils.showCopied(promise, $tooltip);
},
});
BlazeComponent.extendComponent({
onRendered() {
autosize(this.$('textarea.js-add-checklist-item'));
Template.checklistActionsPopup.events({
'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () {
Popup.back(2);
const checklist = this.checklist;
if (checklist && checklist._id) {
Checklists.remove(checklist._id);
}
}),
'click .js-move-checklist': Popup.open('moveChecklist'),
'click .js-copy-checklist': Popup.open('copyChecklist'),
'click .js-hide-checked-checklist-items'(event) {
event.preventDefault();
Template.currentData().checklist.toggleHideCheckedChecklistItems();
Popup.back();
},
events() {
return [
{
'click a.fa.fa-copy'(event) {
const $editor = this.$('textarea');
const promise = Utils.copyTextToClipboard($editor[0].value);
const $tooltip = this.$('.copied-tooltip');
Utils.showCopied(promise, $tooltip);
},
}
];
}
}).register('addChecklistItemForm');
BlazeComponent.extendComponent({
events() {
return [
{
'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () {
Popup.back(2);
const checklist = this.checklist;
if (checklist && checklist._id) {
Checklists.remove(checklist._id);
}
}),
'click .js-move-checklist': Popup.open('moveChecklist'),
'click .js-copy-checklist': Popup.open('copyChecklist'),
'click .js-hide-checked-checklist-items'(event) {
event.preventDefault();
this.data().checklist.toggleHideCheckedChecklistItems();
Popup.back();
},
'click .js-hide-all-checklist-items'(event) {
event.preventDefault();
this.data().checklist.toggleHideAllChecklistItems();
Popup.back();
},
}
]
}
}).register('checklistActionsPopup');
BlazeComponent.extendComponent({
onRendered() {
autosize(this.$('textarea.js-edit-checklist-item'));
'click .js-hide-all-checklist-items'(event) {
event.preventDefault();
Template.currentData().checklist.toggleHideAllChecklistItems();
Popup.back();
},
events() {
return [
{
'click a.fa.fa-copy'(event) {
const $editor = this.$('textarea');
const promise = Utils.copyTextToClipboard($editor[0].value);
});
const $tooltip = this.$('.copied-tooltip');
Utils.showCopied(promise, $tooltip);
},
}
];
}
}).register('editChecklistItemForm');
Template.editChecklistItemForm.onRendered(function () {
autosize(this.$('textarea.js-edit-checklist-item'));
});
Template.editChecklistItemForm.events({
'click a.fa.fa-copy'(event, tpl) {
const $editor = tpl.$('textarea');
const promise = Utils.copyTextToClipboard($editor[0].value);
const $tooltip = tpl.$('.copied-tooltip');
Utils.showCopied(promise, $tooltip);
},
});
Template.checklistItemDetail.helpers({
});
BlazeComponent.extendComponent({
toggleItem() {
const checklist = this.currentData().checklist;
const item = this.currentData().item;
Template.checklistItemDetail.events({
'click .js-checklist-item .check-box-container'() {
const checklist = Template.currentData().checklist;
const item = Template.currentData().item;
if (checklist && item && item._id) {
item.toggleItem();
}
},
events() {
return [
{
'click .js-checklist-item .check-box-container': this.toggleItem,
},
];
});
/**
* Helper to find the dialog instance from a parent popup template.
* copyAndMoveChecklist is included inside moveChecklistPopup / copyChecklistPopup,
* so we traverse up the view hierarchy to find the parent template's dialog.
*/
function getParentDialog(tpl) {
let view = tpl.view.parentView;
while (view) {
if (view.templateInstance && view.templateInstance() && view.templateInstance().dialog) {
return view.templateInstance().dialog;
}
view = view.parentView;
}
return null;
}
/** Shared helpers for copyAndMoveChecklist sub-template */
Template.copyAndMoveChecklist.helpers({
boards() {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.boards() : [];
},
}).register('checklistItemDetail');
swimlanes() {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.swimlanes() : [];
},
lists() {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.lists() : [];
},
cards() {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.cards() : [];
},
isDialogOptionBoardId(boardId) {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.isDialogOptionBoardId(boardId) : false;
},
isDialogOptionSwimlaneId(swimlaneId) {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.isDialogOptionSwimlaneId(swimlaneId) : false;
},
isDialogOptionListId(listId) {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.isDialogOptionListId(listId) : false;
},
isDialogOptionCardId(cardId) {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.isDialogOptionCardId(cardId) : false;
},
isTitleDefault(title) {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.isTitleDefault(title) : title;
},
});
/**
* Helper: register standard card dialog events on a checklist popup template.
* Events bubble up from the copyAndMoveChecklist sub-template to the parent popup.
*/
function registerChecklistDialogEvents(templateName) {
Template[templateName].events({
async 'click .js-done'(event, tpl) {
const dialog = tpl.dialog;
const boardSelect = tpl.$('.js-select-boards')[0];
const boardId = boardSelect.options[boardSelect.selectedIndex].value;
const listSelect = tpl.$('.js-select-lists')[0];
const listId = listSelect.options[listSelect.selectedIndex].value;
const swimlaneSelect = tpl.$('.js-select-swimlanes')[0];
const swimlaneId = swimlaneSelect.options[swimlaneSelect.selectedIndex].value;
const cardSelect = tpl.$('.js-select-cards')[0];
const cardId = cardSelect.options.length > 0
? cardSelect.options[cardSelect.selectedIndex].value
: null;
const options = { boardId, swimlaneId, listId, cardId };
try {
await dialog.setDone(cardId, options);
} catch (e) {
console.error('Error in card dialog operation:', e);
}
Popup.back(2);
},
'change .js-select-boards'(event, tpl) {
tpl.dialog.getBoardData($(event.currentTarget).val());
},
'change .js-select-swimlanes'(event, tpl) {
tpl.dialog.selectedSwimlaneId.set($(event.currentTarget).val());
tpl.dialog.setFirstListId();
},
'change .js-select-lists'(event, tpl) {
tpl.dialog.selectedListId.set($(event.currentTarget).val());
tpl.dialog.selectedCardId.set('');
},
'change .js-select-cards'(event, tpl) {
tpl.dialog.selectedCardId.set($(event.currentTarget).val());
},
});
}
/** Move Checklist Dialog */
(class extends DialogWithBoardSwimlaneListCard {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
return ret;
}
async setDone(cardId, options) {
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
await this.data().checklist.move(cardId);
}
}).register('moveChecklistPopup');
Template.moveChecklistPopup.onCreated(function () {
this.dialog = new BoardSwimlaneListCardDialog(this, {
getDialogOptions() {
return ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
},
async setDone(cardId, options) {
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
await Template.currentData().checklist.move(cardId);
},
});
});
registerChecklistDialogEvents('moveChecklistPopup');
/** Copy Checklist Dialog */
(class extends DialogWithBoardSwimlaneListCard {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
return ret;
}
async setDone(cardId, options) {
ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
await this.data().checklist.copy(cardId);
}
}).register('copyChecklistPopup');
Template.copyChecklistPopup.onCreated(function () {
this.dialog = new BoardSwimlaneListCardDialog(this, {
getDialogOptions() {
return ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
},
async setDone(cardId, options) {
ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
await Template.currentData().checklist.copy(cardId);
},
});
});
registerChecklistDialogEvents('copyChecklistPopup');
@@ -0,0 +1,6 @@
template(name='inlinedCardDescription')
if isOpen.get
form.inlined-form.js-inlined-form(id=id class=classNames)
+Template.contentBlock
else
+Template.elseBlock
+104 -96
View File
@@ -1,119 +1,117 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { LABEL_COLORS } from '/models/metadata/colors';
import { EscapeActions } from '/client/lib/escapeActions';
import { Utils } from '/client/lib/utils';
let labelColors;
Meteor.startup(() => {
labelColors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
labelColors = LABEL_COLORS;
});
BlazeComponent.extendComponent({
onCreated() {
this.currentColor = new ReactiveVar(this.data().color);
},
const getFallbackLabelColor = () => {
if (Array.isArray(labelColors) && labelColors.length > 0) {
return labelColors[0];
}
return 'green';
};
Template.formLabel.onCreated(function () {
const initialColor = this.data?.color || getFallbackLabelColor();
this.currentColor = new ReactiveVar(initialColor);
});
Template.formLabel.helpers({
labels() {
return labelColors.map(color => ({ color, name: '' }));
const colors = Array.isArray(labelColors) ? labelColors : [getFallbackLabelColor()];
return colors.map(color => ({ color, name: '' }));
},
isSelected(color) {
return this.currentColor.get() === color;
return Template.instance().currentColor.get() === color;
},
});
events() {
return [
{
'click .js-palette-color'() {
this.currentColor.set(this.currentData().color);
},
},
];
Template.formLabel.events({
'click .js-palette-color'(event, tpl) {
const paletteData = Blaze.getData(event.currentTarget);
const selectedColor = paletteData?.color || Template.currentData()?.color || getFallbackLabelColor();
tpl.currentColor.set(selectedColor);
},
}).register('formLabel');
});
Template.createLabelPopup.helpers({
// This is the default color for a new label. We search the first color that
// is not already used in the board (although it's not a problem if two
// labels have the same color).
defaultColor() {
const labels = Utils.getCurrentBoard().labels;
const usedColors = _.pluck(labels, 'color');
const availableColors = _.difference(labelColors, usedColors);
return availableColors.length > 1 ? availableColors[0] : labelColors[0];
const board = Utils.getCurrentBoard();
const colors = Array.isArray(labelColors) ? labelColors : [getFallbackLabelColor()];
const labels = Array.isArray(board?.labels) ? board.labels : [];
const usedColors = labels.map(l => l.color);
const availableColors = colors.filter(c => !usedColors.includes(c));
return availableColors.length > 0 ? availableColors[0] : colors[0];
},
});
BlazeComponent.extendComponent({
onRendered() {
const itemsSelector = 'li.js-card-label-item:not(.placeholder)';
const $labels = this.$('.edit-labels-pop-over');
Template.cardLabelsPopup.onRendered(function () {
const tpl = this;
const itemsSelector = 'li.js-card-label-item:not(.placeholder)';
const $labels = tpl.$('.edit-labels-pop-over');
$labels.sortable({
connectWith: '.edit-labels-pop-over',
tolerance: 'pointer',
appendTo: '.edit-labels-pop-over',
helper(element, currentItem) {
let ret = currentItem.clone();
if (currentItem.closest('.popup-container-depth-0').length == 0)
{ // only set css transform at every sub-popup, not at the main popup
const content = currentItem.closest('.content')[0]
const offsetLeft = content.offsetLeft;
const offsetTop = $('.pop-over > .header').height() * -1;
ret.css("transform", `translate(${offsetLeft}px, ${offsetTop}px)`);
}
return ret;
},
distance: 7,
items: itemsSelector,
placeholder: 'card-label-wrapper placeholder',
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
EscapeActions.clickExecute(evt.target, 'inlinedForm');
},
stop(evt, ui) {
const newLabelOrderOnlyIds = ui.item.parent().children().toArray().map(_element => Blaze.getData(_element)._id)
const card = Blaze.getData(this);
card.board().setNewLabelOrder(newLabelOrderOnlyIds);
},
});
// Disable drag-dropping if the current user is not a board member or is comment only
this.autorun(() => {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$labels.sortable({
handle: '.label-handle',
});
$labels.sortable({
connectWith: '.edit-labels-pop-over',
tolerance: 'pointer',
appendTo: '.edit-labels-pop-over',
helper(element, currentItem) {
let ret = currentItem.clone();
if (currentItem.closest('.popup-container-depth-0').length == 0)
{ // only set css transform at every sub-popup, not at the main popup
const content = currentItem.closest('.content')[0]
const offsetLeft = content.offsetLeft;
const offsetTop = $('.pop-over > .header').height() * -1;
ret.css("transform", `translate(${offsetLeft}px, ${offsetTop}px)`);
}
});
return ret;
},
distance: 7,
items: itemsSelector,
placeholder: 'card-label-wrapper placeholder',
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
EscapeActions.clickExecute(evt.target, 'inlinedForm');
},
stop(evt, ui) {
const newLabelOrderOnlyIds = ui.item.parent().children().toArray().map(_element => Blaze.getData(_element)._id)
const card = Blaze.getData(this);
card.board().setNewLabelOrder(newLabelOrderOnlyIds);
},
});
// Disable drag-dropping if the current user is not a board member or is comment only
tpl.autorun(() => {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$labels.sortable({
handle: '.label-handle',
});
}
});
});
Template.cardLabelsPopup.helpers({
isLabelSelected(cardId) {
return (ReactiveCache.getCard(cardId).labelIds || []).includes(this._id);
},
events() {
return [
{
'click .js-select-label'(event) {
const card = this.data();
const labelId = this.currentData()._id;
card.toggleLabel(labelId);
event.preventDefault();
},
'click .js-edit-label': Popup.open('editLabel'),
'click .js-add-label': Popup.open('createLabel'),
}
];
}
}).register('cardLabelsPopup');
});
Template.cardLabelsPopup.events({
});
Template.formLabel.events({
'click .js-palette-color'(event) {
const $this = $(event.currentTarget);
// hide selected ll colors
$('.js-palette-select').addClass('hide');
// show select color
$this.find('.js-palette-select').removeClass('hide');
'click .js-select-label'(event) {
const card = Template.currentData();
const labelId = this._id;
card.toggleLabel(labelId);
event.preventDefault();
},
'click .js-edit-label': Popup.open('editLabel'),
'click .js-add-label': Popup.open('createLabel'),
});
Template.createLabelPopup.events({
@@ -121,11 +119,19 @@ Template.createLabelPopup.events({
'submit .create-label'(event, templateInstance) {
event.preventDefault();
const board = Utils.getCurrentBoard();
if (!board) {
return;
}
const name = templateInstance
.$('#labelName')
.val()
.trim();
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
const selectedColorIcon = templateInstance.find('.js-palette-color .fa-check');
const selectedPaletteNode = selectedColorIcon?.closest
? selectedColorIcon.closest('.js-palette-color')
: null;
const selectedColorData = selectedPaletteNode && Blaze.getData(selectedPaletteNode);
const color = selectedColorData?.color || getFallbackLabelColor();
board.addLabel(name, color);
Popup.back();
},
@@ -140,18 +146,20 @@ Template.editLabelPopup.events({
'submit .edit-label'(event, templateInstance) {
event.preventDefault();
const board = Utils.getCurrentBoard();
if (!board) {
return;
}
const name = templateInstance
.$('#labelName')
.val()
.trim();
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
const selectedColorIcon = templateInstance.find('.js-palette-color .fa-check');
const selectedPaletteNode = selectedColorIcon?.closest
? selectedColorIcon.closest('.js-palette-color')
: null;
const selectedColorData = selectedPaletteNode && Blaze.getData(selectedPaletteNode);
const color = selectedColorData?.color || getFallbackLabelColor();
board.editLabel(this._id, name, color);
Popup.back();
},
});
Template.cardLabelsPopup.helpers({
isLabelSelected(cardId) {
return _.contains(ReactiveCache.getCard(cardId).labelIds, this._id);
},
});
+33 -27
View File
@@ -11,17 +11,21 @@ template(name="minicard")
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
i.fa.fa-bars
.dates
if getReceived
.date
+minicardReceivedDate
if getStart
.date
+minicardStartDate
if getDue
.date
+minicardDueDate
if getEnd
+minicardEndDate
if showReceived
if getReceived
.date
+minicardReceivedDate
if showStart
if getStart
.date
+minicardStartDate
if showDue
if getDue
.date
+minicardDueDate
if showEnd
if getEnd
+minicardEndDate
if getSpentTime
.date
+cardSpentTime
@@ -68,19 +72,20 @@ template(name="minicard")
span.linked-icon.linked-archived
i.fa.fa-archive
+viewer
if currentBoard.allowsCardNumber
if showCardNumber
span.card-number
| ##{getCardNumber}
= getTitle
if labels
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
each labels
unless hiddenMinicardLabelText
span.js-card-label.card-label(class="card-label-{{color}}" title=name)
+viewer
= name
if hiddenMinicardLabelText
.minicard-label(class="card-label-{{color}}" title="{{name}}")
if showLabels
if labels
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
each labels
unless hiddenMinicardLabelText
span.js-card-label.card-label(class="card-label-{{color}}" title=name)
+viewer
= name
if hiddenMinicardLabelText
.minicard-label(class="card-label-{{color}}" title="{{name}}")
.minicard-custom-fields
each customFieldsWD
@@ -179,12 +184,13 @@ template(name="minicard")
span.badge-icon
i.fa.fa-paperclip
span.badge-text= attachments.length
if allSubtasks.count
.badge
span.badge-icon
i.fa.fa-globe
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
if showSubtasks
if allSubtasks.count
.badge
span.badge-icon
i.fa.fa-globe
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
+178 -189
View File
@@ -3,18 +3,15 @@ import { TAPi18n } from '/imports/i18n';
import { CustomFieldStringTemplate } from '/client/lib/customFields';
import { handleFileUpload } from './attachments';
import uploadProgressManager from '../../lib/uploadProgressManager';
import { Utils } from '/client/lib/utils';
// Template.cards.events({
// 'click .member': Popup.open('cardMember')
// });
BlazeComponent.extendComponent({
template() {
return 'minicard';
},
Template.minicard.helpers({
formattedCurrencyCustomFieldValue(definition) {
const customField = this.data()
const customField = this
.customFieldsWD()
.find(f => f._id === definition._id);
const customFieldTrueValue =
@@ -28,7 +25,7 @@ BlazeComponent.extendComponent({
},
formattedStringtemplateCustomFieldValue(definition) {
const customField = this.data()
const customField = this
.customFieldsWD()
.find(f => f._id === definition._id);
@@ -41,7 +38,7 @@ BlazeComponent.extendComponent({
showCreatorOnMinicard() {
// cache "board" to reduce the mini-mongodb access
const board = this.data().board();
const board = this.board();
let ret = false;
if (board) {
ret = board.allowsCreatorOnMinicard ?? false;
@@ -49,176 +46,55 @@ BlazeComponent.extendComponent({
return ret;
},
isWatching() {
const card = this.currentData();
return card.findWatcher(Meteor.userId());
return this.findWatcher(Meteor.userId());
},
showMembers() {
// cache "board" to reduce the mini-mongodb access
const board = this.data().board();
const board = this.board();
let ret = false;
if (board) {
ret =
board.allowsMembers === null ||
board.allowsMembers === undefined ||
board.allowsMembers
;
ret = board.allowsMembersOnMinicard ?? false;
}
return ret;
},
showAssignee() {
// cache "board" to reduce the mini-mongodb access
const board = this.data().board();
const board = this.board();
let ret = false;
if (board) {
ret =
board.allowsAssignee === null ||
board.allowsAssignee === undefined ||
board.allowsAssignee
;
ret = board.allowsAssigneeOnMinicard ?? false;
}
return ret;
},
/** opens the card label popup only if clicked onto a label
* <li> this is necessary to have the data context of the minicard.
* if .js-card-label is used at click event, then only the data context of the label itself is available at this.currentData()
*/
cardLabelsPopup(event) {
if (this.find('.js-card-label:hover')) {
Popup.open("cardLabels")(event, {dataContextIfCurrentDataIsUndefined: this.currentData()});
}
showReceived() {
const board = this.board();
return board ? board.allowsReceivedDateOnMinicard : false;
},
showStart() {
const board = this.board();
return board ? board.allowsStartDateOnMinicard : false;
},
showDue() {
const board = this.board();
return board ? board.allowsDueDateOnMinicard : false;
},
showEnd() {
const board = this.board();
return board ? board.allowsEndDateOnMinicard : false;
},
showLabels() {
const board = this.board();
return board ? board.allowsLabelsOnMinicard : false;
},
showCardNumber() {
const board = this.board();
return board ? board.allowsCardNumberOnMinicard : false;
},
showSubtasks() {
const board = this.board();
return board ? board.allowsSubtasksOnMinicard : false;
},
async toggleChecklistItem() {
const item = this.currentData();
if (item && item._id) {
await item.toggleItem();
}
},
events() {
return [
{
'click .js-linked-link'() {
if (this.data().isLinkedCard()) Utils.goCardId(this.data().linkedId);
else if (this.data().isLinkedBoard())
Utils.goBoardId(this.data().linkedId);
},
'click .js-toggle-minicard-label-text'() {
if (window.localStorage.getItem('hiddenMinicardLabelText')) {
window.localStorage.removeItem('hiddenMinicardLabelText'); //true
} else {
window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true
}
},
'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
'click .minicard-labels' : this.cardLabelsPopup,
'click .js-open-minicard-details-menu'(event) {
event.preventDefault();
event.stopPropagation();
Popup.open('cardDetailsActions').call(this, event);
},
// Drag and drop file upload handlers
'dragover .minicard'(event) {
// 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) {
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) {
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) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
const card = this.data();
const board = card.board();
// 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);
}
}
},
}
];
},
}).register('minicard');
BlazeComponent.extendComponent({
template() {
return 'minicardChecklist';
},
events() {
return [
{
'click .js-open-checklist-menu'(event) {
const data = this.currentData();
const checklist = data.checklist || data;
const card = data.card || this.data();
const context = { currentData: () => ({ checklist, card }) };
Popup.open('checklistActions').call(context, event);
},
},
];
},
visibleItems() {
const checklist = this.currentData().checklist || this.currentData();
const items = checklist.items();
return items.filter(item => {
// Hide finished items if hideCheckedChecklistItems is true
if (item.isFinished && checklist.hideCheckedChecklistItems) {
return false;
}
// Hide all items if hideAllChecklistItems is true
if (checklist.hideAllChecklistItems) {
return false;
}
return true;
});
},
}).register('minicardChecklist');
Template.minicard.helpers({
hiddenMinicardLabelText() {
const currentUser = ReactiveCache.getCurrentUser();
if (currentUser) {
@@ -229,15 +105,23 @@ Template.minicard.helpers({
return false;
}
},
cover() {
if (!this.coverId) return null;
const attachment = ReactiveCache.getAttachment(this.coverId);
if (!attachment) return null;
const coverId = this.coverId;
return {
link() {
return `/cdn/storage/attachments/${coverId}`;
},
};
},
// XXX resolve this nasty hack for https://github.com/veliovgroup/Meteor-Files/issues/763
sess() {
return Meteor.connection && Meteor.connection._lastSessionId
? Meteor.connection._lastSessionId
: null;
},
isWatching() {
return this.findWatcher(Meteor.userId());
},
// Upload progress helpers
hasActiveUploads() {
return uploadProgressManager.hasActiveUploads(this._id);
@@ -283,30 +167,135 @@ Template.minicard.helpers({
}
});
BlazeComponent.extendComponent({
events() {
return [
{
'keydown input.js-edit-card-sort-popup'(evt) {
// enter = save
if (evt.keyCode === 13) {
this.find('button[type=submit]').click();
}
},
'click button.js-submit-edit-card-sort-popup'(event) {
// save button pressed
event.preventDefault();
const sort = this.$('.js-edit-card-sort-popup')[0]
.value
.trim();
if (!Number.isNaN(sort)) {
let card = this.data();
card.move(card.boardId, card.swimlaneId, card.listId, sort);
Popup.back();
}
},
Template.minicard.events({
'click .js-linked-link'() {
if (this.isLinkedCard()) Utils.goCardId(this.linkedId);
else if (this.isLinkedBoard())
Utils.goBoardId(this.linkedId);
},
'click .js-toggle-minicard-label-text'() {
if (window.localStorage.getItem('hiddenMinicardLabelText')) {
window.localStorage.removeItem('hiddenMinicardLabelText'); //true
} else {
window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true
}
},
'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
'click .minicard-labels'(event, tpl) {
if (tpl.find('.js-card-label:hover')) {
Popup.open("cardLabels")(event, {dataContextIfCurrentDataIsUndefined: Template.currentData()});
}
},
'click .js-open-minicard-details-menu'(event, tpl) {
event.preventDefault();
event.stopPropagation();
const card = Template.currentData();
Popup.open('cardDetailsActions').call({currentData: () => card}, event);
},
// Drag and drop file upload handlers
'dragover .minicard'(event) {
// 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) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
const card = this;
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');
}
]
}
}).register('editCardSortOrderPopup');
}
},
'dragleave .minicard'(event) {
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) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
const card = this;
const board = card.board();
// 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);
}
}
},
});
Template.minicardChecklist.helpers({
visibleItems() {
const checklist = this.checklist || this;
const items = checklist.items();
return items.filter(item => {
// Hide finished items if hideCheckedChecklistItems is true
if (item.isFinished && checklist.hideCheckedChecklistItems) {
return false;
}
// Hide all items if hideAllChecklistItems is true
if (checklist.hideAllChecklistItems) {
return false;
}
return true;
});
},
});
Template.minicardChecklist.events({
'click .js-open-checklist-menu'(event) {
const data = Template.currentData();
const checklist = data.checklist || data;
const card = data.card || this;
const context = { currentData: () => ({ checklist, card }) };
Popup.open('checklistActions').call(context, event);
},
});
Template.editCardSortOrderPopup.events({
'keydown input.js-edit-card-sort-popup'(evt, tpl) {
// enter = save
if (evt.keyCode === 13) {
tpl.find('button[type=submit]').click();
}
},
'click button.js-submit-edit-card-sort-popup'(event, tpl) {
// save button pressed
event.preventDefault();
const sort = tpl.$('.js-edit-card-sort-popup')[0]
.value
.trim();
if (!Number.isNaN(sort)) {
let card = this;
card.move(card.boardId, card.swimlaneId, card.listId, sort);
Popup.back();
}
},
});
+54 -22
View File
@@ -1,35 +1,67 @@
import { ReactiveCache } from '/imports/reactiveCache';
Template.resultCard.helpers({
userId() {
return Meteor.userId();
},
getBoard() {
const card = Template.currentData();
try {
if (card && card.boardId) {
const board = ReactiveCache.getBoard(card.boardId);
return board || null;
}
} catch (err) {
// Silently handle errors
}
return null;
},
getSwimlane() {
const card = Template.currentData();
try {
if (card && card.swimlaneId) {
const swimlane = ReactiveCache.getSwimlane(card.swimlaneId);
return swimlane || null;
}
} catch (err) {
// Silently handle errors
}
return null;
},
getList() {
const card = Template.currentData();
try {
if (card && card.listId) {
const list = ReactiveCache.getList(card.listId);
return list || null;
}
} catch (err) {
// Silently handle errors
}
return null;
},
originRelativeUrl() {
const card = Template.currentData();
if (card && card.boardId && card._id) {
return `/board/${card.boardId}?cardId=${card._id}`;
}
return '#';
},
});
BlazeComponent.extendComponent({
clickOnMiniCard(evt) {
evt.preventDefault();
const this_ = this;
const cardId = this.currentData()._id;
const boardId = this.currentData().boardId;
Template.resultCard.events({
'click .js-minicard'(event) {
event.preventDefault();
const cardId = Template.currentData()._id;
const boardId = Template.currentData().boardId;
Meteor.subscribe('popupCardData', cardId, {
onReady() {
Session.set('popupCardId', cardId);
Session.set('popupCardBoardId', boardId);
this_.cardDetailsPopup(evt);
if (!Popup.isOpen()) {
Popup.open("cardDetails")(event);
}
},
});
},
cardDetailsPopup(event) {
if (!Popup.isOpen()) {
Popup.open("cardDetails")(event);
}
},
events() {
return [
{
'click .js-minicard': this.clickOnMiniCard,
},
];
},
}).register('resultCard');
});
+15 -4
View File
@@ -15,7 +15,7 @@ textarea.js-edit-subtask-item {
color: #8c8c8c;
text-decoration: underline;
word-wrap: break-word;
float: right;
flex-shrink: 0;
padding-top: 6px;
}
.delete-text:hover,
@@ -24,8 +24,13 @@ textarea.js-edit-subtask-item {
.js-delete-subtask-item:hover {
color: inherit;
}
.subtask-title {
display: flex;
align-items: center;
width: 100%;
}
.subtask-title .checkbox {
float: left;
flex-shrink: 0;
width: 30px;
height: 30px;
font-size: 18px;
@@ -34,10 +39,12 @@ textarea.js-edit-subtask-item {
.subtask-title .title {
font-size: 18px;
line-height: 25px;
flex: 1;
order: 1;
}
.subtask-title .subtasks-stat {
margin: 0 0.5em;
float: right;
flex-shrink: 0;
padding-top: 6px;
}
.subtask-title .subtasks-stat.is-finished {
@@ -46,6 +53,9 @@ textarea.js-edit-subtask-item {
.subtask-title .js-delete-subtask {
margin: 0 0.5em;
}
.subtask-title .js-view-subtask {
margin: 0 0.5em;
}
.js-confirm-subtask-delete {
background-color: #f7f7f7;
position: absolute;
@@ -175,6 +185,7 @@ body.grey-icons-enabled .subtasks-item .check-box-unicode {
display: inline-block;
}
.subtask-details-menu {
float: right;
flex-shrink: 0;
padding: 6px 10px 6px 10px;
order: 2;
}
+7 -3
View File
@@ -26,7 +26,7 @@ template(name="subtaskDetail")
.subtask-title
span
if canModifyCard
a.subtask-details-menu.js-open-subtask-details-menu(title="{{_ 'subtaskActionsPopup-title'}}")
a.fa.fa-navicon.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.js-close-inlined-form
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
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.js-close-inlined-form
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
span(title=createdAt) {{ displayDate createdAt }}
if canModifyCard
if currentUser.isBoardAdmin
@@ -96,6 +96,10 @@ template(name="subtaskActionsPopup")
a.js-view-subtask(title="{{ subtask.title }}")
i.fa.fa-eye
| {{_ "view-it"}}
li
a.js-go-to-subtask-board
i.fa.fa-folder
| {{_ "go-to-board"}}
if currentUser.isBoardAdmin
a.js-delete-subtask.delete-subtask
i.fa.fa-trash
+93 -81
View File
@@ -1,17 +1,35 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import Cards from '/models/cards';
import { Filter } from '/client/lib/filter';
BlazeComponent.extendComponent({
addSubtask(event) {
Template.subtasks.events({
'click .js-open-subtask-details-menu'(event) {
// Close any existing popup first to avoid accumulating content
if (Popup.isOpen()) {
Popup.close();
}
// Now open the popup for this specific subtask
Popup.open('subtaskActions').call(this, event);
},
async 'submit .js-add-subtask'(event, tpl) {
event.preventDefault();
const textarea = this.find('textarea.js-add-subtask-item');
const textarea = tpl.find('textarea.js-add-subtask-item');
const title = textarea.value.trim();
const cardId = this.currentData().cardId;
const cardId = Template.currentData().cardId;
const card = ReactiveCache.getCard(cardId);
const sortIndex = -1;
const crtBoard = ReactiveCache.getBoard(card.boardId);
const targetBoard = crtBoard.getDefaultSubtasksBoard();
const listId = targetBoard.getDefaultSubtasksListId();
const targetBoard = await crtBoard.getDefaultSubtasksBoardAsync();
if (!targetBoard) {
return;
}
const targetList = await targetBoard.getDefaultSubtasksListAsync();
if (!targetList) {
return;
}
const listId = targetList._id;
//Get the full swimlane data for the parent task.
const parentSwimlane = ReactiveCache.getSwimlane({
@@ -19,20 +37,22 @@ BlazeComponent.extendComponent({
_id: card.swimlaneId,
});
//find the swimlane of the same name in the target board.
const targetSwimlane = ReactiveCache.getSwimlane({
boardId: targetBoard._id,
title: parentSwimlane.title,
});
const targetSwimlane = parentSwimlane
? ReactiveCache.getSwimlane({
boardId: targetBoard._id,
title: parentSwimlane.title,
})
: undefined;
//If no swimlane with a matching title exists in the target board, fall back to the default swimlane.
const swimlaneId =
targetSwimlane === undefined
? targetBoard.getDefaultSwimline()._id
: targetSwimlane._id;
const nextCardNumber = targetBoard.getNextCardNumber();
const nextCardNumber = await targetBoard.getNextCardNumber();
if (title) {
const _id = Cards.insert({
const _id = await Cards.insertAsync({
title,
parentId: cardId,
members: [],
@@ -43,7 +63,7 @@ BlazeComponent.extendComponent({
sort: sortIndex,
swimlaneId,
type: 'cardType-card',
cardNumber: nextCardNumber
cardNumber: nextCardNumber,
});
// In case the filter is active we need to add the newly inserted card in
@@ -53,7 +73,7 @@ BlazeComponent.extendComponent({
Filter.addException(_id);
setTimeout(() => {
this.$('.add-subtask-item')
tpl.$('.add-subtask-item')
.last()
.click();
}, 100);
@@ -61,27 +81,20 @@ BlazeComponent.extendComponent({
textarea.value = '';
textarea.focus();
},
async deleteSubtask() {
const subtask = this.currentData().subtask;
'submit .js-edit-subtask-title'(event, tpl) {
event.preventDefault();
const textarea = tpl.find('textarea.js-edit-subtask-item');
const title = textarea.value.trim();
const subtask = Template.currentData().subtask;
subtask.setTitle(title);
},
async 'click .js-delete-subtask-item'() {
const subtask = Template.currentData().subtask;
if (subtask && subtask._id) {
await subtask.archive();
}
},
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
async editSubtask(event) {
event.preventDefault();
const textarea = this.find('textarea.js-edit-subtask-item');
const title = textarea.value.trim();
const subtask = this.currentData().subtask;
await subtask.setTitle(title);
},
pressKey(event) {
keydown(event) {
//If user press enter key inside a form, submit it
//Unless the user is also holding down the 'shift' key
if (event.keyCode === 13 && !event.shiftKey) {
@@ -90,65 +103,66 @@ BlazeComponent.extendComponent({
$form.find('button[type=submit]').click();
}
},
});
events() {
return [
{
'click .js-open-subtask-details-menu': Popup.open('subtaskActions'),
'submit .js-add-subtask': this.addSubtask,
'submit .js-edit-subtask-title': this.editSubtask,
'click .js-delete-subtask-item': this.deleteSubtask,
keydown: this.pressKey,
},
];
Template.subtasks.onCreated(function () {
this.toggleDeleteDialog = new ReactiveVar(false);
});
Template.subtasks.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
}).register('subtasks');
toggleDeleteDialog() {
return Template.instance().toggleDeleteDialog;
},
});
BlazeComponent.extendComponent({
async toggleItem() {
const item = this.currentData().item;
Template.subtaskItemDetail.events({
async 'click .js-subtasks-item .check-box-unicode'() {
const item = Template.currentData().item;
if (item && item._id) {
await item.toggleItem();
}
},
events() {
return [
{
'click .js-subtasks-item .check-box-unicode': this.toggleItem,
},
];
},
}).register('subtaskItemDetail');
});
BlazeComponent.extendComponent({
Template.subtaskActionsPopup.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
events() {
return [
{
'click .js-view-subtask'(event) {
if ($(event.target).hasClass('js-view-subtask')) {
const subtask = this.currentData().subtask;
const board = subtask.board();
FlowRouter.go('card', {
boardId: board._id,
slug: board.slug,
cardId: subtask._id,
});
}
},
'click .js-delete-subtask' : Popup.afterConfirm('subtaskDelete', async function () {
Popup.back(2);
const subtask = this.subtask;
if (subtask && subtask._id) {
await subtask.archive();
}
}),
}
]
}
}).register('subtaskActionsPopup');
});
Template.subtaskActionsPopup.events({
'click .js-view-subtask'(event) {
if ($(event.target).hasClass('js-view-subtask')) {
const subtask = Template.currentData().subtask;
const board = subtask.board();
FlowRouter.go('card', {
boardId: board._id,
slug: board.slug,
cardId: subtask._id,
swimlaneId: subtask.swimlaneId,
listId: subtask.listId,
});
}
},
'click .js-go-to-subtask-board'() {
const subtask = Template.currentData().subtask;
const board = subtask.board();
if (board) {
Popup.close();
FlowRouter.go('board', { id: board._id, slug: board.slug });
}
},
'click .js-delete-subtask' : Popup.afterConfirm('subtaskDelete', async function () {
Popup.back(2);
const subtask = this.subtask;
if (subtask && subtask._id) {
await subtask.archive();
}
}),
});
Template.editSubtaskItemForm.helpers({
user() {
@@ -158,5 +172,3 @@ Template.editSubtaskItemForm.helpers({
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
});
@@ -1,27 +0,0 @@
<template name="originalPosition">
<div class="original-position-info">
{{#if isLoading}}
<div class="original-position-loading">
⏳ Loading original position...
</div>
{{else if showOriginalPosition}}
<div class="original-position-details">
{{#if hasMovedFromOriginal}}
<div class="original-position-moved">
<span class="original-position-text">ℹ️ {{getOriginalPositionDescription}}</span>
</div>
{{else}}
<div class="original-position-unchanged">
<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,19 @@
template(name="originalPosition")
.original-position-info
if isLoading
.original-position-loading
| ⏳ Loading original position...
else
if showOriginalPosition
.original-position-details
if hasMovedFromOriginal
.original-position-moved
span.original-position-text ℹ️ {{getOriginalPositionDescription}}
else
.original-position-unchanged
span.original-position-text ✅ In original position
if getOriginalTitle
.original-title
strong Original title:
| {{getOriginalTitle}}
+36 -40
View File
@@ -1,69 +1,69 @@
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);
}
});
}
Template.originalPosition.onCreated(function () {
this.originalPosition = new ReactiveVar(null);
this.isLoading = new ReactiveVar(false);
this.hasMoved = new ReactiveVar(false);
loadOriginalPosition(entityId, entityType) {
this.isLoading.set(true);
const tpl = this;
function loadOriginalPosition(entityId, entityType) {
tpl.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);
tpl.isLoading.set(false);
if (error) {
console.error('Error loading original position:', error);
this.originalPosition.set(null);
tpl.originalPosition.set(null);
} else {
this.originalPosition.set(result);
tpl.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);
tpl.hasMoved.set(movedResult);
}
});
}
});
}
this.autorun(() => {
const data = Template.currentData();
if (data && data.entityId && data.entityType) {
loadOriginalPosition(data.entityId, data.entityType);
}
});
});
Template.originalPosition.helpers({
getOriginalPosition() {
return this.originalPosition.get();
}
return Template.instance().originalPosition.get();
},
isLoading() {
return this.isLoading.get();
}
return Template.instance().isLoading.get();
},
hasMovedFromOriginal() {
return this.hasMoved.get();
}
return Template.instance().hasMoved.get();
},
getOriginalPositionDescription() {
const position = this.getOriginalPosition();
const position = Template.instance().originalPosition.get();
if (!position) return 'No original position data';
if (position.originalPosition) {
const entityType = this.data().entityType;
const data = Template.currentData();
const entityType = data.entityType;
let description = `Original position: ${position.originalPosition.sort || 0}`;
if (entityType === 'list' && position.originalSwimlaneId) {
@@ -81,18 +81,14 @@ class OriginalPositionComponent extends BlazeComponent {
}
return 'No original position data';
}
},
getOriginalTitle() {
const position = this.getOriginalPosition();
const position = Template.instance().originalPosition.get();
return position ? position.originalTitle : '';
}
},
showOriginalPosition() {
return this.getOriginalPosition() !== null;
}
}
OriginalPositionComponent.register('originalPosition');
export default OriginalPositionComponent;
return Template.instance().originalPosition.get() !== null;
},
});
+16
View File
@@ -0,0 +1,16 @@
import {
setupDatePicker,
datePickerRendered,
datePickerHelpers,
datePickerEvents,
} from '/client/lib/datepicker';
Template.datepicker.onCreated(function () {
setupDatePicker(this);
});
Template.datepicker.onRendered(function () {
datePickerRendered(this);
});
Template.datepicker.helpers(datePickerHelpers());
+6 -5
View File
@@ -1,6 +1,7 @@
template(name='inlinedForm')
if isOpen.get
form.inlined-form.js-inlined-form(id=id class=classNames)
+Template.contentBlock
else
+Template.elseBlock
.js-inlined-form-wrapper(style='display: contents;' data-position=position)
if isOpen.get
form.inlined-form.js-inlined-form(id=id class=classNames)
+Template.contentBlock
else
+Template.elseBlock
+2
View File
@@ -1,4 +1,5 @@
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import Cards from '/models/cards';
// Add click handler to ganttView for card titles
Template.ganttView.events({
@@ -14,6 +15,7 @@ Template.ganttView.events({
},
});
import { Template } from 'meteor/templating';
import { Utils } from '/client/lib/utils';
// Blaze template helpers for ganttView
function getISOWeekInfo(d) {
+21 -25
View File
@@ -1,34 +1,30 @@
BlazeComponent.extendComponent({
onCreated() {
// Provide the expected parent component properties for cardDetails
this.showOverlay = new ReactiveVar(false);
this.mouseHasEnterCardDetails = false;
},
Template.ganttCard.onCreated(function () {
// Provide the expected parent component properties for cardDetails
this.showOverlay = new ReactiveVar(false);
this.mouseHasEnterCardDetails = false;
});
Template.ganttCard.helpers({
selectedCard() {
// The selected card is now passed as a parameter to the component
return this.currentData();
return Template.currentData();
},
});
events() {
return [
{
'click .js-close-card-details'(event) {
event.preventDefault();
// Find the ganttView template instance and clear selectedCardId
let view = Blaze.currentView;
while (view) {
if (view.templateInstance && view.templateInstance().selectedCardId) {
view.templateInstance().selectedCardId.set(null);
break;
}
view = view.parentView;
}
},
},
];
Template.ganttCard.events({
'click .js-close-card-details'(event) {
event.preventDefault();
// Find the ganttView template instance and clear selectedCardId
let view = Blaze.currentView;
while (view) {
if (view.templateInstance && view.templateInstance().selectedCardId) {
view.templateInstance().selectedCardId.set(null);
break;
}
view = view.parentView;
}
},
}).register('ganttCard');
});
// Add click handler to ganttView for card titles
Template.ganttView.events({
+217 -220
View File
@@ -4,39 +4,68 @@ import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { wekanGetMembersToMap } from './wekanMembersMapper';
import { csvGetMembersToMap } from './csvMembersMapper';
import getSlug from 'limax';
import { UserSearchIndex } from '/models/users';
import { Utils } from '/client/lib/utils';
const Papa = require('papaparse');
BlazeComponent.extendComponent({
Template.importHeaderBar.helpers({
title() {
return `import-board-title-${Session.get('importSource')}`;
},
}).register('importHeaderBar');
});
BlazeComponent.extendComponent({
onCreated() {
this.error = new ReactiveVar('');
this.steps = ['importTextarea', 'importMapMembers'];
this._currentStepIndex = new ReactiveVar(0);
this.importedData = new ReactiveVar();
this.membersToMap = new ReactiveVar([]);
this.importSource = Session.get('importSource');
},
// Helper to find the closest ancestor template instance by name
function findParentTemplateInstance(childTemplateInstance, parentTemplateName) {
let view = childTemplateInstance.view;
while (view) {
if (view.name === `Template.${parentTemplateName}` && view.templateInstance) {
return view.templateInstance();
}
view = view.parentView;
}
return null;
}
currentTemplate() {
return this.steps[this._currentStepIndex.get()];
},
function _prepareAdditionalData(dataObject) {
const importSource = Session.get('importSource');
let membersToMap;
switch (importSource) {
case 'trello':
membersToMap = trelloGetMembersToMap(dataObject);
break;
case 'wekan':
membersToMap = wekanGetMembersToMap(dataObject);
break;
case 'csv':
membersToMap = csvGetMembersToMap(dataObject);
break;
}
return membersToMap;
}
nextStep() {
Template.import.onCreated(function () {
this.error = new ReactiveVar('');
this.steps = ['importTextarea', 'importMapMembers'];
this._currentStepIndex = new ReactiveVar(0);
this.importedData = new ReactiveVar();
this.membersToMap = new ReactiveVar([]);
this.importSource = Session.get('importSource');
this.nextStep = () => {
const nextStepIndex = this._currentStepIndex.get() + 1;
if (nextStepIndex >= this.steps.length) {
this.finishImport();
} else {
this._currentStepIndex.set(nextStepIndex);
}
},
};
importData(evt, dataSource) {
this.setError = (error) => {
this.error.set(error);
};
this.importData = (evt, dataSource) => {
evt.preventDefault();
const input = this.find('.js-import-json').value;
if (dataSource === 'csv') {
@@ -44,7 +73,7 @@ BlazeComponent.extendComponent({
const ret = Papa.parse(csv);
if (ret && ret.data && ret.data.length) this.importedData.set(ret.data);
else throw new Meteor.Error('error-csv-schema');
const membersToMap = this._prepareAdditionalData(ret.data);
const membersToMap = _prepareAdditionalData(ret.data);
this.membersToMap.set(membersToMap);
this.nextStep();
} else {
@@ -52,7 +81,7 @@ BlazeComponent.extendComponent({
const dataObject = JSON.parse(input);
this.setError('');
this.importedData.set(dataObject);
const membersToMap = this._prepareAdditionalData(dataObject);
const membersToMap = _prepareAdditionalData(dataObject);
// store members data and mapping in Session
// (we go deep and 2-way, so storing in data context is not a viable option)
this.membersToMap.set(membersToMap);
@@ -61,13 +90,9 @@ BlazeComponent.extendComponent({
this.setError('error-json-malformed');
}
}
},
};
setError(error) {
this.error.set(error);
},
finishImport() {
this.finishImport = () => {
const additionalData = {};
const membersMapping = this.membersToMap.get();
if (membersMapping) {
@@ -95,44 +120,27 @@ BlazeComponent.extendComponent({
FlowRouter.go('board', {
id: res,
slug: title,
})
});
//Utils.goBoardId(res);
}
},
);
},
};
});
_prepareAdditionalData(dataObject) {
const importSource = Session.get('importSource');
let membersToMap;
switch (importSource) {
case 'trello':
membersToMap = trelloGetMembersToMap(dataObject);
break;
case 'wekan':
membersToMap = wekanGetMembersToMap(dataObject);
break;
case 'csv':
membersToMap = csvGetMembersToMap(dataObject);
break;
}
return membersToMap;
Template.import.helpers({
error() {
return Template.instance().error;
},
_screenAdditionalData() {
return 'mapMembers';
},
}).register('import');
BlazeComponent.extendComponent({
template() {
return 'importTextarea';
currentTemplate() {
return Template.instance().steps[Template.instance()._currentStepIndex.get()];
},
});
Template.importTextarea.helpers({
instruction() {
return `import-board-instruction-${Session.get('importSource')}`;
},
importPlaceHolder() {
const importSource = Session.get('importSource');
if (importSource === 'csv') {
@@ -141,81 +149,37 @@ BlazeComponent.extendComponent({
return 'import-json-placeholder';
}
},
});
events() {
return [
{
submit(evt) {
return this.parentComponent().importData(
evt,
Session.get('importSource'),
);
},
},
];
Template.importTextarea.events({
submit(evt, tpl) {
const importTpl = findParentTemplateInstance(tpl, 'import');
if (importTpl) {
return importTpl.importData(evt, Session.get('importSource'));
}
},
}).register('importTextarea');
});
BlazeComponent.extendComponent({
onCreated() {
this.usersLoaded = new ReactiveVar(false);
// Module-level reference so popup children can access importMapMembers methods
let _importMapMembersTpl = null;
this.autorun(() => {
const handle = this.subscribe(
'user-miniprofile',
this.members().map(member => {
return member.username;
}),
);
Tracker.nonreactive(() => {
Tracker.autorun(() => {
if (
handle.ready() &&
!this.usersLoaded.get() &&
this.members().length
) {
this._refreshMembers(
this.members().map(member => {
if (!member.wekanId) {
let user = ReactiveCache.getUser({ username: member.username });
if (!user) {
user = ReactiveCache.getUser({ importUsernames: member.username });
}
if (user) {
// eslint-disable-next-line no-console
// console.log('found username:', user.username);
member.wekanId = user._id;
}
}
return member;
}),
);
}
this.usersLoaded.set(handle.ready());
});
});
});
},
Template.importMapMembers.onCreated(function () {
_importMapMembersTpl = this;
this.usersLoaded = new ReactiveVar(false);
members() {
return this.parentComponent().membersToMap.get();
},
this.members = () => {
const importTpl = findParentTemplateInstance(this, 'import');
return importTpl ? importTpl.membersToMap.get() : [];
};
_refreshMembers(listOfMembers) {
return this.parentComponent().membersToMap.set(listOfMembers);
},
this._refreshMembers = (listOfMembers) => {
const importTpl = findParentTemplateInstance(this, 'import');
if (importTpl) {
importTpl.membersToMap.set(listOfMembers);
}
};
/**
* Will look into the list of members to import for the specified memberId,
* then set its property to the supplied value.
* If unset is true, it will remove the property from the rest of the list as well.
*
* use:
* - memberId = null to use selected member
* - value = null to unset a property
* - unset = true to ensure property is only set on 1 member at a time
*/
_setPropertyForMember(property, value, memberId, unset = false) {
this._setPropertyForMember = (property, value, memberId, unset = false) => {
const listOfMembers = this.members();
let finder = null;
if (memberId) {
@@ -241,17 +205,13 @@ BlazeComponent.extendComponent({
});
// Session.get gives us a copy, we have to set it back so it sticks
this._refreshMembers(listOfMembers);
},
};
setSelectedMember(memberId) {
this.setSelectedMember = (memberId) => {
return this._setPropertyForMember('selected', true, memberId, true);
},
};
/**
* returns the member with specified id,
* or the selected member if memberId is not specified
*/
getMember(memberId = null) {
this.getMember = (memberId = null) => {
const allMembers = this.members();
let finder = null;
if (memberId) {
@@ -260,117 +220,154 @@ BlazeComponent.extendComponent({
finder = user => user.selected;
}
return allMembers.find(finder);
},
};
mapSelectedMember(wekanId) {
this.mapSelectedMember = (wekanId) => {
return this._setPropertyForMember('wekanId', wekanId, null);
},
};
unmapMember(memberId) {
this.unmapMember = (memberId) => {
return this._setPropertyForMember('wekanId', null, memberId);
},
};
onSubmit(evt) {
this.autorun(() => {
const handle = this.subscribe(
'user-miniprofile',
this.members().map(member => {
return member.username;
}),
);
Tracker.nonreactive(() => {
Tracker.autorun(() => {
if (
handle.ready() &&
!this.usersLoaded.get() &&
this.members().length
) {
this._refreshMembers(
this.members().map(member => {
if (!member.wekanId) {
let user = ReactiveCache.getUser({ username: member.username });
if (!user) {
user = ReactiveCache.getUser({ importUsernames: member.username });
}
if (user) {
member.wekanId = user._id;
}
}
return member;
}),
);
}
this.usersLoaded.set(handle.ready());
});
});
});
});
Template.importMapMembers.onDestroyed(function () {
if (_importMapMembersTpl === this) {
_importMapMembersTpl = null;
}
});
Template.importMapMembers.helpers({
usersLoaded() {
return Template.instance().usersLoaded;
},
members() {
return Template.instance().members();
},
});
Template.importMapMembers.events({
submit(evt, tpl) {
evt.preventDefault();
this.parentComponent().nextStep();
const importTpl = findParentTemplateInstance(tpl, 'import');
if (importTpl) {
importTpl.nextStep();
}
},
events() {
return [
{
submit: this.onSubmit,
'click .js-select-member'(evt) {
const memberToMap = this.currentData();
if (memberToMap.wekan) {
// todo xxx ask for confirmation?
this.unmapMember(memberToMap.id);
} else {
this.setSelectedMember(memberToMap.id);
Popup.open('importMapMembersAdd')(evt);
}
},
},
];
'click .js-select-member'(evt, tpl) {
const memberToMap = Template.currentData();
if (memberToMap.wekan) {
// todo xxx ask for confirmation?
tpl.unmapMember(memberToMap.id);
} else {
tpl.setSelectedMember(memberToMap.id);
Popup.open('importMapMembersAdd')(evt);
}
},
}).register('importMapMembers');
BlazeComponent.extendComponent({
onRendered() {
this.find('.js-map-member input').focus();
},
onSelectUser() {
Popup.getOpenerComponent(5).mapSelectedMember(this.currentData().__originalId);
Popup.back();
},
events() {
return [
{
'click .js-select-import': this.onSelectUser,
},
];
},
}).register('importMapMembersAddPopup');
});
// Global reactive variables for import member popup
const importMemberPopupState = {
searching: new ReactiveVar(false),
searchResults: new ReactiveVar([]),
noResults: new ReactiveVar(false),
searchTimeout: null
searchTimeout: null,
};
BlazeComponent.extendComponent({
onCreated() {
// Use global state
this.searching = importMemberPopupState.searching;
this.searchResults = importMemberPopupState.searchResults;
this.noResults = importMemberPopupState.noResults;
this.searchTimeout = importMemberPopupState.searchTimeout;
},
Template.importMapMembersAddPopup.onCreated(function () {
this.searching = importMemberPopupState.searching;
this.searchResults = importMemberPopupState.searchResults;
this.noResults = importMemberPopupState.noResults;
this.searchTimeout = null;
onRendered() {
this.find('.js-search-member-input').focus();
},
this.searching.set(false);
this.searchResults.set([]);
this.noResults.set(false);
});
performSearch(query) {
if (!query || query.length < 2) {
this.searchResults.set([]);
this.noResults.set(false);
return;
Template.importMapMembersAddPopup.onRendered(function () {
this.find('.js-search-member-input').focus();
});
Template.importMapMembersAddPopup.onDestroyed(function () {
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
this.searching.set(false);
});
function importPerformSearch(tpl, query) {
if (!query || query.length < 2) {
tpl.searchResults.set([]);
tpl.noResults.set(false);
return;
}
tpl.searching.set(true);
tpl.noResults.set(false);
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
tpl.searchResults.set(results);
tpl.searching.set(false);
if (results.length === 0) {
tpl.noResults.set(true);
}
}
Template.importMapMembersAddPopup.events({
'click .js-select-import'(event, tpl) {
if (_importMapMembersTpl) {
_importMapMembersTpl.mapSelectedMember(Template.currentData().__originalId);
}
Popup.back();
},
'keyup .js-search-member-input'(event, tpl) {
const query = event.target.value.trim();
if (tpl.searchTimeout) {
clearTimeout(tpl.searchTimeout);
}
this.searching.set(true);
this.noResults.set(false);
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
this.searchResults.set(results);
this.searching.set(false);
if (results.length === 0) {
this.noResults.set(true);
}
tpl.searchTimeout = setTimeout(() => {
importPerformSearch(tpl, query);
}, 300);
},
events() {
return [
{
'keyup .js-search-member-input'(event) {
const query = event.target.value.trim();
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
this.searchTimeout = setTimeout(() => {
this.performSearch(query);
}, 300);
},
},
];
},
}).register('importMapMembersAddPopupSearch');
});
Template.importMapMembersAddPopup.helpers({
searchResults() {
@@ -381,5 +378,5 @@ Template.importMapMembersAddPopup.helpers({
},
noResults() {
return importMemberPopupState.noResults;
}
})
},
});
+1 -1
View File
@@ -2,7 +2,7 @@ template(name="basicTabs")
.basicTabs-container(class="{{name}}")
ul.tabs-list
each tabs
li.tab-item(class="{{isActiveTab slug}} {{class}}") {{name}}
li.tab-item(class="{{isActiveTab slug}} {{class}}" data-tab="{{slug}}") {{name}}
.tabs-content-container
+Template.contentBlock
+24 -18
View File
@@ -1,4 +1,12 @@
const { ReactiveVar } = require('meteor/reactive-var');
import { ReactiveVar } from 'meteor/reactive-var';
// Creation-time stack tracking which basicTabs is currently being rendered.
// Blaze creates parent views before children, so when tabContent.onCreated fires,
// its parent basicTabs instance is on top of this stack.
// This is necessary because Blaze content blocks render with parentView pointing
// back to the calling context (e.g. membersWidget), not the basicTabs template —
// so walking the view tree cannot find the basicTabs instance.
const _creatingStack = [];
Template.basicTabs.onCreated(function () {
const activeTab = this.data.activeTab
@@ -10,6 +18,13 @@ Template.basicTabs.onCreated(function () {
const current = this._activeTab.get();
return current && current.slug === slug;
};
_creatingStack.push(this);
});
Template.basicTabs.onRendered(function () {
const idx = _creatingStack.lastIndexOf(this);
if (idx !== -1) _creatingStack.splice(idx, 1);
});
Template.basicTabs.helpers({
@@ -26,25 +41,16 @@ Template.basicTabs.events({
},
});
function findBasicTabsInstance() {
let view = Blaze.currentView;
while (view) {
if (view.name === 'Template.basicTabs' && view.templateInstance) {
const inst = view.templateInstance();
if (inst && inst.isActiveSlug) {
return inst;
}
}
view = view.parentView;
}
return null;
}
Template.tabContent.onCreated(function () {
// Capture the parent basicTabs instance at creation time via the stack.
// isActiveSlug reads a ReactiveVar on the basicTabs instance, so the
// isActiveTab helper below will re-run reactively when the tab changes.
this._basicTabsInst = _creatingStack[_creatingStack.length - 1] || null;
});
Template.tabContent.helpers({
isActiveTab(slug) {
const inst = findBasicTabsInstance();
if (inst && inst.isActiveSlug(slug)) {
return 'active';
}
const inst = Template.instance()._basicTabsInst;
if (inst && inst.isActiveSlug(slug)) return 'active';
},
});
+336 -67
View File
@@ -273,11 +273,39 @@ body.list-resizing-active * {
max-width: 30px;
margin: 0;
}
/* In collapsed lists, keep controls inside the 30px column.
The inlined wrapper is normally display: contents; convert it to a real
flex container so control ordering and centering apply reliably. */
.list.list-collapsed .list-header > .js-inlined-form-wrapper {
display: flex !important;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
overflow: visible;
padding-top: 0 !important;
}
/* Keep collapsed header controls in a deterministic vertical order:
collapse toggle, drag handle, then rotated title. */
.list.list-collapsed .list-header > .js-inlined-form-wrapper > .list-header-handle-desktop,
.list.list-collapsed .list-header > .js-inlined-form-wrapper > .list-header-handle {
order: 2;
}
.list.list-collapsed .list-header > .js-inlined-form-wrapper > .js-collapse {
order: 1;
}
.list.list-collapsed .list-header > .js-inlined-form-wrapper > .list-rotated {
order: 3;
}
.list.list-collapsed .list-header .js-collapse {
position: relative !important;
left: -10px !important;
margin: 5px auto;
z-index: 10;
z-index: 30;
padding: 5px;
font-size: 16px;
white-space: nowrap;
@@ -287,55 +315,69 @@ body.list-resizing-active * {
top: auto !important;
}
.list.list-collapsed .list-header .list-header-handle {
position: static !important;
position: relative !important;
margin: 5px auto;
z-index: 10;
z-index: 31;
padding: 5px;
display: block;
width: auto;
top: auto !important;
right: auto !important;
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto !important;
}
.list.list-collapsed .list-header .list-header-handle-desktop {
position: static !important;
position: relative !important;
margin: 5px auto;
z-index: 10;
z-index: 31;
padding: 5px;
display: block;
width: auto;
top: auto !important;
right: auto !important;
left: auto !important;
transform: none !important;
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto !important;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
width: 100% !important;
max-width: 100%;
height: auto !important;
margin: 20px 0 0 0 !important;
margin: 10px 0 0 0 !important;
position: relative !important;
overflow: visible !important;
transform: rotate(90deg);
overflow: hidden !important;
transform: none;
transform-origin: center center;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
pointer-events: none;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
text-align: center;
overflow: visible;
white-space: nowrap;
overflow: hidden;
white-space: normal;
display: block !important;
font-size: 12px;
line-height: 1.2;
color: #333;
padding: 4px 8px;
padding: 0;
margin: 0;
width: auto;
height: auto;
width: 100%;
max-height: 100%;
height: 100%;
position: static;
left: auto;
top: auto;
transform: none;
writing-mode: vertical-rl;
text-orientation: mixed;
transform: rotate(180deg);
z-index: 10;
visibility: visible !important;
opacity: 1 !important;
@@ -473,13 +515,25 @@ form.inlined-form .list-composer {
color: #333;
}
/* Title text container - full width below buttons */
/* Title text container */
.list-header > div {
padding-top: 25px;
padding-top: 0;
width: 100%;
display: block;
clear: both;
}
/* Expanded desktop collapse button — static, participates in flex row 1. */
.list:not(.list-collapsed):not(.mobile-view) .list-header .js-collapse {
position: static !important;
top: auto !important;
left: auto !important;
right: auto !important;
transform: none !important;
margin: 0 !important;
min-width: 24px;
padding: 2px 4px !important;
}
.list.list-collapsed .list-header .js-collapse {
display: inline-block !important;
visibility: visible !important;
@@ -509,25 +563,28 @@ form.inlined-form .list-composer {
height: 100% !important;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
width: 100% !important;
height: auto !important;
margin: 20px 0 0 0 !important;
margin: 10px 0 0 0 !important;
position: relative !important;
transform: rotate(90deg);
overflow: hidden !important;
transform: none;
flex: 1;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: auto;
width: 100%;
font-size: 12px;
height: auto;
height: 100%;
line-height: 1.2;
padding: 4px 8px;
padding: 0;
margin: 0;
overflow: visible;
overflow: hidden;
position: static;
left: auto;
top: auto;
transform: none;
writing-mode: vertical-rl;
text-orientation: mixed;
transform: rotate(180deg);
text-align: center;
visibility: visible !important;
opacity: 1 !important;
@@ -557,25 +614,28 @@ form.inlined-form .list-composer {
height: 100% !important;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
width: 100% !important;
height: auto !important;
margin: 20px 0 0 0 !important;
margin: 10px 0 0 0 !important;
position: relative !important;
transform: rotate(90deg);
overflow: hidden !important;
transform: none;
flex: 1;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: auto;
width: 100%;
font-size: 12px;
height: auto;
height: 100%;
line-height: 1.2;
padding: 4px 8px;
padding: 0;
margin: 0;
overflow: visible;
overflow: hidden;
position: static;
left: auto;
top: auto;
transform: none;
writing-mode: vertical-rl;
text-orientation: mixed;
transform: rotate(180deg);
text-align: center;
visibility: visible !important;
opacity: 1 !important;
@@ -605,25 +665,28 @@ form.inlined-form .list-composer {
height: 100% !important;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
width: 100% !important;
height: auto !important;
margin: 20px 0 0 0 !important;
margin: 10px 0 0 0 !important;
position: relative !important;
transform: rotate(90deg);
overflow: hidden !important;
transform: none;
flex: 1;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: auto;
width: 100%;
font-size: 12px;
height: auto;
height: 100%;
line-height: 1.2;
padding: 4px 8px;
padding: 0;
margin: 0;
overflow: visible;
overflow: hidden;
position: static;
left: auto;
top: auto;
transform: none;
writing-mode: vertical-rl;
text-orientation: mixed;
transform: rotate(180deg);
text-align: center;
visibility: visible !important;
opacity: 1 !important;
@@ -1133,6 +1196,7 @@ form.inlined-form .list-composer {
/* Allow long list titles to expand on desktop (non-mobile, non-collapsed) */
.list:not(.mobile-view):not(.list-collapsed) .list-header {
padding-top: calc(2.5vh - 6px) !important;
overflow: visible !important;
}
@@ -1162,79 +1226,184 @@ form.inlined-form .list-composer {
margin-top: unset !important;
}
.list-header-white {
border-bottom: 6px solid #fff;
border-bottom: 6px solid #fff !important;
}
.list-header-green {
border-bottom: 6px solid #3cb500;
border-bottom: 6px solid #3cb500 !important;
}
.list-header-yellow {
border-bottom: 6px solid #fad900;
border-bottom: 6px solid #fad900 !important;
}
.list-header-orange {
border-bottom: 6px solid #ff9f19;
border-bottom: 6px solid #ff9f19 !important;
}
.list-header-red {
border-bottom: 6px solid #eb4646;
border-bottom: 6px solid #eb4646 !important;
}
.list-header-purple {
border-bottom: 6px solid #a632db;
border-bottom: 6px solid #a632db !important;
}
.list-header-blue {
border-bottom: 6px solid #0079bf;
border-bottom: 6px solid #0079bf !important;
}
.list-header-pink {
border-bottom: 6px solid #ff78cb;
border-bottom: 6px solid #ff78cb !important;
}
.list-header-sky {
border-bottom: 6px solid #00c2e0;
border-bottom: 6px solid #00c2e0 !important;
}
.list-header-black {
border-bottom: 6px solid #4d4d4d;
border-bottom: 6px solid #4d4d4d !important;
}
.list-header-lime {
border-bottom: 6px solid #51e898;
border-bottom: 6px solid #51e898 !important;
}
.list-header-silver {
border-bottom: 6px solid #e4e4e4;
border-bottom: 6px solid #e4e4e4 !important;
}
.list-header-peachpuff {
border-bottom: 6px solid #ffdab9;
border-bottom: 6px solid #ffdab9 !important;
}
.list-header-crimson {
border-bottom: 6px solid #dc143c;
border-bottom: 6px solid #dc143c !important;
}
.list-header-plum {
border-bottom: 6px solid #dda0dd;
border-bottom: 6px solid #dda0dd !important;
}
.list-header-darkgreen {
border-bottom: 6px solid #006400;
border-bottom: 6px solid #006400 !important;
}
/* Final collapsed desktop overrides:
keep drag handle visible in every collapsed list and preserve rotated title. */
.list.list-collapsed:not(.mobile-view) {
overflow: visible !important;
z-index: 1;
}
.list.list-collapsed:not(.mobile-view) .list-header {
display: flex !important;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 30px !important;
max-width: 30px !important;
min-height: 100% !important;
height: 100% !important;
padding: 5px 0 !important;
overflow: visible !important;
}
.list.list-collapsed:not(.mobile-view) .list-header > .js-inlined-form-wrapper {
display: flex !important;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100% !important;
height: 100% !important;
padding-top: 0 !important;
}
.list.list-collapsed:not(.mobile-view) .list-header > .js-inlined-form-wrapper > .list-header-handle-desktop,
.list.list-collapsed:not(.mobile-view) .list-header > .js-inlined-form-wrapper > .list-header-handle {
order: 2;
}
.list.list-collapsed:not(.mobile-view) .list-header > .js-inlined-form-wrapper > .js-collapse {
order: 1;
}
.list.list-collapsed:not(.mobile-view) .list-header > .js-inlined-form-wrapper > .list-rotated {
order: 3;
}
.list.list-collapsed:not(.mobile-view) .list-header .list-header-handle-desktop,
.list.list-collapsed:not(.mobile-view) .list-header .list-header-handle {
position: relative !important;
top: auto !important;
right: auto !important;
left: auto !important;
transform: none !important;
display: block !important;
margin: 4px auto 2px auto !important;
padding: 5px !important;
z-index: 500 !important;
opacity: 1 !important;
visibility: visible !important;
pointer-events: auto !important;
color: #4d4d4d !important;
}
.list.list-collapsed:not(.mobile-view) .list-header .js-collapse {
position: relative !important;
top: auto !important;
left: auto !important;
right: auto !important;
display: block !important;
margin: 2px auto !important;
z-index: 400 !important;
opacity: 1 !important;
visibility: visible !important;
}
.list.list-collapsed:not(.mobile-view) .list-header .list-rotated {
position: relative !important;
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin: 4px 0 0 0 !important;
transform: none !important;
transform-origin: initial !important;
overflow: hidden !important;
z-index: 1 !important;
pointer-events: none !important;
display: block !important;
flex: 0 0 auto !important;
align-self: flex-start;
}
.list.list-collapsed:not(.mobile-view) .list-header .list-rotated h2.list-header-name {
display: block !important;
width: 100% !important;
height: auto !important;
margin: 0 !important;
max-height: 100% !important;
padding: 2px 0 !important;
white-space: normal !important;
overflow: hidden !important;
transform: none !important;
writing-mode: vertical-lr !important;
text-orientation: mixed !important;
text-align: left !important;
visibility: visible !important;
opacity: 1 !important;
}
.list-header-slateblue {
border-bottom: 6px solid #6a5acd;
border-bottom: 6px solid #6a5acd !important;
}
.list-header-magenta {
border-bottom: 6px solid #f0f;
border-bottom: 6px solid #f0f !important;
}
.list-header-gold {
border-bottom: 6px solid #ffd700;
border-bottom: 6px solid #ffd700 !important;
}
.list-header-navy {
border-bottom: 6px solid #000080;
border-bottom: 6px solid #000080 !important;
}
.list-header-gray {
border-bottom: 6px solid #808080;
border-bottom: 6px solid #808080 !important;
}
.list-header-saddlebrown {
border-bottom: 6px solid #8b4513;
border-bottom: 6px solid #8b4513 !important;
}
.list-header-paleturquoise {
border-bottom: 6px solid #afeeee;
border-bottom: 6px solid #afeeee !important;
}
.list-header-mistyrose {
border-bottom: 6px solid #ffe4e1;
border-bottom: 6px solid #ffe4e1 !important;
}
.list-header-indigo {
border-bottom: 6px solid #4b0082;
border-bottom: 6px solid #4b0082 !important;
}
.list.list-collapsed .collapsed-list-drag-area {
@@ -1249,6 +1418,106 @@ form.inlined-form .list-composer {
.list.list-collapsed .collapsed-list-drag-area:active {
cursor: grabbing;
}
/* Final expanded desktop header layout:
collapse, title, drag handle, add-to-top, menu. */
/* Final expanded desktop header layout:
Row 1: collapse | drag handle | + add card | menu
Row 2: list title (full width) */
.list:not(.list-collapsed):not(.mobile-view) .list-header {
display: block !important;
/* Match collapsed header top anchor for toolbar controls. */
padding-top: 5px !important;
}
.list:not(.list-collapsed):not(.mobile-view) .list-header > .js-inlined-form-wrapper {
display: flex !important;
flex-wrap: wrap;
align-items: flex-start;
width: 100%;
gap: 0;
}
/* Collapse button — first item in toolbar row */
.list:not(.list-collapsed):not(.mobile-view) .list-header > .js-inlined-form-wrapper > .js-collapse {
order: 1;
flex: 0 0 auto;
position: static !important;
top: auto !important;
left: auto !important;
right: auto !important;
transform: none !important;
margin: 2px 2px 0 0 !important;
padding: 2px 4px !important;
min-width: 24px;
width: auto;
display: inline-flex !important;
align-items: center;
justify-content: center;
z-index: 20 !important;
}
/* Title div — second row, full width */
.list:not(.list-collapsed):not(.mobile-view) .list-header > .js-inlined-form-wrapper > div:not(.list-header-menu) {
order: 10;
flex: 1 0 100%;
min-width: 0;
padding-top: 4px !important;
padding-right: 0 !important;
}
/* Hide standalone duplicate drag handle outside the menu group */
.list:not(.list-collapsed):not(.mobile-view) .list-header > .js-inlined-form-wrapper > .list-header-handle-desktop {
display: none !important;
}
/* Watch icon in toolbar row */
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-watch-icon {
order: 2;
flex: 0 0 auto;
padding-left: 0;
align-self: center;
}
/* Menu group — right side of toolbar row */
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-menu {
order: 5;
float: none;
display: inline-flex !important;
align-items: center;
gap: 4px;
margin-left: auto;
margin-top: 2px;
flex: 0 0 auto;
}
/* Reset absolute positioning on all menu-group items */
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-menu .list-header-handle-desktop,
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-menu .list-header-plus-top,
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-menu .js-open-list-menu {
position: static !important;
top: auto !important;
right: auto !important;
left: auto !important;
transform: none !important;
display: inline-flex !important;
align-items: center;
justify-content: center;
margin: 0 !important;
padding: 2px 6px !important;
}
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-menu .list-header-handle-desktop {
order: 1;
}
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-menu .list-header-plus-top {
order: 2;
}
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-menu .js-open-list-menu {
order: 3;
}
.list.list-collapsed .list-header-name-collapsed {
writing-mode: vertical-rl;
text-align: center;
+244 -216
View File
@@ -1,199 +1,212 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { EscapeActions } from '/client/lib/escapeActions';
import { MultiSelection } from '/client/lib/multiSelection';
import { Utils } from '/client/lib/utils';
require('/client/lib/jquery-ui.js')
const { calculateIndex } = Utils;
BlazeComponent.extendComponent({
// Proxy
openForm(options) {
this.childComponents('listBody')[0].openForm(options);
},
Template.list.onCreated(function () {
this.newCardFormIsVisible = new ReactiveVar(true);
onCreated() {
this.newCardFormIsVisible = new ReactiveVar(true);
},
// Proxy - find the listBody child template instance via the DOM
this.openForm = (options) => {
const listBodyEl = this.find('.list-body');
const view = listBodyEl && Blaze.getView(listBodyEl, 'Template.listBody');
const listBodyInstance = view?.templateInstance?.();
if (listBodyInstance) listBodyInstance.openForm(options);
};
});
// The jquery UI sortable library is the best solution I've found so far. I
// tried sortable and dragula but they were not powerful enough four our use
// case. I also considered writing/forking a drag-and-drop + sortable library
// but it's probably too much work.
// By calling asking the sortable library to cancel its move on the `stop`
// callback, we basically solve all issues related to reactive updates. A
// comment below provides further details.
onRendered() {
const boardComponent = this.parentComponent().parentComponent();
// The jquery UI sortable library is the best solution I've found so far. I
// tried sortable and dragula but they were not powerful enough four our use
// case. I also considered writing/forking a drag-and-drop + sortable library
// but it's probably too much work.
// By calling asking the sortable library to cancel its move on the `stop`
// callback, we basically solve all issues related to reactive updates. A
// comment below provides further details.
Template.list.onRendered(function () {
const boardBodyEl = this.firstNode?.parentElement?.closest?.('.board-body') ||
document.querySelector('.board-body');
const boardView = boardBodyEl && Blaze.getView(boardBodyEl, 'Template.boardBody');
const boardComponent = boardView?.templateInstance?.();
// Initialize list resize functionality immediately
this.initializeListResize();
// Initialize list resize functionality immediately
this.initializeListResize();
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
const $cards = this.$('.js-minicards');
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
const $cards = this.$('.js-minicards');
$cards.sortable({
connectWith: '.js-minicards:not(.js-list-full)',
tolerance: 'pointer',
appendTo: '.board-canvas',
helper(evt, item) {
const helper = item.clone();
if (MultiSelection.isActive()) {
const andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
if (andNOthers > 0) {
helper.append(
$(
Blaze.toHTML(
HTML.DIV(
{ class: 'and-n-other' },
TAPi18n.__('and-n-other-card', { count: andNOthers }),
),
$cards.sortable({
connectWith: '.js-minicards:not(.js-list-full)',
tolerance: 'pointer',
appendTo: '.board-canvas',
helper(evt, item) {
const helper = item.clone();
if (MultiSelection.isActive()) {
const andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
if (andNOthers > 0) {
helper.append(
$(
Blaze.toHTML(
HTML.DIV(
{ class: 'and-n-other' },
TAPi18n.__('and-n-other-card', { count: andNOthers }),
),
),
);
}
),
);
}
return helper;
},
distance: 7,
items: itemsSelector,
placeholder: 'minicard-wrapper placeholder',
scrollSpeed: 10,
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
EscapeActions.executeUpTo('popup-close');
boardComponent.setIsDragging(true);
},
stop(evt, ui) {
// To attribute the new index number, we need to get the DOM element
// of the previous and the following card -- if any.
const prevCardDom = ui.item.prev('.js-minicard').get(0);
const nextCardDom = ui.item.next('.js-minicard').get(0);
const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
const currentBoard = Utils.getCurrentBoard();
const defaultSwimlaneId = currentBoard.getDefaultSwimline()._id;
let targetSwimlaneId = null;
}
return helper;
},
distance: 7,
items: itemsSelector,
placeholder: 'minicard-wrapper placeholder',
scrollSpeed: 10,
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
EscapeActions.executeUpTo('popup-close');
if (boardComponent) boardComponent.setIsDragging(true);
},
stop(evt, ui) {
// To attribute the new index number, we need to get the DOM element
// of the previous and the following card -- if any.
const prevCardDom = ui.item.prev('.js-minicard').get(0);
const nextCardDom = ui.item.next('.js-minicard').get(0);
const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
const listData = Blaze.getData(ui.item.parents('.list').get(0));
const listId = listData._id;
const currentBoard = Utils.getCurrentBoard();
const defaultSwimlaneId = currentBoard.getDefaultSwimline()._id;
let targetSwimlaneId = null;
// only set a new swimelane ID if the swimlanes view is active
if (
Utils.boardView() === 'board-view-swimlanes' ||
currentBoard.isTemplatesBoard()
)
targetSwimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))
._id;
// only set a new swimelane ID if the swimlanes view is active
if (
Utils.boardView() === 'board-view-swimlanes' ||
currentBoard.isTemplatesBoard()
) {
targetSwimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))
._id;
} else if (listData.swimlaneId) {
targetSwimlaneId = listData.swimlaneId;
}
// Normally the jquery-ui sortable library moves the dragged DOM element
// to its new position, which disrupts Blaze reactive updates mechanism
// (especially when we move the last card of a list, or when multiple
// users move some cards at the same time). To prevent these UX glitches
// we ask sortable to gracefully cancel the move, and to put back the
// DOM in its initial state. The card move is then handled reactively by
// Blaze with the below query.
$cards.sortable('cancel');
// Normally the jquery-ui sortable library moves the dragged DOM element
// to its new position, which disrupts Blaze reactive updates mechanism
// (especially when we move the last card of a list, or when multiple
// users move some cards at the same time). To prevent these UX glitches
// we ask sortable to gracefully cancel the move, and to put back the
// DOM in its initial state. The card move is then handled reactively by
// Blaze with the below query.
$cards.sortable('cancel');
if (MultiSelection.isActive()) {
ReactiveCache.getCards(MultiSelection.getMongoSelector(), { sort: ['sort'] }).forEach((card, i) => {
const newSwimlaneId = targetSwimlaneId
? targetSwimlaneId
: card.swimlaneId || defaultSwimlaneId;
card.move(
currentBoard._id,
newSwimlaneId,
listId,
sortIndex.base + i * sortIndex.increment,
);
});
} else {
const cardDomElement = ui.item.get(0);
const card = Blaze.getData(cardDomElement);
if (MultiSelection.isActive()) {
ReactiveCache.getCards(MultiSelection.getMongoSelector(), { sort: ['sort'] }).forEach((card, i) => {
const newSwimlaneId = targetSwimlaneId
? targetSwimlaneId
: card.swimlaneId || defaultSwimlaneId;
card.move(currentBoard._id, newSwimlaneId, listId, sortIndex.base);
}
boardComponent.setIsDragging(false);
},
sort(event, ui) {
const $boardCanvas = $('.board-canvas');
const boardCanvas = $boardCanvas[0];
if (event.pageX < 10) { // scroll to the left
boardCanvas.scrollLeft -= 15;
ui.helper[0].offsetLeft -= 15;
}
if (
event.pageX > boardCanvas.offsetWidth - 10 &&
boardCanvas.scrollLeft < $boardCanvas.data('scrollLeftMax') // don't scroll more than possible
) { // scroll to the right
boardCanvas.scrollLeft += 15;
}
if (
event.pageY > boardCanvas.offsetHeight - 10 &&
event.pageY + boardCanvas.scrollTop < $boardCanvas.data('scrollTopMax') // don't scroll more than possible
) { // scroll to the bottom
boardCanvas.scrollTop += 15;
}
if (event.pageY < 10) { // scroll to the top
boardCanvas.scrollTop -= 15;
}
},
activate(event, ui) {
const $boardCanvas = $('.board-canvas');
const boardCanvas = $boardCanvas[0];
// scrollTopMax and scrollLeftMax only available at Firefox (https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTopMax)
// https://www.it-swarm.com.de/de/javascript/so-erhalten-sie-den-maximalen-dokument-scrolltop-wert/1069126844/
$boardCanvas.data('scrollTopMax', boardCanvas.scrollHeight - boardCanvas.clientTop);
// https://stackoverflow.com/questions/5138373/how-do-i-get-the-max-value-of-scrollleft/5704386#5704386
$boardCanvas.data('scrollLeftMax', boardCanvas.scrollWidth - boardCanvas.clientWidth);
},
});
this.autorun(() => {
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',
// Disable drag-dropping when user is not member
!Utils.canModifyBoard(),
// Not disable drag-dropping while in multi-selection mode
// MultiSelection.isActive() || !Utils.canModifyBoard(),
);
}
});
// We want to re-run this function any time a card is added.
this.autorun(() => {
const currentBoardId = Tracker.nonreactive(() => {
return Session.get('currentBoard');
});
Tracker.afterFlush(() => {
$cards.find(itemsSelector).droppable({
hoverClass: 'draggable-hover-card',
accept: '.js-member,.js-label',
drop(event, ui) {
const cardId = Blaze.getData(this)._id;
const card = ReactiveCache.getCard(cardId);
if (ui.draggable.hasClass('js-member')) {
const memberId = Blaze.getData(ui.draggable.get(0)).userId;
card.assignMember(memberId);
} else {
const labelId = Blaze.getData(ui.draggable.get(0))._id;
card.addLabel(labelId);
}
},
card.move(
currentBoard._id,
newSwimlaneId,
listId,
sortIndex.base + i * sortIndex.increment,
);
});
} else {
const cardDomElement = ui.item.get(0);
const card = Blaze.getData(cardDomElement);
const newSwimlaneId = targetSwimlaneId
? targetSwimlaneId
: card.swimlaneId || defaultSwimlaneId;
card.move(currentBoard._id, newSwimlaneId, listId, sortIndex.base);
}
if (boardComponent) boardComponent.setIsDragging(false);
},
sort(event, ui) {
const $boardCanvas = $('.board-canvas');
const boardCanvas = $boardCanvas[0];
if (event.pageX < 10) { // scroll to the left
boardCanvas.scrollLeft -= 15;
ui.helper[0].offsetLeft -= 15;
}
if (
event.pageX > boardCanvas.offsetWidth - 10 &&
boardCanvas.scrollLeft < $boardCanvas.data('scrollLeftMax') // don't scroll more than possible
) { // scroll to the right
boardCanvas.scrollLeft += 15;
}
if (
event.pageY > boardCanvas.offsetHeight - 10 &&
event.pageY + boardCanvas.scrollTop < $boardCanvas.data('scrollTopMax') // don't scroll more than possible
) { // scroll to the bottom
boardCanvas.scrollTop += 15;
}
if (event.pageY < 10) { // scroll to the top
boardCanvas.scrollTop -= 15;
}
},
activate(event, ui) {
const $boardCanvas = $('.board-canvas');
const boardCanvas = $boardCanvas[0];
// scrollTopMax and scrollLeftMax only available at Firefox (https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTopMax)
// https://www.it-swarm.com.de/de/javascript/so-erhalten-sie-den-maximalen-dokument-scrolltop-wert/1069126844/
$boardCanvas.data('scrollTopMax', boardCanvas.scrollHeight - boardCanvas.clientTop);
// https://stackoverflow.com/questions/5138373/how-do-i-get-the-max-value-of-scrollleft/5704386#5704386
$boardCanvas.data('scrollLeftMax', boardCanvas.scrollWidth - boardCanvas.clientWidth);
},
});
this.autorun(() => {
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',
// Disable drag-dropping when user is not member
!Utils.canModifyBoard(),
// Not disable drag-dropping while in multi-selection mode
// MultiSelection.isActive() || !Utils.canModifyBoard(),
);
}
});
// We want to re-run this function any time a card is added.
this.autorun(() => {
const currentBoardId = Tracker.nonreactive(() => {
return Session.get('currentBoard');
});
Tracker.afterFlush(() => {
$cards.find(itemsSelector).droppable({
hoverClass: 'draggable-hover-card',
accept: '.js-member,.js-label',
drop(event, ui) {
const cardId = Blaze.getData(this)._id;
const card = ReactiveCache.getCard(cardId);
if (ui.draggable.hasClass('js-member')) {
const memberId = Blaze.getData(ui.draggable.get(0)).userId;
card.assignMember(memberId);
} else {
const labelId = Blaze.getData(ui.draggable.get(0))._id;
card.addLabel(labelId);
}
},
});
});
},
});
});
Template.list.helpers({
listWidth() {
const user = ReactiveCache.getCurrentUser();
const list = Template.currentData();
@@ -254,31 +267,53 @@ BlazeComponent.extendComponent({
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');
collapsed() {
return Utils.getListCollapseState(this);
},
});
// initializeListResize as a method on the template instance
Template.list.onCreated(function () {
const tpl = this;
tpl.initializeListResize = function () {
// Resolve list data from the template instance to avoid relying on
// Template.currentData() from async callbacks where no current view exists.
const list = tpl.data || Blaze.getData(tpl.firstNode);
if (!list) {
console.warn('No list data available for list resize initialization');
return;
}
const $list = tpl.$('.js-list');
const $resizeHandle = tpl.$('.js-list-resize-handle');
const list = Template.currentData();
const $list = this.$('.js-list');
const $resizeHandle = this.$('.js-list-resize-handle');
const isCollapsed = Utils.getListCollapseState(list);
if (isCollapsed) {
// Collapsed lists do not render a resize handle by design.
return;
}
// 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();
if (!tpl.view.isDestroyed) {
tpl.initializeListResize();
}
}, 100);
return;
}
// Helper to get autoWidth state
const getAutoWidth = () => {
const user = ReactiveCache.getCurrentUser();
if (!user) return false;
return user.isAutoWidth(list.boardId);
};
// Reactively show/hide resize handle based on collapse and auto-width state
this.autorun(() => {
const isAutoWidth = this.autoWidth();
tpl.autorun(() => {
const isAutoWidth = getAutoWidth();
const isCollapsed = Utils.getListCollapseState(list);
if (isCollapsed || isAutoWidth) {
$resizeHandle.hide();
@@ -291,20 +326,34 @@ BlazeComponent.extendComponent({
let startX = 0;
let startWidth = 0;
let minWidth = 270; // Minimum width matching system default
let listConstraint = this.listConstraint(); // Store constraint value for use in event handlers
const component = this; // Store reference to component for use in event handlers
// Get listConstraint value
const getListConstraint = () => {
const user = ReactiveCache.getCurrentUser();
if (user) {
return user.getListConstraintFromStorage(list.boardId, list._id);
}
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) {}
return 550;
};
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');
@@ -331,7 +380,6 @@ BlazeComponent.extendComponent({
$list[0].style.setProperty('flex-grow', '0');
$list[0].style.setProperty('flex-shrink', '0');
e.preventDefault();
e.stopPropagation();
};
@@ -345,6 +393,7 @@ BlazeComponent.extendComponent({
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
const deltaX = currentX - startX;
const finalWidth = Math.max(minWidth, startWidth + deltaX);
const listConstraint = getListConstraint();
// Ensure the final width is applied
$list[0].style.setProperty('--list-width', `${finalWidth}px`);
@@ -361,14 +410,10 @@ BlazeComponent.extendComponent({
$('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') {
}
@@ -428,17 +473,15 @@ BlazeComponent.extendComponent({
$(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 or collapse changes
component.autorun(() => {
tpl.autorun(() => {
const collapsed = Utils.getListCollapseState(list);
if (component.autoWidth() || collapsed) {
if (getAutoWidth() || collapsed) {
$resizeHandle.hide();
} else {
$resizeHandle.show();
@@ -446,19 +489,13 @@ BlazeComponent.extendComponent({
});
// Clean up on component destruction
component.onDestroyed(() => {
tpl.view.onViewDestroyed(() => {
$(document).off('mousemove', doResize);
$(document).off('mouseup', stopResize);
$(document).off('touchmove', doResize);
$(document).off('touchend', stopResize);
});
},
}).register('list');
Template.list.helpers({
collapsed() {
return Utils.getListCollapseState(this);
},
};
});
Template.miniList.events({
@@ -468,15 +505,6 @@ Template.miniList.events({
},
});
// Enable drag-reorder for collapsed lists from .js-collapsed-list-drag area
this.$('.js-collapsed-list-drag').draggable({
axis: 'x',
helper: 'clone',
revert: 'invalid',
start(evt, ui) {
boardComponent.setIsDragging(true);
},
stop(evt, ui) {
boardComponent.setIsDragging(false);
}
});
// NOTE: Collapsed list drag-reorder was previously here but referenced
// boardComponent from an outer scope. If needed, this should be moved
// into Template.list.onRendered where boardComponent is available.
File diff suppressed because it is too large. Load diff
+66 -1
View File
@@ -30,7 +30,7 @@ template(name="listHeader")
| &nbsp;
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
else
a.list-collapse-indicator.js-collapse(title="{{_ 'collapse'}}")
a.list-collapse-indicator.js-collapse(title="{{#if collapsed}}{{_ 'uncollapse'}}{{else}}{{_ 'collapse'}}{{/if}}" aria-label="{{#if collapsed}}{{_ 'uncollapse'}}{{else}}{{_ 'collapse'}}{{/if}}")
if collapsed
i.fa.fa-caret-right
else
@@ -161,6 +161,18 @@ template(name="listActionPopup")
a.js-set-wip-limit
i.fa.fa-ban
| {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
hr
ul.pop-over-list
unless currentUser.isWorker
li
a.js-copy-list
i.fa.fa-clipboard
| {{_ 'copyListPopup-title'}}
if currentUser.isBoardAdmin
li
a.js-move-list
i.fa.fa-arrow-right
| {{_ 'moveListPopup-title'}}
unless currentUser.isWorker
hr
ul.pop-over-list
@@ -200,6 +212,11 @@ template(name="listMorePopup")
//
a.js-delete {{_ 'delete'}}
template(name="listArchivePopup")
p {{_ "list-archive-pop"}}
p {{_ "list-archive-suggest"}}
button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
template(name="listDeletePopup")
p {{_ "list-delete-pop"}}
unless archived
@@ -258,6 +275,54 @@ template(name="setListColorPopup")
button.primary.confirm.js-submit {{_ 'save'}}
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
template(name="copyListPopup")
label(for='copy-list-title') {{_ 'title'}}:
input#copy-list-title.full-line(type="text" value="{{title}}" autofocus)
label {{_ 'boards'}}:
select.js-select-boards
each boards
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
each swimlanes
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
div
input(type="radio" name="list-position" value="left" checked id="list-position-left" style="display: inline")
label(for="list-position-left") {{_ 'left-of-list'}}
div
input(type="radio" name="list-position" value="right" id="list-position-right" style="display: inline")
label(for="list-position-right") {{_ 'right-of-list'}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
template(name="moveListPopup")
label(for='move-list-title') {{_ 'title'}}:
input#move-list-title.full-line(type="text" value="{{title}}" autofocus)
label {{_ 'boards'}}:
select.js-select-boards
each boards
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
each swimlanes
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
div
input(type="radio" name="list-position" value="left" checked id="list-position-left" style="display: inline")
label(for="list-position-left") {{_ 'left-of-list'}}
div
input(type="radio" name="list-position" value="right" id="list-position-right" style="display: inline")
label(for="list-position-right") {{_ 'right-of-list'}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
template(name="addListPopup")
form.js-add-list-form
input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}" autocomplete="off" autofocus)
+356 -292
View File
@@ -2,19 +2,24 @@ import { ReactiveCache } from '/imports/reactiveCache';
import Lists from '../../../models/lists';
import { TAPi18n } from '/imports/i18n';
import dragscroll from '@wekanteam/dragscroll';
import { BoardSwimlaneListDialog } from '/client/lib/dialogWithBoardSwimlaneList';
import Cards from '/models/cards';
import { LIST_COLORS } from '/models/metadata/colors';
import { MultiSelection } from '/client/lib/multiSelection';
import { Utils } from '/client/lib/utils';
let listsColors;
Meteor.startup(() => {
listsColors = Lists.simpleSchema()._schema.color.allowedValues;
listsColors = LIST_COLORS;
});
BlazeComponent.extendComponent({
Template.listHeader.helpers({
canSeeAddCard() {
const list = Template.currentData();
return (
(!list.getWipLimit('enabled') ||
list.getWipLimit('soft') ||
!this.reachedWipLimit()) &&
!Template.instance().reachedWipLimit()) &&
!ReactiveCache.getCurrentUser().isWorker()
);
},
@@ -22,42 +27,19 @@ BlazeComponent.extendComponent({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
async starred(check = undefined) {
starred() {
const list = Template.currentData();
const status = list.isStarred();
if (check === undefined) {
// just check
return status;
} else {
await list.star(!status);
return !status;
}
return list.isStarred();
},
collapsed(check = undefined) {
collapsed() {
const list = Template.currentData();
const status = Utils.getListCollapseState(list);
if (check === undefined) {
// just check
return status;
} else {
const next = typeof check === 'boolean' ? check : !status;
Utils.setListCollapseState(list, next);
return next;
}
},
async editTitle(event) {
event.preventDefault();
const newTitle = this.childComponents('inlinedForm')[0]
.getValue()
.trim();
const list = this.currentData();
if (newTitle) {
await list.rename(newTitle.trim());
}
return Utils.getListCollapseState(list);
},
isWatching() {
const list = this.currentData();
const list = Template.currentData();
return list.findWatcher(Meteor.userId());
},
@@ -73,10 +55,9 @@ BlazeComponent.extendComponent({
cardsCount() {
const list = Template.currentData();
let swimlaneId = '';
if (Utils.boardView() === 'board-view-swimlanes')
swimlaneId = this.parentComponent()
.parentComponent()
.data()._id;
if (Utils.boardView() === 'board-view-swimlanes') {
swimlaneId = list.swimlaneId || '';
}
const ret = list.cards(swimlaneId).length;
return ret;
@@ -99,7 +80,8 @@ BlazeComponent.extendComponent({
},
showCardsCountForList(count) {
const limit = this.limitToShowCardsCount();
const currentUser = ReactiveCache.getCurrentUser();
const limit = currentUser ? currentUser.getLimitToShowCardsCount() : false;
return limit >= 0 && count >= limit;
},
@@ -111,40 +93,6 @@ BlazeComponent.extendComponent({
}
},
events() {
return [
{
'click .js-list-star'(event) {
event.preventDefault();
this.starred(!this.starred());
},
'click .js-collapse'(event) {
event.preventDefault();
this.collapsed(!this.collapsed());
},
'click .js-open-list-menu': Popup.open('listAction'),
'click .js-add-card.list-header-plus-top'(event) {
const listDom = $(event.target).parents(
`#js-list-${this.currentData()._id}`,
)[0];
const listComponent = BlazeComponent.getComponentForElement(listDom);
listComponent.openForm({
position: 'top',
});
},
'click .js-unselect-list'() {
Session.set('currentList', null);
},
submit: this.editTitle,
},
];
},
}).register('listHeader');
Template.listHeader.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
numberFieldsSum() {
const list = Template.currentData();
if (!list) return 0;
@@ -177,6 +125,7 @@ Template.listHeader.helpers({
}
return total;
},
hasNumberFieldsSum() {
const boardId = Session.get('currentBoard');
const fields = ReactiveCache.getCustomFields({
@@ -188,6 +137,56 @@ Template.listHeader.helpers({
},
});
// Helper function on template instance for reachedWipLimit check
Template.listHeader.onCreated(function () {
this.reachedWipLimit = function () {
const list = Template.currentData();
return (
list.getWipLimit('enabled') &&
list.getWipLimit('value') <= list.cards().length
);
};
});
Template.listHeader.events({
async 'click .js-list-star'(event) {
event.preventDefault();
const list = Template.currentData();
const status = list.isStarred();
await list.star(!status);
},
'click .js-collapse'(event) {
event.preventDefault();
const list = Template.currentData();
const status = Utils.getListCollapseState(list);
Utils.setListCollapseState(list, !status);
},
'click .js-open-list-menu': Popup.open('listAction'),
'click .js-add-card.list-header-plus-top'(event) {
const listDom = $(event.target).parents(
`#js-list-${Template.currentData()._id}`,
)[0];
const view = Blaze.getView(listDom, 'Template.list');
const listComponent = view?.templateInstance?.();
if (listComponent) {
listComponent.openForm({
position: 'top',
});
}
},
'click .js-unselect-list'() {
Session.set('currentList', null);
},
async 'submit'(event, tpl) {
event.preventDefault();
const newTitle = tpl.$('textarea,input[type=text]').val()?.trim();
const list = Template.currentData();
if (newTitle) {
await list.rename(newTitle.trim());
}
},
});
Template.listActionPopup.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
@@ -206,7 +205,8 @@ Template.listActionPopup.events({
'click .js-list-subscribe'() {},
'click .js-add-card.list-header-plus-top'(event) {
const listDom = $(`#js-list-${this._id}`)[0];
const listComponent = BlazeComponent.getComponentForElement(listDom);
const view = Blaze.getView(listDom, 'Template.list');
const listComponent = view?.templateInstance?.();
if (listComponent) {
listComponent.openForm({
position: 'top',
@@ -216,7 +216,8 @@ Template.listActionPopup.events({
},
'click .js-add-card.list-header-plus-bottom'(event) {
const listDom = $(`#js-list-${this._id}`)[0];
const listComponent = BlazeComponent.getComponentForElement(listDom);
const view = Blaze.getView(listDom, 'Template.list');
const listComponent = view?.templateInstance?.();
if (listComponent) {
listComponent.openForm({
position: 'bottom',
@@ -239,59 +240,17 @@ Template.listActionPopup.events({
if (!err && ret) Popup.back();
});
},
async 'click .js-close-list'(event) {
event.preventDefault();
'click .js-close-list': Popup.afterConfirm('listArchive', async function() {
await this.archive();
Popup.back();
},
Popup.close();
}),
'click .js-set-wip-limit': Popup.open('setWipLimit'),
'click .js-copy-list': Popup.open('copyList'),
'click .js-move-list': Popup.open('moveList'),
'click .js-more': Popup.open('listMore'),
});
BlazeComponent.extendComponent({
applyWipLimit() {
const list = Template.currentData();
const limit = parseInt(
Template.instance()
.$('.wip-limit-value')
.val(),
10,
);
if (limit < list.cards().length && !list.getWipLimit('soft')) {
Template.instance()
.$('.wip-limit-error')
.click();
} else {
Meteor.call('applyWipLimit', list._id, limit);
Popup.back();
}
},
async enableSoftLimit() {
const list = Template.currentData();
if (
list.getWipLimit('soft') &&
list.getWipLimit('value') < list.cards().length
) {
await list.setWipLimit(list.cards().length);
}
Meteor.call('enableSoftLimit', Template.currentData()._id);
},
async enableWipLimit() {
const list = Template.currentData();
// Prevent user from using previously stored wipLimit.value if it is less than the current number of cards in the list
if (
!list.getWipLimit('enabled') &&
list.getWipLimit('value') < list.cards().length
) {
await list.setWipLimit(list.cards().length);
}
Meteor.call('enableWipLimit', list._id);
},
Template.setWipLimitPopup.helpers({
isWipLimitSoft() {
return Template.currentData().getWipLimit('soft');
},
@@ -303,125 +262,225 @@ BlazeComponent.extendComponent({
wipLimitValue() {
return Template.currentData().getWipLimit('value');
},
});
events() {
return [
{
'click .js-enable-wip-limit': this.enableWipLimit,
'click .wip-limit-apply': this.applyWipLimit,
'click .wip-limit-error': Popup.open('wipLimitError'),
'click .materialCheckBox': this.enableSoftLimit,
},
];
Template.setWipLimitPopup.events({
async 'click .js-enable-wip-limit'() {
const list = Template.currentData();
// Prevent user from using previously stored wipLimit.value if it is less than the current number of cards in the list
if (
!list.getWipLimit('enabled') &&
list.getWipLimit('value') < list.cards().length
) {
await list.setWipLimit(list.cards().length);
}
Meteor.call('enableWipLimit', list._id);
},
}).register('setWipLimitPopup');
'click .wip-limit-apply'(event, tpl) {
const list = Template.currentData();
const limit = parseInt(
tpl.$('.wip-limit-value').val(),
10,
);
if (limit < list.cards().length && !list.getWipLimit('soft')) {
tpl.$('.wip-limit-error').click();
} else {
Meteor.call('applyWipLimit', list._id, limit);
Popup.back();
}
},
'click .wip-limit-error': Popup.open('wipLimitError'),
async 'click .materialCheckBox'() {
const list = Template.currentData();
if (
list.getWipLimit('soft') &&
list.getWipLimit('value') < list.cards().length
) {
await list.setWipLimit(list.cards().length);
}
Meteor.call('enableSoftLimit', Template.currentData()._id);
},
});
Template.listMorePopup.events({
'click .js-delete': Popup.afterConfirm('listDelete', function() {
Popup.back();
const allCards = this.allCards();
const allCardIds = _.pluck(allCards, '_id');
const list = Lists.findOne(this._id);
if (!list) return;
const allCards = list.allCards();
const allCardIds = allCards.map(c => c._id);
// it's okay if the linked cards are on the same list
if (
ReactiveCache.getCards({
$and: [
{ listId: { $ne: this._id } },
{ listId: { $ne: list._id } },
{ linkedId: { $in: allCardIds } },
],
}).length === 0
) {
allCardIds.map(_id => Cards.remove(_id));
Lists.remove(this._id);
Lists.remove(list._id);
} else {
// TODO: Figure out more informative message.
// Popup with a hint that the list cannot be deleted as there are
// linked cards. We can adapt the query above so we can list the linked
// cards.
// Related:
// client/components/cards/cardDetails.js about line 969
// https://github.com/wekan/wekan/issues/2785
const message = `${TAPi18n.__(
'delete-linked-cards-before-this-list',
)} linkedId: ${
this._id
list._id
} at client/components/lists/listHeader.js and https://github.com/wekan/wekan/issues/2785`;
alert(message);
}
Utils.goBoardId(this.boardId);
Utils.goBoardId(list.boardId);
}),
});
Template.listHeader.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
function registerListDialogTemplate(templateName) {
Template[templateName].helpers({
boards() {
return Template.instance().dialog.boards();
},
swimlanes() {
return Template.instance().dialog.swimlanes();
},
lists() {
return Template.instance().dialog.lists();
},
isDialogOptionBoardId(boardId) {
return Template.instance().dialog.isDialogOptionBoardId(boardId);
},
isDialogOptionSwimlaneId(swimlaneId) {
return Template.instance().dialog.isDialogOptionSwimlaneId(swimlaneId);
},
isDialogOptionListId(listId) {
return Template.instance().dialog.isDialogOptionListId(listId);
},
isTitleDefault(title) {
return Template.instance().dialog.isTitleDefault(title);
},
});
Template[templateName].events({
async 'click .js-done'(event, tpl) {
const dialog = tpl.dialog;
const boardSelect = tpl.$('.js-select-boards')[0];
const boardId = boardSelect?.options[boardSelect?.selectedIndex]?.value;
const swimlaneSelect = tpl.$('.js-select-swimlanes')[0];
const swimlaneId = swimlaneSelect?.options[swimlaneSelect?.selectedIndex]?.value;
const listSelect = tpl.$('.js-select-lists')[0];
const listId = listSelect?.options[listSelect?.selectedIndex]?.value || null;
const position = tpl.$('input[name="list-position"]:checked').val() || 'right';
try {
await dialog.setDone({ boardId, swimlaneId, listId, position });
} catch (e) {
console.error('Error in list dialog operation:', e);
}
Popup.back(2);
},
'change .js-select-boards'(event, tpl) {
tpl.dialog.getBoardData($(event.currentTarget).val());
},
'change .js-select-swimlanes'(event, tpl) {
tpl.dialog.selectedSwimlaneId.set($(event.currentTarget).val());
tpl.dialog.setFirstListId();
},
'change .js-select-lists'(event, tpl) {
tpl.dialog.selectedListId.set($(event.currentTarget).val());
},
});
}
Template.copyListPopup.onCreated(function () {
this.dialog = new BoardSwimlaneListDialog(this, {
getDialogOptions() {
return null;
},
async setDone(options) {
const tpl = Template.instance();
const title = tpl.$('#copy-list-title').val().trim();
if (!title) return;
const list = Template.currentData();
await Meteor.callAsync('copyList', list._id, options.boardId, options.swimlaneId, title, options.listId, options.position);
},
});
});
registerListDialogTemplate('copyListPopup');
Template.moveListPopup.onCreated(function () {
this.dialog = new BoardSwimlaneListDialog(this, {
getDialogOptions() {
return null;
},
async setDone(options) {
const tpl = Template.instance();
const title = tpl.$('#move-list-title').val().trim();
const list = Template.currentData();
await Meteor.callAsync('moveList', list._id, options.boardId, options.swimlaneId, options.listId, options.position, title);
},
});
});
registerListDialogTemplate('moveListPopup');
Template.setListColorPopup.onCreated(function () {
const data = Template.currentData();
this.currentList = Lists.findOne(data._id) || data;
this.currentColor = new ReactiveVar(this.currentList.color);
});
BlazeComponent.extendComponent({
onCreated() {
this.currentList = this.currentData();
this.currentColor = new ReactiveVar(this.currentList.color);
},
Template.setListColorPopup.helpers({
colors() {
return listsColors.map(color => ({ color, name: '' }));
},
isSelected(color) {
if (this.currentColor.get() === null) {
const tpl = Template.instance();
if (tpl.currentColor.get() === null) {
return color === 'white';
} else {
return this.currentColor.get() === color;
return tpl.currentColor.get() === color;
}
},
});
events() {
return [
{
'click .js-palette-color'() {
this.currentColor.set(this.currentData().color);
},
async 'click .js-submit'() {
await this.currentList.setColor(this.currentColor.get());
Popup.close();
},
async 'click .js-remove-color'() {
await this.currentList.setColor(null);
Popup.close();
},
},
];
Template.setListColorPopup.events({
'click .js-palette-color'(event, tpl) {
const paletteData = Blaze.getData(event.currentTarget);
tpl.currentColor.set(paletteData?.color);
},
}).register('setListColorPopup');
BlazeComponent.extendComponent({
applyListWidth() {
const list = Template.currentData();
const board = list.boardId;
const width = parseInt(
Template.instance()
.$('.list-width-value')
.val(),
10,
);
const constraint = parseInt(
Template.instance()
.$('.list-constraint-value')
.val(),
10,
);
// FIXME(mark-i-m): where do we put constants?
if (width < 270 || !width || constraint < 270 || !constraint) {
Template.instance()
.$('.list-width-error')
.click();
} else {
Meteor.call('applyListWidth', board, list._id, width, constraint);
Popup.back();
async 'submit form'(event, tpl) {
event.preventDefault();
console.log('[ListColor] submit form, color:', tpl.currentColor.get(), 'listId:', tpl.currentList?._id, 'setColor type:', typeof tpl.currentList?.setColor);
try {
const result = await tpl.currentList.setColor(tpl.currentColor.get());
console.log('[ListColor] submit form setColor result:', result);
} catch (err) {
console.error('[ListColor] submit form setColor error:', err);
}
Popup.close();
},
async 'click .js-submit'(event, tpl) {
event.preventDefault();
console.log('[ListColor] click submit, color:', tpl.currentColor.get(), 'listId:', tpl.currentList?._id, 'setColor type:', typeof tpl.currentList?.setColor);
try {
const result = await tpl.currentList.setColor(tpl.currentColor.get());
console.log('[ListColor] click submit setColor result:', result);
} catch (err) {
console.error('[ListColor] click submit setColor error:', err);
}
Popup.close();
},
async 'click .js-remove-color'(event, tpl) {
event.preventDefault();
console.log('[ListColor] remove color, listId:', tpl.currentList?._id);
try {
await tpl.currentList.setColor(null);
} catch (err) {
console.error('[ListColor] remove color error:', err);
}
Popup.close();
},
});
Template.setListWidthPopup.helpers({
listWidthValue() {
const list = Template.currentData();
const board = list.boardId;
@@ -439,122 +498,127 @@ BlazeComponent.extendComponent({
const user = ReactiveCache.getCurrentUser();
return user && user.isAutoWidth(boardId);
},
});
events() {
return [
{
'click .js-auto-width-board'() {
dragscroll.reset();
ReactiveCache.getCurrentUser().toggleAutoWidth(Utils.getCurrentBoardId());
},
'click .list-width-apply': this.applyListWidth,
'click .list-width-error': Popup.open('listWidthError'),
},
];
Template.setListWidthPopup.events({
'click .js-auto-width-board'() {
dragscroll.reset();
ReactiveCache.getCurrentUser().toggleAutoWidth(Utils.getCurrentBoardId());
},
}).register('setListWidthPopup');
'click .list-width-apply'(event, tpl) {
const list = Template.currentData();
const board = list.boardId;
const width = parseInt(
tpl.$('.list-width-value').val(),
10,
);
const constraint = parseInt(
tpl.$('.list-constraint-value').val(),
10,
);
BlazeComponent.extendComponent({
onCreated() {
this.currentBoard = Utils.getCurrentBoard();
this.currentSwimlaneId = new ReactiveVar(null);
this.currentListId = new ReactiveVar(null);
// FIXME(mark-i-m): where do we put constants?
if (width < 270 || !width || constraint < 270 || !constraint) {
tpl.$('.list-width-error').click();
} else {
Meteor.call('applyListWidth', board, list._id, width, constraint);
Popup.back();
}
},
'click .list-width-error': Popup.open('listWidthError'),
});
// Get the swimlane context from opener
const openerData = Popup.getOpenerComponent()?.data();
Template.addListPopup.onCreated(function () {
this.currentBoard = Utils.getCurrentBoard();
this.currentSwimlaneId = new ReactiveVar(null);
this.currentListId = new ReactiveVar(null);
// If opened from swimlane menu, openerData is the swimlane
if (openerData?.type === 'swimlane' || openerData?.type === 'template-swimlane') {
this.currentSwimlane = openerData;
this.currentSwimlaneId.set(openerData._id);
} else if (openerData?._id) {
// If opened from list menu, get swimlane from the list
const list = ReactiveCache.getList({ _id: openerData._id });
this.currentSwimlane = list?.swimlaneId ? ReactiveCache.getSwimlane({ _id: list.swimlaneId }) : null;
// Get the swimlane context from opener
const openerComponent = Popup.getOpenerComponent();
const openerData = openerComponent?.data || Popup._getTopStack()?.dataContext;
// If opened from swimlane menu, openerData is the swimlane
if (openerData?.type === 'swimlane' || openerData?.type === 'template-swimlane') {
this.currentSwimlane = openerData;
this.currentSwimlaneId.set(openerData._id);
} else if (openerData?._id) {
// If opened from list menu, get swimlane from the list
const list = ReactiveCache.getList({ _id: openerData._id });
if (list) {
this.currentSwimlane = list.swimlaneId
? ReactiveCache.getSwimlane({ _id: list.swimlaneId })
: null;
this.currentSwimlaneId.set(this.currentSwimlane?._id || null);
this.currentListId.set(openerData._id);
}
},
}
if (!this.currentSwimlaneId.get()) {
const defaultSwimlane = this.currentBoard.getDefaultSwimline?.();
if (defaultSwimlane?._id) {
this.currentSwimlane = defaultSwimlane;
this.currentSwimlaneId.set(defaultSwimlane._id);
}
}
});
Template.addListPopup.helpers({
currentSwimlaneData() {
const swimlaneId = this.currentSwimlaneId.get();
const tpl = Template.instance();
const swimlaneId = tpl.currentSwimlaneId.get();
return swimlaneId ? ReactiveCache.getSwimlane({ _id: swimlaneId }) : null;
},
currentListIdValue() {
return this.currentListId.get();
return Template.instance().currentListId.get();
},
swimlaneLists() {
const swimlaneId = this.currentSwimlaneId.get();
const tpl = Template.instance();
const swimlaneId = tpl.currentSwimlaneId.get();
if (swimlaneId) {
return ReactiveCache.getLists({ swimlaneId, archived: false }).sort((a, b) => a.sort - b.sort);
}
return this.currentBoard.lists;
return tpl.currentBoard.lists;
},
});
events() {
return [
{
'submit .js-add-list-form'(evt) {
evt.preventDefault();
Template.addListPopup.events({
async 'submit .js-add-list-form'(evt, tpl) {
evt.preventDefault();
const titleInput = this.find('.list-name-input');
const title = titleInput?.value.trim();
const titleInput = tpl.find('.list-name-input');
const title = titleInput?.value.trim();
if (!title) return;
if (!title) return;
let sortIndex = 0;
const boardId = Utils.getCurrentBoardId();
let swimlaneId = this.currentSwimlane?._id;
const positionInput = tpl.find('.list-position-input');
const afterListId =
positionInput && positionInput.value ? positionInput.value.trim() : null;
const nextListId =
positionInput &&
positionInput.selectedIndex >= 0 &&
positionInput.options[positionInput.selectedIndex + 1]
? positionInput.options[positionInput.selectedIndex + 1].value
: null;
const targetSwimlaneId =
(tpl.currentSwimlaneId && tpl.currentSwimlaneId.get && tpl.currentSwimlaneId.get()) ||
(tpl.currentSwimlane && tpl.currentSwimlane._id) ||
null;
const positionInput = this.find('.list-position-input');
if (positionInput && positionInput.value) {
const positionId = positionInput.value.trim();
const selectedList = ReactiveCache.getList({ boardId, _id: positionId, archived: false });
if (selectedList) {
sortIndex = selectedList.sort + 1;
// Use the swimlane ID from the selected list to ensure the new list
// is added to the same swimlane as the selected list
swimlaneId = selectedList.swimlaneId;
} else {
// No specific position, add at end of swimlane
if (swimlaneId) {
const swimlaneLists = ReactiveCache.getLists({ swimlaneId, archived: false });
const lastSwimlaneList = swimlaneLists.sort((a, b) => b.sort - a.sort)[0];
sortIndex = Utils.calculateIndexData(lastSwimlaneList, null).base;
} else {
const lastList = this.currentBoard.getLastList();
sortIndex = Utils.calculateIndexData(lastList, null).base;
}
}
} else {
// No position input, add at end of swimlane
if (swimlaneId) {
const swimlaneLists = ReactiveCache.getLists({ swimlaneId, archived: false });
const lastSwimlaneList = swimlaneLists.sort((a, b) => b.sort - a.sort)[0];
sortIndex = Utils.calculateIndexData(lastSwimlaneList, null).base;
} else {
const lastList = this.currentBoard.getLastList();
sortIndex = Utils.calculateIndexData(lastList, null).base;
}
}
Lists.insert({
title,
boardId: Session.get('currentBoard'),
sort: sortIndex,
type: 'list',
swimlaneId: swimlaneId,
});
Popup.back();
},
'click .js-list-template': Popup.open('searchElement'),
},
];
try {
await Meteor.callAsync('createListAfter', {
title,
boardId: Session.get('currentBoard'),
swimlaneId: targetSwimlaneId,
afterListId,
nextListId,
type: 'list',
});
Popup.back();
} catch (error) {
console.error('Failed to create list after selected list:', error);
}
},
}).register('addListPopup');
'click .js-list-template': Popup.open('searchElement'),
});
+14 -15
View File
@@ -1,5 +1,6 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import AccessibilitySettings from '/models/accessibilitySettings';
// Shared helpers for both accessibility templates
const accessibilityHelpers = {
@@ -18,21 +19,19 @@ const accessibilityHelpers = {
};
// Main accessibility page component
BlazeComponent.extendComponent({
onCreated() {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
Template.accessibility.onCreated(function () {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
Meteor.subscribe('setting');
Meteor.subscribe('accessibilitySettings');
},
...accessibilityHelpers
}).register('accessibility');
Meteor.subscribe('setting');
Meteor.subscribe('accessibilitySettings');
});
Template.accessibility.helpers(accessibilityHelpers);
// Header bar component
BlazeComponent.extendComponent({
onCreated() {
Meteor.subscribe('accessibilitySettings');
},
...accessibilityHelpers
}).register('accessibilityHeaderBar');
Template.accessibilityHeaderBar.onCreated(function () {
Meteor.subscribe('accessibilitySettings');
});
Template.accessibilityHeaderBar.helpers(accessibilityHelpers);
+2
View File
@@ -1,3 +1,5 @@
import Boards from '/models/boards';
Template.bookmarks.helpers({
hasStarredBoards() {
const user = ReactiveCache.getCurrentUser();
+49 -10
View File
@@ -1,18 +1,57 @@
import { CardSearchPagedComponent } from '../../lib/cardSearch';
import { CardSearchPaged } from '../../lib/cardSearch';
BlazeComponent.extendComponent({}).register('brokenCardsHeaderBar');
Template.brokenCards.onCreated(function () {
const search = new CardSearchPaged(this);
this.search = search;
Meteor.subscribe('brokenCards', search.sessionId);
});
Template.brokenCards.helpers({
userId() {
return Meteor.userId();
},
// Return ReactiveVars so jade can use .get pattern
searching() {
return Template.instance().search.searching;
},
hasResults() {
return Template.instance().search.hasResults;
},
hasQueryErrors() {
return Template.instance().search.hasQueryErrors;
},
errorMessages() {
return Template.instance().search.queryErrorMessages();
},
resultsCount() {
return Template.instance().search.resultsCount;
},
resultsHeading() {
return Template.instance().search.resultsHeading;
},
results() {
return Template.instance().search.results;
},
getSearchHref() {
return Template.instance().search.getSearchHref();
},
hasPreviousPage() {
return Template.instance().search.hasPreviousPage;
},
hasNextPage() {
return Template.instance().search.hasNextPage;
},
});
class BrokenCardsComponent extends CardSearchPagedComponent {
onCreated() {
super.onCreated();
Meteor.subscribe('brokenCards', this.sessionId);
}
}
BrokenCardsComponent.register('brokenCards');
Template.brokenCards.events({
'click .js-next-page'(evt, tpl) {
evt.preventDefault();
tpl.search.nextPage();
},
'click .js-previous-page'(evt, tpl) {
evt.preventDefault();
tpl.search.previousPage();
},
});
+117 -166
View File
@@ -1,178 +1,38 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
import { TAPi18n } from '/imports/i18n';
import { Utils } from '/client/lib/utils';
// const subManager = new SubsManager();
BlazeComponent.extendComponent({
Template.dueCardsHeaderBar.helpers({
dueCardsView() {
// eslint-disable-next-line no-console
// console.log('sort:', Utils.dueCardsView());
return Utils && Utils.dueCardsView ? Utils.dueCardsView() : 'me';
},
events() {
return [
{
'click .js-due-cards-view-change': Popup.open('dueCardsViewChange'),
},
];
},
}).register('dueCardsHeaderBar');
Template.dueCards.helpers({
userId() {
return Meteor.userId();
},
dueCardsList() {
const component = BlazeComponent.getComponentForElement(this.firstNode);
if (component && component.dueCardsList) {
return component.dueCardsList();
}
return [];
},
hasResults() {
const component = BlazeComponent.getComponentForElement(this.firstNode);
if (component && component.hasResults) {
return component.hasResults.get();
}
return false;
},
searching() {
const component = BlazeComponent.getComponentForElement(this.firstNode);
if (component && component.isLoading) {
return component.isLoading.get();
}
return true; // Show loading by default
},
hasQueryErrors() {
return false; // No longer using search, so always false
},
errorMessages() {
return []; // No longer using search, so always empty
},
cardsCount() {
const component = BlazeComponent.getComponentForElement(this.firstNode);
if (component && component.cardsCount) {
return component.cardsCount();
}
return 0;
},
resultsText() {
const component = BlazeComponent.getComponentForElement(this.firstNode);
if (component && component.resultsText) {
return component.resultsText();
}
return '';
},
});
BlazeComponent.extendComponent({
events() {
return [
{
'click .js-due-cards-view-me'() {
if (Utils && Utils.setDueCardsView) {
Utils.setDueCardsView('me');
}
Popup.back();
},
Template.dueCardsHeaderBar.events({
'click .js-due-cards-view-change': Popup.open('dueCardsViewChange'),
});
'click .js-due-cards-view-all'() {
if (Utils && Utils.setDueCardsView) {
Utils.setDueCardsView('all');
}
Popup.back();
},
},
];
},
}).register('dueCardsViewChangePopup');
Template.dueCards.onCreated(function () {
this._cachedCards = null;
this._cachedTimestamp = null;
this.subscriptionHandle = null;
this.isLoading = new ReactiveVar(true);
this.hasResults = new ReactiveVar(false);
this.searching = new ReactiveVar(false);
class DueCardsComponent extends BlazeComponent {
onCreated() {
super.onCreated();
const tpl = this;
this._cachedCards = null;
this._cachedTimestamp = null;
this.subscriptionHandle = null;
this.isLoading = new ReactiveVar(true);
this.hasResults = new ReactiveVar(false);
this.searching = new ReactiveVar(false);
// Subscribe to the optimized due cards publication
this.autorun(() => {
const allUsers = this.dueCardsView() === 'all';
if (this.subscriptionHandle) {
this.subscriptionHandle.stop();
}
this.subscriptionHandle = Meteor.subscribe('dueCards', allUsers);
// Update loading state based on subscription
this.autorun(() => {
if (this.subscriptionHandle && this.subscriptionHandle.ready()) {
if (process.env.DEBUG === 'true') {
console.log('dueCards: subscription ready, loading data...');
}
this.isLoading.set(false);
const cards = this.dueCardsList();
this.hasResults.set(cards && cards.length > 0);
} else {
if (process.env.DEBUG === 'true') {
console.log('dueCards: subscription not ready, showing loading...');
}
this.isLoading.set(true);
this.hasResults.set(false);
}
});
});
}
onDestroyed() {
super.onDestroyed();
if (this.subscriptionHandle) {
this.subscriptionHandle.stop();
}
}
dueCardsView() {
// eslint-disable-next-line no-console
//console.log('sort:', Utils.dueCardsView());
function dueCardsView() {
return Utils && Utils.dueCardsView ? Utils.dueCardsView() : 'me';
}
sortByBoard() {
return this.dueCardsView() === 'board';
}
hasResults() {
return this.hasResults.get();
}
cardsCount() {
const cards = this.dueCardsList();
return cards ? cards.length : 0;
}
resultsText() {
const count = this.cardsCount();
if (count === 1) {
return TAPi18n.__('one-card-found');
} else {
// Get the translated text and manually replace %s with the count
const baseText = TAPi18n.__('n-cards-found');
const result = baseText.replace('%s', count);
if (process.env.DEBUG === 'true') {
console.log('dueCards: base text:', baseText, 'count:', count, 'result:', result);
}
return result;
}
}
dueCardsList() {
function dueCardsList() {
// Check if subscription is ready
if (!this.subscriptionHandle || !this.subscriptionHandle.ready()) {
if (!tpl.subscriptionHandle || !tpl.subscriptionHandle.ready()) {
if (process.env.DEBUG === 'true') {
console.log('dueCards client: subscription not ready');
}
@@ -180,11 +40,11 @@ class DueCardsComponent extends BlazeComponent {
}
// Use cached results if available to avoid expensive re-sorting
if (this._cachedCards && this._cachedTimestamp && (Date.now() - this._cachedTimestamp < 5000)) {
if (tpl._cachedCards && tpl._cachedTimestamp && (Date.now() - tpl._cachedTimestamp < 5000)) {
if (process.env.DEBUG === 'true') {
console.log('dueCards client: using cached results,', this._cachedCards.length, 'cards');
console.log('dueCards client: using cached results,', tpl._cachedCards.length, 'cards');
}
return this._cachedCards;
return tpl._cachedCards;
}
// Get cards directly from the subscription (already sorted by the publication)
@@ -208,7 +68,7 @@ class DueCardsComponent extends BlazeComponent {
}
// Filter cards based on user view preference
const allUsers = this.dueCardsView() === 'all';
const allUsers = dueCardsView() === 'all';
const currentUser = ReactiveCache.getCurrentUser();
let filteredCards = cards;
@@ -255,15 +115,106 @@ class DueCardsComponent extends BlazeComponent {
}
// Cache the results for 5 seconds to avoid re-filtering on every render
this._cachedCards = filteredCards;
this._cachedTimestamp = Date.now();
tpl._cachedCards = filteredCards;
tpl._cachedTimestamp = Date.now();
// Update reactive variables
this.hasResults.set(filteredCards && filteredCards.length > 0);
this.isLoading.set(false);
tpl.hasResults.set(filteredCards && filteredCards.length > 0);
tpl.isLoading.set(false);
return filteredCards;
}
}
DueCardsComponent.register('dueCards');
// Store dueCardsList on the instance so helpers can call it
this.dueCardsList = dueCardsList;
this.dueCardsView = dueCardsView;
// Subscribe to the optimized due cards publication
this.autorun(() => {
const allUsers = dueCardsView() === 'all';
if (tpl.subscriptionHandle) {
tpl.subscriptionHandle.stop();
}
tpl.subscriptionHandle = Meteor.subscribe('dueCards', allUsers);
// Update loading state based on subscription
tpl.autorun(() => {
if (tpl.subscriptionHandle && tpl.subscriptionHandle.ready()) {
if (process.env.DEBUG === 'true') {
console.log('dueCards: subscription ready, loading data...');
}
tpl.isLoading.set(false);
const cards = dueCardsList();
tpl.hasResults.set(cards && cards.length > 0);
} else {
if (process.env.DEBUG === 'true') {
console.log('dueCards: subscription not ready, showing loading...');
}
tpl.isLoading.set(true);
tpl.hasResults.set(false);
}
});
});
});
Template.dueCards.onDestroyed(function () {
if (this.subscriptionHandle) {
this.subscriptionHandle.stop();
}
});
Template.dueCards.helpers({
userId() {
return Meteor.userId();
},
// Return ReactiveVar so jade can use .get pattern
searching() {
return Template.instance().isLoading;
},
hasResults() {
return Template.instance().hasResults;
},
hasQueryErrors() {
return new ReactiveVar(false);
},
errorMessages() {
return [];
},
dueCardsList() {
const tpl = Template.instance();
return tpl.dueCardsList ? tpl.dueCardsList() : [];
},
resultsText() {
const tpl = Template.instance();
const cards = tpl.dueCardsList ? tpl.dueCardsList() : [];
const count = cards ? cards.length : 0;
if (count === 1) {
return TAPi18n.__('one-card-found');
} else {
// Get the translated text and manually replace %s with the count
const baseText = TAPi18n.__('n-cards-found');
const result = baseText.replace('%s', count);
if (process.env.DEBUG === 'true') {
console.log('dueCards: base text:', baseText, 'count:', count, 'result:', result);
}
return result;
}
},
});
Template.dueCardsViewChangePopup.events({
'click .js-due-cards-view-me'() {
if (Utils && Utils.setDueCardsView) {
Utils.setDueCardsView('me');
}
Popup.back();
},
'click .js-due-cards-view-all'() {
if (Utils && Utils.setDueCardsView) {
Utils.setDueCardsView('all');
}
Popup.back();
},
});
+32 -32
View File
@@ -1,5 +1,9 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { findWhere } from '/imports/lib/collectionHelpers';
import { TAPi18n } from '/imports/i18n';
import Attachments from '/models/attachments';
import { Utils } from '/client/lib/utils';
import autosize from 'autosize';
var converter = require('@wekanteam/html-to-markdown');
const specialHandles = [
@@ -19,8 +23,8 @@ const boardSpecialHandles = [
const specialHandleNames = specialHandles.map(m => m.username);
BlazeComponent.extendComponent({
onRendered() {
Template.editor.onRendered(function () {
const tpl = this;
// Start: Copy <pre> code https://github.com/wekan/wekan/issues/5149
// TODO: Try to make copyPre visible at Card Details after editing or closing editor or Card Details.
// - Also this same TODO below at event, if someone gets it working.
@@ -66,7 +70,7 @@ BlazeComponent.extendComponent({
})
.filter(Boolean);
// Order: 1. Users, 2. Card-specific options, 3. Board-wide options
callback(_.union(users, cardSpecialHandles, boardSpecialHandles));
callback([...new Set([...users, ...cardSpecialHandles, ...boardSpecialHandles])]);
},
template(user) {
if (user.profile && user.profile.fullname) {
@@ -89,7 +93,7 @@ BlazeComponent.extendComponent({
];
const enableTextarea = function() {
const $textarea = this.$(textareaSelector);
const $textarea = tpl.$(textareaSelector);
autosize($textarea);
$textarea.escapeableTextComplete(mentions);
};
@@ -206,11 +210,11 @@ BlazeComponent.extendComponent({
const currentCard = Utils.getCurrentCard();
const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
const processUpload = function(file) {
const uploader = Attachments.insert(
const processUpload = async function(file) {
const uploader = await Attachments.insertAsync(
{
file,
meta: Utils.getCommonAttachmentMetaFrom(card),
meta: Utils.getCommonAttachmentMetaFrom(currentCard),
chunkSize: 'dynamic',
},
false,
@@ -314,29 +318,25 @@ BlazeComponent.extendComponent({
enableTextarea();
}
enableTextarea();
},
events() {
return [
{
'click a.fa.fa-copy'(event) {
const $editor = this.$('textarea.editor');
const promise = Utils.copyTextToClipboard($editor[0].value);
});
const $tooltip = this.$('.copied-tooltip');
Utils.showCopied(promise, $tooltip);
},
'click a.fa.fa-brands.fa-markdown'(event) {
const $editor = this.$('textarea.editor');
$editor[0].value = converter.convert($editor[0].value);
},
// TODO: Try to make copyPre visible at Card Details after editing or closing editor or Card Details.
//'click .js-close-inlined-form'(event) {
// Utils.copyPre();
//},
}
]
}
}).register('editor');
Template.editor.events({
'click a.fa.fa-copy'(event, tpl) {
const $editor = tpl.$('textarea.editor');
const promise = Utils.copyTextToClipboard($editor[0].value);
const $tooltip = tpl.$('.copied-tooltip');
Utils.showCopied(promise, $tooltip);
},
'click a.fa.fa-brands.fa-markdown'(event, tpl) {
const $editor = tpl.$('textarea.editor');
$editor[0].value = converter.convert($editor[0].value);
},
// TODO: Try to make copyPre visible at Card Details after editing or closing editor or Card Details.
//'click .js-close-inlined-form'(event) {
// Utils.copyPre();
//},
});
import DOMPurify from 'dompurify';
import { sanitizeHTML } from '/imports/lib/secureDOMPurify';
@@ -387,7 +387,7 @@ Blaze.Template.registerHelper(
const currentBoard = Utils.getCurrentBoard();
if (!currentBoard)
return HTML.Raw(sanitizeHTML(content));
const knowedUsers = _.union(currentBoard.members
const knowedUsers = [...new Set([...currentBoard.members
.filter(member => member.isActive)
.map(member => {
const u = ReactiveCache.getUser(member.userId);
@@ -395,14 +395,14 @@ Blaze.Template.registerHelper(
member.username = u.username;
}
return member;
}), [...specialHandles]);
}), ...specialHandles])];
const mentionRegex = /\B@([\w.-]*)/gi;
let currentMention;
while ((currentMention = mentionRegex.exec(content)) !== null) {
const [fullMention, quoteduser, simple] = currentMention;
const username = quoteduser || simple;
const knowedUser = _.findWhere(knowedUsers, { username });
const knowedUser = findWhere(knowedUsers, { username });
if (!knowedUser) {
continue;
}
+15
View File
@@ -84,6 +84,21 @@
margin-right: auto;
margin-left: auto;
}
.js-new-search {
cursor: pointer;
transition: background-color 0.2s;
}
.board-color-belize.js-new-search {
background-color: #216694 !important;
color: #fff !important;
border: none !important;
}
.board-color-belize.js-new-search:hover {
background-color: #1a4d6a !important;
}
.board-color-belize.js-new-search:active {
background-color: #0d334d !important;
}
.global-search-operator {
font-family: Courier;
}
+19 -40
View File
@@ -11,68 +11,47 @@ template(name="globalSearchModalTitle")
| {{_ 'globalSearch-title'}}
template(name="resultsPaged")
if resultsHeading.get
h1
= resultsHeading.get
a(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}")
| 🔗
each card in results.get
if resultsHeading
h1= resultsHeading
each card in results
+resultCard(card)
table.global-search-footer
tr
td.global-search-previous-page
if hasPreviousPage.get
if hasPreviousPage
button.js-previous-page
| {{_ 'previous-page' }}
td.global-search-next-page(align="right")
if hasNextPage.get
if hasNextPage
button.js-next-page
| {{_ 'next-page' }}
template(name="globalSearch")
if currentUser
.wrapper
form.global-search-page.js-search-query-form
.wrapper(class=boardColorClass)
form.global-search-page.js-search-query-form(onsubmit="return false;")
input.global-search-query-input(
style="{# if hasResults.get #}display: inline-block;{#/if#}"
style="display: inline-block;"
id="global-search-input"
type="text"
name="searchQuery"
placeholder="{{_ 'search-example'}}"
value="{{ query.get }}"
value="{{ queryValue }}"
autofocus dir="auto"
)
a.js-new-search
button.js-new-search.board-color-belize(type="button" style="background: #216694 !important; background-color: #216694 !important; color: #fff !important; border: none !important;")
| 🧹
if debug.get.show
h1 Debug
if debug.get.showSelector
h2 Selector
button.js-copy-debug-selector
= 'Copy'
pre(
id="debug-selector"
)
= sessionData.selector
if debug.get.showProjection
h2 Projection
button.js-copy-debug-projection
= 'Copy'
pre(
id="debug-projection"
)
= sessionData.projection
if searching.get
if searching
+spinner
else if hasResults.get
else if hasResults
.global-search-results-list-wrapper
if hasQueryErrors.get
if hasQueryErrors
ul
each msg in errorMessages
li.global-search-error-messages
= msg
+resultsPaged(this)
else if serverError.get
+resultsPaged getSearchData
else if serverError
.global-search-page
.global-search-help
h1 {{_ 'server-error' }}
@@ -83,22 +62,22 @@ template(name="globalSearch")
.global-search-help
h2 {{_ 'boards' }}
.lists-wrapper
each title in myBoardNames.get
each title in myBoardNames
span.card-label.list-title.js-board-title
= title
h2 {{_ 'lists' }}
.lists-wrapper
each title in myLists.get
each title in myLists
span.card-label.list-title.js-list-title
= title
h2 {{_ 'label-colors' }}
.palette-colors: each label in labelColors
span.card-label.palette-color.js-label-color(class="card-label-{{label.color}}")
= label.name
if myLabelNames.get.length
if myLabelNames
h2 {{_ 'label-names' }}
.lists-wrapper
each name in myLabelNames.get
each name in myLabelNames
span.card-label.list-title.js-label-name
= name
.global-search-instructions
+420 -275
View File
@@ -1,299 +1,444 @@
import { TAPi18n } from '/imports/i18n';
import { CardSearchPagedComponent } from '../../lib/cardSearch';
import Boards from '../../../models/boards';
import { CardSearchPaged } from '../../lib/cardSearch';
import { LABEL_COLORS } from '/models/metadata/colors';
import { Query, QueryErrors } from '../../../config/query-classes';
import { ReactiveCache } from '/imports/reactiveCache';
// const subManager = new SubsManager();
BlazeComponent.extendComponent({
events() {
return [
{
'click .js-due-cards-view-change': Popup.open('globalSearchViewChange'),
},
];
},
}).register('globalSearchHeaderBar');
Template.globalSearchHeaderBar.events({
'click .js-due-cards-view-change': Popup.open('globalSearchViewChange'),
});
Template.globalSearch.onCreated(function () {
const search = new CardSearchPaged(this);
this.search = search;
this.myLists = new ReactiveVar([]);
this.myLabelNames = new ReactiveVar([]);
this.myBoardNames = new ReactiveVar([]);
this.parsingErrors = new QueryErrors();
this.queryParams = null;
Meteor.call('myLists', (err, data) => {
if (!err) {
this.myLists.set(data);
}
});
Meteor.call('myLabelNames', (err, data) => {
if (!err) {
this.myLabelNames.set(data);
}
});
Meteor.call('myBoardNames', (err, data) => {
if (!err) {
this.myBoardNames.set(data);
}
});
});
Template.globalSearch.onRendered(function () {
Meteor.subscribe('setting');
// eslint-disable-next-line no-console
//console.log('lang:', TAPi18n.getLanguage());
if (Session.get('globalQuery')) {
searchAllBoards(this, Session.get('globalQuery'));
}
});
function searchAllBoards(tpl, queryText) {
const search = tpl.search;
queryText = queryText.trim();
// eslint-disable-next-line no-console
//console.log('queryText:', queryText);
search.query.set(queryText);
search.resetSearch();
tpl.parsingErrors = new QueryErrors();
if (!queryText) {
return;
}
search.searching.set(true);
const query = new Query();
query.buildParams(queryText);
// eslint-disable-next-line no-console
// console.log('params:', query.getParams());
tpl.queryParams = query.getQueryParams().getParams();
if (query.hasErrors()) {
search.searching.set(false);
search.queryErrors = query.errors();
search.hasResults.set(true);
search.hasQueryErrors.set(true);
return;
}
search.runGlobalSearch(query.getQueryParams());
}
function errorMessages(tpl) {
if (tpl.parsingErrors.hasErrors()) {
return tpl.parsingErrors.errorMessages();
}
return tpl.search.queryErrorMessages();
}
Template.globalSearch.helpers({
userId() {
return Meteor.userId();
},
});
class GlobalSearchComponent extends CardSearchPagedComponent {
onCreated() {
super.onCreated();
this.myLists = new ReactiveVar([]);
this.myLabelNames = new ReactiveVar([]);
this.myBoardNames = new ReactiveVar([]);
this.parsingErrors = new QueryErrors();
this.queryParams = null;
// Return ReactiveVar values so Blaze can use them
searching() {
const val = Template.instance().search.searching.get();
return val;
},
hasResults() {
const val = Template.instance().search.hasResults.get();
return val;
},
hasQueryErrors() {
const val = Template.instance().search.hasQueryErrors.get();
return val;
},
serverError() {
const val = Template.instance().search.serverError.get();
return val;
},
query() {
return Template.instance().search.query;
},
debug() {
const val = Template.instance().search.debug.get();
return val;
},
resultsHeading() {
const rv = Template.instance().search.resultsHeading;
return rv ? rv.get() : '';
},
results() {
const rv = Template.instance().search.results;
return rv ? rv.get() : [];
},
hasNextPage() {
const rv = Template.instance().search.hasNextPage;
return rv ? rv.get() : false;
},
hasPreviousPage() {
const rv = Template.instance().search.hasPreviousPage;
return rv ? rv.get() : false;
},
sessionData() {
return Template.instance().search.sessionData;
},
getSearchHref() {
return Template.instance().search.getSearchHref();
},
Meteor.call('myLists', (err, data) => {
if (!err) {
this.myLists.set(data);
}
});
Meteor.call('myLabelNames', (err, data) => {
if (!err) {
this.myLabelNames.set(data);
}
});
Meteor.call('myBoardNames', (err, data) => {
if (!err) {
this.myBoardNames.set(data);
}
});
}
onRendered() {
Meteor.subscribe('setting');
// eslint-disable-next-line no-console
//console.log('lang:', TAPi18n.getLanguage());
if (Session.get('globalQuery')) {
this.searchAllBoards(Session.get('globalQuery'));
}
}
resetSearch() {
super.resetSearch();
this.parsingErrors = new QueryErrors();
}
myLists() {
const rv = Template.instance().myLists;
return rv ? rv.get() : [];
},
myLabelNames() {
const rv = Template.instance().myLabelNames;
return rv ? rv.get() : [];
},
myBoardNames() {
const rv = Template.instance().myBoardNames;
return rv ? rv.get() : [];
},
errorMessages() {
if (this.parsingErrors.hasErrors()) {
return this.parsingErrors.errorMessages();
}
return this.queryErrorMessages();
}
return errorMessages(Template.instance());
},
parsingErrorMessages() {
this.parsingErrors.errorMessages();
}
getSearchData() {
return Template.instance().search;
},
searchAllBoards(queryText) {
queryText = queryText.trim();
// eslint-disable-next-line no-console
//console.log('queryText:', queryText);
this.query.set(queryText);
this.resetSearch();
if (!queryText) {
return;
}
this.searching.set(true);
const query = new Query();
query.buildParams(queryText);
// eslint-disable-next-line no-console
// console.log('params:', query.getParams());
this.queryParams = query.getQueryParams().getParams();
if (query.hasErrors()) {
this.searching.set(false);
this.queryErrors = query.errors();
this.hasResults.set(true);
this.hasQueryErrors.set(true);
return;
}
this.runGlobalSearch(query.getQueryParams());
}
boardColorClass() {
const boardId = Session.get('currentBoard');
const board = boardId ? ReactiveCache.getBoard(boardId) : null;
return board ? board.colorClass() : 'board-color-belize';
},
searchInstructions() {
const tags = {
operator_board: TAPi18n.__('operator-board'),
operator_list: TAPi18n.__('operator-list'),
operator_swimlane: TAPi18n.__('operator-swimlane'),
operator_comment: TAPi18n.__('operator-comment'),
operator_label: TAPi18n.__('operator-label'),
operator_label_abbrev: TAPi18n.__('operator-label-abbrev'),
operator_user: TAPi18n.__('operator-user'),
operator_user_abbrev: TAPi18n.__('operator-user-abbrev'),
operator_member: TAPi18n.__('operator-member'),
operator_member_abbrev: TAPi18n.__('operator-member-abbrev'),
operator_assignee: TAPi18n.__('operator-assignee'),
operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'),
operator_creator: TAPi18n.__('operator-creator'),
operator_due: TAPi18n.__('operator-due'),
operator_created: TAPi18n.__('operator-created'),
operator_modified: TAPi18n.__('operator-modified'),
operator_status: TAPi18n.__('operator-status'),
operator_has: TAPi18n.__('operator-has'),
operator_sort: TAPi18n.__('operator-sort'),
operator_limit: TAPi18n.__('operator-limit'),
operator_debug: TAPi18n.__('operator-debug'),
operator_org: TAPi18n.__('operator-org'),
operator_team: TAPi18n.__('operator-team'),
predicate_overdue: TAPi18n.__('predicate-overdue'),
predicate_archived: TAPi18n.__('predicate-archived'),
predicate_all: TAPi18n.__('predicate-all'),
predicate_ended: TAPi18n.__('predicate-ended'),
predicate_week: TAPi18n.__('predicate-week'),
predicate_month: TAPi18n.__('predicate-month'),
predicate_quarter: TAPi18n.__('predicate-quarter'),
predicate_year: TAPi18n.__('predicate-year'),
predicate_attachment: TAPi18n.__('predicate-attachment'),
predicate_description: TAPi18n.__('predicate-description'),
predicate_checklist: TAPi18n.__('predicate-checklist'),
predicate_public: TAPi18n.__('predicate-public'),
predicate_private: TAPi18n.__('predicate-private'),
predicate_due: TAPi18n.__('predicate-due'),
predicate_created: TAPi18n.__('predicate-created'),
predicate_modified: TAPi18n.__('predicate-modified'),
predicate_start: TAPi18n.__('predicate-start'),
predicate_end: TAPi18n.__('predicate-end'),
predicate_assignee: TAPi18n.__('predicate-assignee'),
predicate_member: TAPi18n.__('predicate-member'),
predicate_selector: TAPi18n.__('predicate-selector'),
predicate_projection: TAPi18n.__('predicate-projection'),
};
try {
const tags = {
operator_board: TAPi18n.__('operator-board'),
operator_list: TAPi18n.__('operator-list'),
operator_swimlane: TAPi18n.__('operator-swimlane'),
operator_comment: TAPi18n.__('operator-comment'),
operator_label: TAPi18n.__('operator-label'),
operator_label_abbrev: TAPi18n.__('operator-label-abbrev'),
operator_user: TAPi18n.__('operator-user'),
operator_user_abbrev: TAPi18n.__('operator-user-abbrev'),
operator_member: TAPi18n.__('operator-member'),
operator_member_abbrev: TAPi18n.__('operator-member-abbrev'),
operator_assignee: TAPi18n.__('operator-assignee'),
operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'),
operator_creator: TAPi18n.__('operator-creator'),
operator_due: TAPi18n.__('operator-due'),
operator_created: TAPi18n.__('operator-created'),
operator_modified: TAPi18n.__('operator-modified'),
operator_status: TAPi18n.__('operator-status'),
operator_has: TAPi18n.__('operator-has'),
operator_sort: TAPi18n.__('operator-sort'),
operator_limit: TAPi18n.__('operator-limit'),
operator_debug: TAPi18n.__('operator-debug'),
operator_org: TAPi18n.__('operator-org'),
operator_team: TAPi18n.__('operator-team'),
predicate_overdue: TAPi18n.__('predicate-overdue'),
predicate_archived: TAPi18n.__('predicate-archived'),
predicate_all: TAPi18n.__('predicate-all'),
predicate_ended: TAPi18n.__('predicate-ended'),
predicate_week: TAPi18n.__('predicate-week'),
predicate_month: TAPi18n.__('predicate-month'),
predicate_quarter: TAPi18n.__('predicate-quarter'),
predicate_year: TAPi18n.__('predicate-year'),
predicate_attachment: TAPi18n.__('predicate-attachment'),
predicate_description: TAPi18n.__('predicate-description'),
predicate_checklist: TAPi18n.__('predicate-checklist'),
predicate_public: TAPi18n.__('predicate-public'),
predicate_private: TAPi18n.__('predicate-private'),
predicate_due: TAPi18n.__('predicate-due'),
predicate_created: TAPi18n.__('predicate-created'),
predicate_modified: TAPi18n.__('predicate-modified'),
predicate_start: TAPi18n.__('predicate-start'),
predicate_end: TAPi18n.__('predicate-end'),
predicate_assignee: TAPi18n.__('predicate-assignee'),
predicate_member: TAPi18n.__('predicate-member'),
predicate_selector: TAPi18n.__('predicate-selector'),
predicate_projection: TAPi18n.__('predicate-projection'),
};
let text = '';
[
['# ', 'globalSearch-instructions-heading'],
['\n', 'globalSearch-instructions-description'],
['\n\n', 'globalSearch-instructions-operators'],
['\n- ', 'globalSearch-instructions-operator-board'],
['\n- ', 'globalSearch-instructions-operator-list'],
['\n- ', 'globalSearch-instructions-operator-swimlane'],
['\n- ', 'globalSearch-instructions-operator-comment'],
['\n- ', 'globalSearch-instructions-operator-label'],
['\n- ', 'globalSearch-instructions-operator-hash'],
['\n- ', 'globalSearch-instructions-operator-user'],
['\n- ', 'globalSearch-instructions-operator-at'],
['\n- ', 'globalSearch-instructions-operator-member'],
['\n- ', 'globalSearch-instructions-operator-assignee'],
['\n- ', 'globalSearch-instructions-operator-creator'],
['\n- ', 'globalSearch-instructions-operator-org'],
['\n- ', 'globalSearch-instructions-operator-team'],
['\n- ', 'globalSearch-instructions-operator-due'],
['\n- ', 'globalSearch-instructions-operator-created'],
['\n- ', 'globalSearch-instructions-operator-modified'],
['\n- ', 'globalSearch-instructions-operator-status'],
['\n - ', 'globalSearch-instructions-status-archived'],
['\n - ', 'globalSearch-instructions-status-public'],
['\n - ', 'globalSearch-instructions-status-private'],
['\n - ', 'globalSearch-instructions-status-all'],
['\n - ', 'globalSearch-instructions-status-ended'],
['\n- ', 'globalSearch-instructions-operator-has'],
['\n- ', 'globalSearch-instructions-operator-sort'],
['\n- ', 'globalSearch-instructions-operator-limit'],
['\n## ', 'heading-notes'],
['\n- ', 'globalSearch-instructions-notes-1'],
['\n- ', 'globalSearch-instructions-notes-2'],
['\n- ', 'globalSearch-instructions-notes-3'],
['\n- ', 'globalSearch-instructions-notes-3-2'],
['\n- ', 'globalSearch-instructions-notes-4'],
['\n- ', 'globalSearch-instructions-notes-5'],
].forEach(([prefix, instruction]) => {
text += `${prefix}${TAPi18n.__(instruction, tags)}`
// Replace *<text>* with `<text>` so markdown shows correctly
.replace(/\*\</, '`<')
.replace(/\>\*/, '\>\`')
});
return text;
}
let text = '';
[
['# ', 'globalSearch-instructions-heading'],
['\n', 'globalSearch-instructions-description'],
['\n\n', 'globalSearch-instructions-operators'],
['\n- ', 'globalSearch-instructions-operator-board'],
['\n- ', 'globalSearch-instructions-operator-list'],
['\n- ', 'globalSearch-instructions-operator-swimlane'],
['\n- ', 'globalSearch-instructions-operator-comment'],
['\n- ', 'globalSearch-instructions-operator-label'],
['\n- ', 'globalSearch-instructions-operator-hash'],
['\n- ', 'globalSearch-instructions-operator-user'],
['\n- ', 'globalSearch-instructions-operator-at'],
['\n- ', 'globalSearch-instructions-operator-member'],
['\n- ', 'globalSearch-instructions-operator-assignee'],
['\n- ', 'globalSearch-instructions-operator-creator'],
['\n- ', 'globalSearch-instructions-operator-org'],
['\n- ', 'globalSearch-instructions-operator-team'],
['\n- ', 'globalSearch-instructions-operator-due'],
['\n- ', 'globalSearch-instructions-operator-created'],
['\n- ', 'globalSearch-instructions-operator-modified'],
['\n- ', 'globalSearch-instructions-operator-status'],
['\n - ', 'globalSearch-instructions-status-archived'],
['\n - ', 'globalSearch-instructions-status-public'],
['\n - ', 'globalSearch-instructions-status-private'],
['\n - ', 'globalSearch-instructions-status-all'],
['\n - ', 'globalSearch-instructions-status-ended'],
['\n- ', 'globalSearch-instructions-operator-has'],
['\n- ', 'globalSearch-instructions-operator-sort'],
['\n- ', 'globalSearch-instructions-operator-limit'],
['\n## ', 'heading-notes'],
['\n- ', 'globalSearch-instructions-notes-1'],
['\n- ', 'globalSearch-instructions-notes-2'],
['\n- ', 'globalSearch-instructions-notes-3'],
['\n- ', 'globalSearch-instructions-notes-3-2'],
['\n- ', 'globalSearch-instructions-notes-4'],
['\n- ', 'globalSearch-instructions-notes-5'],
].forEach(([prefix, instruction]) => {
text += `${prefix}${TAPi18n.__(instruction, tags)}`
// Replace *<text>* with `<text>` so markdown shows correctly
.replace(/\*\</, '`<')
.replace(/\>\*/, '\>\`');
});
return text;
} catch (e) {
console.error('Error building searchInstructions:', e);
return '';
}
},
labelColors() {
return Boards.simpleSchema()._schema['labels.$.color'].allowedValues.map(
color => {
return { color, name: TAPi18n.__(`color-${color}`) };
},
try {
if (!LABEL_COLORS) return [];
return LABEL_COLORS.map(
color => {
return { color, name: TAPi18n.__(`color-${color}`) };
},
);
} catch (e) {
console.error('Error building labelColors:', e);
return [];
}
},
});
Template.globalSearch.events({
'click input.global-search-query-input'(evt) {
evt.preventDefault();
evt.stopPropagation();
},
'focus input.global-search-query-input'(evt) {
evt.preventDefault();
evt.stopPropagation();
},
'keyup input.global-search-query-input'(evt, tpl) {
evt.preventDefault();
evt.stopPropagation();
// Store the query value
tpl.search.query.set(evt.target.value);
},
'keydown input.global-search-query-input'(evt, tpl) {
// Allow normal key behavior but capture Enter key
if (evt.key === 'Enter') {
evt.preventDefault();
evt.stopPropagation();
searchAllBoards(tpl, evt.target.value);
}
},
'submit .js-search-query-form'(evt, tpl) {
evt.preventDefault();
searchAllBoards(tpl, evt.target.searchQuery.value);
},
'click .js-label-color'(evt, tpl) {
evt.preventDefault();
const input = document.getElementById('global-search-input');
tpl.search.query.set(
`${input.value} ${TAPi18n.__('operator-label')}:"${
evt.currentTarget.textContent
}"`,
);
}
document.getElementById('global-search-input').focus();
},
'click .js-board-title'(evt, tpl) {
evt.preventDefault();
const input = document.getElementById('global-search-input');
tpl.search.query.set(
`${input.value} ${TAPi18n.__('operator-board')}:"${
evt.currentTarget.textContent
}"`,
);
document.getElementById('global-search-input').focus();
},
'click .js-list-title'(evt, tpl) {
evt.preventDefault();
const input = document.getElementById('global-search-input');
tpl.search.query.set(
`${input.value} ${TAPi18n.__('operator-list')}:"${
evt.currentTarget.textContent
}"`,
);
document.getElementById('global-search-input').focus();
},
'click .js-label-name'(evt, tpl) {
evt.preventDefault();
const input = document.getElementById('global-search-input');
tpl.search.query.set(
`${input.value} ${TAPi18n.__('operator-label')}:"${
evt.currentTarget.textContent
}"`,
);
document.getElementById('global-search-input').focus();
},
'click .js-new-search'(evt, tpl) {
evt.preventDefault();
const input = document.getElementById('global-search-input');
input.value = '';
tpl.search.query.set('');
tpl.search.hasResults.set(false);
},
'click .js-next-page'(evt, tpl) {
evt.preventDefault();
tpl.search.nextPage();
},
'click .js-previous-page'(evt, tpl) {
evt.preventDefault();
tpl.search.previousPage();
},
});
events() {
return super.events().concat([
{
'submit .js-search-query-form'(evt) {
evt.preventDefault();
this.searchAllBoards(evt.target.searchQuery.value);
},
'click .js-label-color'(evt) {
evt.preventDefault();
const input = document.getElementById('global-search-input');
this.query.set(
`${input.value} ${TAPi18n.__('operator-label')}:"${
evt.currentTarget.textContent
}"`,
);
document.getElementById('global-search-input').focus();
},
'click .js-copy-debug-selector'(evt) {
/* Get the text field */
const selector = document.getElementById("debug-selector");
// resultsPaged template helpers - receives search data context from parent template.
Template.resultsPaged.helpers({
resultsHeading() {
const data = Template.currentData();
if (data && data.resultsHeading) {
const rv = data.resultsHeading;
return rv && typeof rv.get === 'function' ? rv.get() : rv;
}
return '';
},
results() {
const data = Template.currentData();
if (data && data.results) {
const rv = data.results;
return rv && typeof rv.get === 'function' ? rv.get() : rv;
}
return [];
},
getSearchHref() {
const data = Template.currentData();
if (data && typeof data.getSearchHref === 'function') {
return data.getSearchHref();
}
return '';
},
hasPreviousPage() {
const data = Template.currentData();
if (data && data.hasPreviousPage) {
const rv = data.hasPreviousPage;
return rv && typeof rv.get === 'function' ? rv.get() : rv;
}
return false;
},
hasNextPage() {
const data = Template.currentData();
if (data && data.hasNextPage) {
const rv = data.hasNextPage;
return rv && typeof rv.get === 'function' ? rv.get() : rv;
}
return false;
},
});
try {
navigator.clipboard.writeText(selector.textContent);
alert("Selector copied to clipboard");
} catch(err) {
alert("Error copying text: " + err);
}
},
'click .js-copy-debug-projection'(evt) {
/* Get the text field */
const projection = document.getElementById("debug-projection");
try {
navigator.clipboard.writeText(projection.textContent);
alert("Projection copied to clipboard");
} catch(err) {
alert("Error copying text: " + err);
}
},
'click .js-board-title'(evt) {
evt.preventDefault();
const input = document.getElementById('global-search-input');
this.query.set(
`${input.value} ${TAPi18n.__('operator-board')}:"${
evt.currentTarget.textContent
}"`,
);
document.getElementById('global-search-input').focus();
},
'click .js-list-title'(evt) {
evt.preventDefault();
const input = document.getElementById('global-search-input');
this.query.set(
`${input.value} ${TAPi18n.__('operator-list')}:"${
evt.currentTarget.textContent
}"`,
);
document.getElementById('global-search-input').focus();
},
'click .js-label-name'(evt) {
evt.preventDefault();
const input = document.getElementById('global-search-input');
this.query.set(
`${input.value} ${TAPi18n.__('operator-label')}:"${
evt.currentTarget.textContent
}"`,
);
document.getElementById('global-search-input').focus();
},
'click .js-new-search'(evt) {
evt.preventDefault();
const input = document.getElementById('global-search-input');
input.value = '';
this.query.set('');
this.hasResults.set(false);
},
},
]);
}
}
GlobalSearchComponent.register('globalSearch');
Template.resultsPaged.events({
'click .js-next-page'(evt) {
evt.preventDefault();
// Walk up to find the search instance
let view = Template.instance().view;
while (view) {
const tplInst = view.templateInstance && view.templateInstance();
if (tplInst && tplInst.search) {
tplInst.search.nextPage();
return;
}
view = view.parentView;
}
},
'click .js-previous-page'(evt) {
evt.preventDefault();
let view = Template.instance().view;
while (view) {
const tplInst = view.templateInstance && view.templateInstance();
if (tplInst && tplInst.search) {
tplInst.search.previousPage();
return;
}
view = view.parentView;
}
},
});
+6 -2
View File
@@ -126,5 +126,9 @@ template(name="offlineWarning")
p
i.fa.fa-warning
| {{_ 'app-is-offline'}}
a.app-try-reconnect {{_ 'app-try-reconnect'}}
if appOfflineMessage
br
span= appOfflineMessage
if canReconnectNow
br
a.app-try-reconnect {{_ 'app-try-reconnect'}}
+26 -2
View File
@@ -1,5 +1,9 @@
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { ReactiveCache } from '/imports/reactiveCache';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import Announcements from '/models/announcements';
import { Utils } from '/client/lib/utils';
Meteor.subscribe('user-admin');
Meteor.subscribe('boards');
@@ -43,7 +47,27 @@ Template.header.helpers({
},
appIsOffline() {
return !Meteor.status().connected;
const status = Meteor.status();
return ['waiting', 'failed', 'offline'].includes(status.status);
},
appOfflineMessage() {
const status = Meteor.status();
if (status.status === 'failed' && status.reason) {
return status.reason;
}
if (status.status === 'offline') {
return 'Connection is paused.';
}
return null;
},
canReconnectNow() {
const status = Meteor.status();
return ['waiting', 'failed', 'offline'].includes(status.status);
},
hasAnnouncement() {
@@ -132,7 +156,7 @@ Template.header.events({
Session.set('currentCard', null);
},
'click .js-toggle-desktop-drag-handles'() {
currentUser = Meteor.user();
const currentUser = Meteor.user();
if (currentUser) {
Meteor.call('toggleDesktopDragHandles');
} else if (window.localStorage.getItem('showDesktopDragHandles')) {
+2
View File
@@ -1,6 +1,8 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import Users from '/models/users';
import { EscapeActions } from '/client/lib/escapeActions';
let alreadyCheck = 1;
let isCheckDone = false;
+67 -61
View File
@@ -1,6 +1,7 @@
import { CardSearchPagedComponent } from '../../lib/cardSearch';
import { CardSearchPaged } from '../../lib/cardSearch';
import { Utils } from '/client/lib/utils';
BlazeComponent.extendComponent({
Template.myCardsHeaderBar.helpers({
myCardsSort() {
// eslint-disable-next-line no-console
// console.log('sort:', Utils.myCardsSort());
@@ -12,86 +13,69 @@ BlazeComponent.extendComponent({
// console.log('sort:', Utils.myCardsView());
return Utils.myCardsView();
},
});
events() {
return [
{
'click .js-toggle-my-cards-choose-sort': Popup.open(
'myCardsSortChange',
),
'click .js-my-cards-view-change': Popup.open(
'myCardsViewChange'),
},
];
},
}).register('myCardsHeaderBar');
Template.myCardsHeaderBar.events({
'click .js-toggle-my-cards-choose-sort': Popup.open(
'myCardsSortChange',
),
'click .js-my-cards-view-change': Popup.open(
'myCardsViewChange'),
});
Template.myCards.onCreated(function () {
const search = new CardSearchPaged(this);
this.search = search;
// Override getSubscription for myCards
search.getSubscription = function (queryParams) {
return Meteor.subscribe(
'myCards',
search.sessionId,
search.subscriptionCallbacks,
);
};
search.runGlobalSearch(null);
Meteor.subscribe('setting');
});
Template.myCards.helpers({
userId() {
return Meteor.userId();
},
});
BlazeComponent.extendComponent({
events() {
return [
{
'click .js-my-cards-view-boards'() {
Utils.setMyCardsView('boards');
Popup.back();
},
'click .js-my-cards-view-table'() {
Utils.setMyCardsView('table');
Popup.back();
},
},
];
// Return ReactiveVar so jade can use .get pattern
searching() {
return Template.instance().search.searching;
},
}).register('myCardsViewChangePopup');
class MyCardsComponent extends CardSearchPagedComponent {
onCreated() {
super.onCreated();
this.runGlobalSearch(null);
Meteor.subscribe('setting');
}
// eslint-disable-next-line no-unused-vars
getSubscription(queryParams) {
return Meteor.subscribe(
'myCards',
this.sessionId,
this.subscriptionCallbacks,
);
}
myCardsView() {
// eslint-disable-next-line no-console
//console.log('sort:', Utils.myCardsView());
return Utils.myCardsView();
}
},
labelName(board, labelId) {
const label = board.getLabelById(labelId)
const name = label.name
return name
}
const label = board.getLabelById(labelId);
const name = label.name;
return name;
},
labelColor(board, labelId) {
const label = board.getLabelById(labelId)
const color = label.color
return color
}
const label = board.getLabelById(labelId);
const color = label.color;
return color;
},
myCardsList() {
const search = Template.instance().search;
const boards = [];
let board = null;
let swimlane = null;
let list = null;
const cursor = this.getResults();
const cursor = search.getResults();
if (cursor) {
cursor.forEach(card => {
@@ -177,6 +161,28 @@ class MyCardsComponent extends CardSearchPagedComponent {
}
return [];
}
}
MyCardsComponent.register('myCards');
},
});
Template.myCards.events({
'click .js-next-page'(evt, tpl) {
evt.preventDefault();
tpl.search.nextPage();
},
'click .js-previous-page'(evt, tpl) {
evt.preventDefault();
tpl.search.previousPage();
},
});
Template.myCardsViewChangePopup.events({
'click .js-my-cards-view-boards'() {
Utils.setMyCardsView('boards');
Popup.back();
},
'click .js-my-cards-view-table'() {
Utils.setMyCardsView('table');
Popup.back();
},
});
+154 -120
View File
@@ -3,7 +3,7 @@
border-radius: 0.4vw;
border: 1px solid #dbdbdb;
border-bottom-color: #c2c2c2;
box-shadow: 0 0.2vh 0.8vh rgba(0,0,0,0.3);
box-shadow: 0 0.2vh 0.8vh rgba(0, 0, 0, 0.3);
position: absolute;
/* Wider default to fit full color palette */
width: min(380px, 55vw);
@@ -15,10 +15,10 @@
}
.pop-over p,
.pop-over textarea,
.pop-over input[type="text"],
.pop-over input[type="email"],
.pop-over input[type="password"],
.pop-over input[type="file"] {
.pop-over input[type='text'],
.pop-over input[type='email'],
.pop-over input[type='password'],
.pop-over input[type='file'] {
width: 100%;
}
.pop-over select {
@@ -79,8 +79,22 @@
}
/* Allow dynamic max-height to override default constraint */
.pop-over[style*="max-height"] .content-wrapper {
max-height: inherit;
.pop-over[style*='max-height'] {
overflow: hidden;
display: flex;
flex-direction: column;
}
.pop-over[style*='max-height'] .header {
flex: 0 0 auto;
}
.pop-over[style*='max-height'] .content-wrapper {
flex: 1 1 auto;
min-height: 0;
max-height: none;
overflow-y: auto;
overflow-x: hidden;
}
.pop-over .content-container {
width: 100%;
@@ -89,17 +103,17 @@
}
/* Allow dynamic max-height to override default constraint for content-container */
.pop-over[style*="max-height"] .content-container {
max-height: inherit;
.pop-over[style*='max-height'] .content-container {
max-height: none;
}
/* Fix overflow in the Member Settings (member menu) popup:
the popup itself gets a max-height inline style, but the header consumes space.
Make the header overlay the scrollable area so the list can't spill out. */
.pop-over[data-popup="memberMenuPopup"] {
.pop-over[data-popup='memberMenuPopup'] {
overflow: hidden;
}
.pop-over[data-popup="memberMenuPopup"] > .header {
.pop-over[data-popup='memberMenuPopup'] > .header {
position: absolute;
top: 0;
left: 0;
@@ -107,30 +121,30 @@
margin-bottom: 0;
z-index: 1;
}
.pop-over[data-popup="memberMenuPopup"] > .content-wrapper {
.pop-over[data-popup='memberMenuPopup'] > .content-wrapper {
padding-top: calc(4.5vh + 1vh);
box-sizing: border-box;
}
/* Admin edit popups: use full height */
.pop-over[data-popup="editUserPopup"],
.pop-over[data-popup="editOrgPopup"],
.pop-over[data-popup="editTeamPopup"] {
.pop-over[data-popup='editUserPopup'],
.pop-over[data-popup='editOrgPopup'],
.pop-over[data-popup='editTeamPopup'] {
height: calc(100vh - 20px) !important;
max-height: calc(100vh - 20px) !important;
}
.pop-over[data-popup="editUserPopup"] .content-wrapper,
.pop-over[data-popup="editOrgPopup"] .content-wrapper,
.pop-over[data-popup="editTeamPopup"] .content-wrapper {
.pop-over[data-popup='editUserPopup'] .content-wrapper,
.pop-over[data-popup='editOrgPopup'] .content-wrapper,
.pop-over[data-popup='editTeamPopup'] .content-wrapper {
max-height: calc(100vh - 80px) !important; /* Subtract header height */
height: calc(100vh - 80px) !important;
overflow-y: auto !important;
}
.pop-over[data-popup="editUserPopup"] .content-container,
.pop-over[data-popup="editOrgPopup"] .content-container,
.pop-over[data-popup="editTeamPopup"] .content-container {
.pop-over[data-popup='editUserPopup'] .content-container,
.pop-over[data-popup='editOrgPopup'] .content-container,
.pop-over[data-popup='editTeamPopup'] .content-container {
max-height: calc(100vh - 80px) !important; /* Subtract header height */
height: calc(100vh - 80px) !important;
}
@@ -142,7 +156,7 @@
}
/* Specific styling for language popup list */
.pop-over[data-popup="changeLanguagePopup"] .pop-over-list {
.pop-over[data-popup='changeLanguagePopup'] .pop-over-list {
max-height: none;
overflow: visible;
height: auto;
@@ -150,7 +164,7 @@
}
/* Ensure content div in language popup contains all items */
.pop-over[data-popup="changeLanguagePopup"] .content {
.pop-over[data-popup='changeLanguagePopup'] .content {
height: auto;
/* Remove forced min-height to avoid top gap */
display: flex;
@@ -158,7 +172,7 @@
}
/* Ensure hidden stack pages truly take no space */
.pop-over[data-popup="changeLanguagePopup"] .content.no-height {
.pop-over[data-popup='changeLanguagePopup'] .content.no-height {
min-height: 0 !important;
height: 0 !important;
padding: 0 !important;
@@ -167,7 +181,7 @@
}
/* Make language popup extend to bottom of browser window */
.pop-over[data-popup="changeLanguagePopup"] {
.pop-over[data-popup='changeLanguagePopup'] {
position: fixed !important;
bottom: 0 !important;
top: auto !important;
@@ -184,12 +198,12 @@
}
/* Allow dynamic height for Change Language popup */
.pop-over[data-popup="changeLanguagePopup"] .header {
.pop-over[data-popup='changeLanguagePopup'] .header {
flex-shrink: 0 !important;
height: auto !important;
}
.pop-over[data-popup="changeLanguagePopup"] .content-wrapper {
.pop-over[data-popup='changeLanguagePopup'] .content-wrapper {
flex: 1 !important;
overflow-y: auto !important;
overflow-x: hidden !important;
@@ -199,7 +213,7 @@
width: 100% !important;
}
.pop-over[data-popup="changeLanguagePopup"] .content-container {
.pop-over[data-popup='changeLanguagePopup'] .content-container {
height: auto !important;
max-height: none !important;
flex: 1 !important;
@@ -208,7 +222,7 @@
width: 100% !important;
}
.pop-over[data-popup="changeLanguagePopup"] .content {
.pop-over[data-popup='changeLanguagePopup'] .content {
height: auto !important;
max-height: none !important;
padding-bottom: 50px !important;
@@ -216,59 +230,59 @@
}
/* 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"] {
.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 {
.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 {
.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 {
.pop-over[data-popup*='Date'] .datepicker-container {
width: 100% !important;
padding: 15px !important;
}
.pop-over[data-popup*="Date"] .datepicker-container .fields {
.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 {
.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 {
.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"] {
.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;
@@ -277,71 +291,109 @@
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 {
.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 {
.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 {
/* Workspace Settings popup: ensure modal doesn't overflow viewport */
.pop-over[data-popup='workspaceActionsPopup'] {
max-height: 80vh !important;
width: min(420px, 85vw) !important;
}
.pop-over[data-popup='workspaceActionsPopup'] .content-wrapper {
max-height: calc(80vh - 70px) !important; /* Subtract header height */
overflow-y: auto !important;
}
.pop-over[data-popup='workspaceActionsPopup'] .content-container {
max-height: calc(80vh - 70px) !important;
}
.pop-over[data-popup='workspaceActionsPopup'] .content {
max-height: none !important; /* Allow content to scroll within container */
overflow: visible !important; /* Overflow handled by parent */
}
/* Card Details Actions popup: ensure menu doesn't overflow viewport */
.pop-over[data-popup="cardDetailsActionsPopup"] {
max-height: 80vh !important;
width: min(380px, 85vw) !important;
}
.pop-over[data-popup="cardDetailsActionsPopup"] .content-wrapper {
max-height: calc(80vh - 70px) !important; /* Subtract header height */
overflow-y: auto !important;
}
.pop-over[data-popup="cardDetailsActionsPopup"] .content-container {
max-height: calc(80vh - 70px) !important;
}
.pop-over[data-popup="cardDetailsActionsPopup"] .content {
max-height: none !important; /* Allow content to scroll within container */
overflow: visible !important; /* Overflow handled by parent */
}
.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 {
.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 {
.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 {
.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 {
.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;
@@ -350,11 +402,10 @@
.pop-over .content-container .content {
/* Match wider popover, leave padding */
width: 100%;
padding: 0 1.3vw 1.3vh;
padding: 1.3vh 1.3vw 1.3vh;
box-sizing: border-box;
/* Ensure content is not shifted left */
margin-left: 0 !important;
transform: none !important;
}
/* Utility: remove left gutter inside specific popups */
@@ -388,30 +439,13 @@
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 */
/* Fix popup depth containers - don't block transforms which are needed for popup navigation */
.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;
}
@@ -457,22 +491,22 @@
margin: 40px auto;
}
.pop-over .popup-container-depth-1 {
transform: translateX(-300px);
transform: none;
}
.pop-over .popup-container-depth-2 {
transform: translateX(-600px);
transform: none;
}
.pop-over .popup-container-depth-3 {
transform: translateX(-900px);
transform: none;
}
.pop-over .popup-container-depth-4 {
transform: translateX(-1200px);
transform: none;
}
.pop-over .popup-container-depth-5 {
transform: translateX(-1500px);
transform: none;
}
.pop-over .popup-container-depth-6 {
transform: translateX(-1800px);
transform: none;
}
.select-members-list,
.select-avatars-list {
@@ -493,15 +527,15 @@
text-decoration: none;
overflow: hidden;
line-height: 33px;
display:flex;
/* flex-wrap:wrap;*/
gap:5px;
display: flex;
/* flex-wrap:wrap;*/
gap: 5px;
align-items: center;
color: #000 !important;
}
.pop-over-list li > a > .member{
.pop-over-list li > a > .member {
align-self: flex-start;
flex:0 0 auto;
flex: 0 0 auto;
}
.pop-over-list li > a .item-name {
display: block;
@@ -673,10 +707,10 @@ body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
}
.pop-over .content-container .content p,
.pop-over .content-container .content textarea,
.pop-over .content-container .content input[type="text"],
.pop-over .content-container .content input[type="email"],
.pop-over .content-container .content input[type="password"],
.pop-over .content-container .content input[type="file"] {
.pop-over .content-container .content input[type='text'],
.pop-over .content-container .content input[type='email'],
.pop-over .content-container .content input[type='password'],
.pop-over .content-container .content input[type='file'] {
width: 100%;
box-sizing: border-box;
}
+6 -1
View File
@@ -1,3 +1,5 @@
import { CSSEvents } from '/client/lib/cssEvents';
Popup.template.events({
'click .js-back-view'() {
Popup.back();
@@ -26,8 +28,11 @@ Popup.template.events({
// When a popup content is removed (ie, when the user press the "back" button),
// we need to wait for the container translation to end before removing the
// actual DOM element. For that purpose we use the undocumented `_uihooks` API.
Popup.template.onRendered(() => {
Popup.template.onRendered(function () {
const container = this.find('.content-container');
if (!container) {
return;
}
container._uihooks = {
removeElement(node) {
$(node).addClass('no-height');
+10 -8
View File
@@ -1,11 +1,13 @@
import { Spinner } from '/client/lib/spinner';
import { getSpinnerTemplate } from '/client/lib/spinner';
(class extends Spinner {
}.register('spinner'));
Template.spinner.helpers({
getSpinnerTemplate() {
return getSpinnerTemplate();
},
});
(class extends Spinner {
Template.spinnerRaw.helpers({
getSpinnerTemplateRaw() {
let ret = super.getSpinnerTemplate() + 'Raw';
return ret;
}
}.register('spinnerRaw'));
return getSpinnerTemplate() + 'Raw';
},
});
+12 -14
View File
@@ -22,20 +22,18 @@ const supportHelpers = {
};
// Main support page component
BlazeComponent.extendComponent({
onCreated() {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
Template.support.onCreated(function () {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
Meteor.subscribe('setting');
},
...supportHelpers
}).register('support');
Meteor.subscribe('setting');
});
Template.support.helpers(supportHelpers);
// Header bar component
BlazeComponent.extendComponent({
onCreated() {
Meteor.subscribe('setting');
},
...supportHelpers
}).register('supportHeaderBar');
Template.supportHeaderBar.onCreated(function () {
Meteor.subscribe('setting');
});
Template.supportHeaderBar.helpers(supportHelpers);
@@ -1,34 +0,0 @@
const peakAnticipation = 200;
Mixins.InfiniteScrolling = BlazeComponent.extendComponent({
onCreated() {
this._nextPeak = Infinity;
},
setNextPeak(v) {
this._nextPeak = v;
},
getNextPeak() {
return this._nextPeak;
},
resetNextPeak() {
this._nextPeak = Infinity;
},
events() {
return [
{
scroll(evt) {
const domElement = evt.currentTarget;
let altitude = domElement.scrollTop + domElement.offsetHeight;
altitude += peakAnticipation;
if (altitude >= this.callFirstWith(null, 'getNextPeak')) {
this.mixinParent().callFirstWith(null, 'reachNextPeak');
}
},
},
];
},
});
@@ -1,13 +1,14 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
import Users from '/models/users';
Template.notification.events({
'click .read-status .materialCheckBox'() {
async 'click .read-status .materialCheckBox'() {
const update = {};
const newReadValue = this.read ? null : Date.now();
const newReadValue = this.read ? null : new Date();
update[`profile.notifications.${this.index}.read`] = newReadValue;
Users.update(Meteor.userId(), { $set: update }, (error, result) => {
await Users.updateAsync(Meteor.userId(), { $set: update }).catch((error) => {
if (error) {
console.error('Error updating notification:', error);
}
@@ -15,7 +15,7 @@ Template.body.events({
Template.notifications.helpers({
unreadNotifications() {
const notifications = ReactiveCache.getCurrentUser().notifications();
const unreadNotifications = _.filter(notifications, v => !v.read);
const unreadNotifications = notifications.filter(v => !v.read);
return unreadNotifications.length;
},
});
@@ -1,5 +1,7 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { toggleNotificationsDrawer } from './notifications.js';
import Users from '/models/users';
Template.notificationsDrawer.onCreated(function() {
Meteor.subscribe('notificationActivities');
@@ -24,7 +26,7 @@ Template.notificationsDrawer.helpers({
readNotifications() {
const user = ReactiveCache.getCurrentUser();
const list = user ? user.notifications() : [];
const readNotifications = _.filter(list, v => !!v.read);
const readNotifications = list.filter(v => !!v.read);
return readNotifications.length;
},
});
@@ -34,7 +36,7 @@ Template.notificationsDrawer.events({
event.stopPropagation();
Session.set('showNotificationMenu', !Session.get('showNotificationMenu'));
},
'click .notification-menu .menu-item'(event) {
async 'click .notification-menu .menu-item'(event) {
const target = event.currentTarget;
if (target.classList.contains('mark-all-read')) {
@@ -42,8 +44,10 @@ Template.notificationsDrawer.events({
for (const index in notifications) {
if (notifications.hasOwnProperty(index) && !notifications[index].read) {
const update = {};
update[`profile.notifications.${index}.read`] = Date.now();
Users.update(Meteor.userId(), { $set: update });
update[`profile.notifications.${index}.read`] = new Date();
await Users.updateAsync(Meteor.userId(), { $set: update }).catch((error) => {
console.error('Error marking notification as read:', error);
});
}
}
Session.set('showNotificationMenu', false);
@@ -53,7 +57,9 @@ Template.notificationsDrawer.events({
if (notifications.hasOwnProperty(index) && notifications[index].read) {
const update = {};
update[`profile.notifications.${index}.read`] = null;
Users.update(Meteor.userId(), { $set: update });
await Users.updateAsync(Meteor.userId(), { $set: update }).catch((error) => {
console.error('Error marking notification as unread:', error);
});
}
}
Session.set('showNotificationMenu', false);
+197 -192
View File
@@ -1,12 +1,15 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import Actions from '/models/actions';
import Rules from '/models/rules';
import Triggers from '/models/triggers';
import { Utils } from '/client/lib/utils';
BlazeComponent.extendComponent({
onCreated() {
// Ensure boards are available for action dropdowns
this.subscribe('boards');
},
Template.boardActions.onCreated(function () {
this.subscribe('boards');
});
Template.boardActions.helpers({
boards() {
const ret = ReactiveCache.getBoards(
{
@@ -32,193 +35,195 @@ BlazeComponent.extendComponent({
}
return 'Loading boards...';
},
});
events() {
return [
{
'click .js-create-card-action'(event) {
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const cardName = this.find('#card-name').value;
const listName = this.find('#list-name').value;
const swimlaneName = this.find('#swimlane-name2').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, this);
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'createCard',
swimlaneName,
cardName,
listName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
},
'click .js-add-swimlane-action'(event) {
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const swimlaneName = this.find('#swimlane-name').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, this);
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'addSwimlane',
swimlaneName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
},
'click .js-add-spec-move-action'(event) {
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const actionSelected = this.find('#move-spec-action').value;
const swimlaneName = this.find('#swimlaneName').value || '*';
const listName = this.find('#listName').value || '*';
const boardId = Session.get('currentBoard');
const destBoardId = this.find('#board-id').value;
const desc = Utils.getTriggerActionDesc(event, this);
if (actionSelected === 'top') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'moveCardToTop',
listName,
swimlaneName,
boardId: destBoardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
if (actionSelected === 'bottom') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'moveCardToBottom',
listName,
swimlaneName,
boardId: destBoardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
'click .js-add-gen-move-action'(event) {
const desc = Utils.getTriggerActionDesc(event, this);
const boardId = Session.get('currentBoard');
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const actionSelected = this.find('#move-gen-action').value;
if (actionSelected === 'top') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'moveCardToTop',
listTitle: '*',
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
if (actionSelected === 'bottom') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'moveCardToBottom',
listTitle: '*',
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
'click .js-add-arch-action'(event) {
const desc = Utils.getTriggerActionDesc(event, this);
const boardId = Session.get('currentBoard');
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const actionSelected = this.find('#arch-action').value;
if (actionSelected === 'archive') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'archive',
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
if (actionSelected === 'unarchive') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'unarchive',
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
'click .js-link-card-action'(event) {
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const swimlaneName = this.find('#swimlaneName-link').value || '*';
const listName = this.find('#listName-link').value || '*';
const boardId = Session.get('currentBoard');
const destBoardId = this.find('#board-id-link').value;
const desc = Utils.getTriggerActionDesc(event, this);
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'linkCard',
listName,
swimlaneName,
boardId: destBoardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
},
},
];
Template.boardActions.events({
'click .js-create-card-action'(event, tpl) {
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const cardName = tpl.find('#card-name').value;
const listName = tpl.find('#list-name').value;
const swimlaneName = tpl.find('#swimlane-name2').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, tpl);
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'createCard',
swimlaneName,
cardName,
listName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
},
}).register('boardActions');
'click .js-add-swimlane-action'(event, tpl) {
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const swimlaneName = tpl.find('#swimlane-name').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, tpl);
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'addSwimlane',
swimlaneName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
},
'click .js-add-spec-move-action'(event, tpl) {
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const actionSelected = tpl.find('#move-spec-action').value;
const swimlaneName = tpl.find('#swimlaneName').value || '*';
const listName = tpl.find('#listName').value || '*';
const boardId = Session.get('currentBoard');
const destBoardId = tpl.find('#board-id').value;
const desc = Utils.getTriggerActionDesc(event, tpl);
if (actionSelected === 'top') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'moveCardToTop',
listName,
swimlaneName,
boardId: destBoardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
if (actionSelected === 'bottom') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'moveCardToBottom',
listName,
swimlaneName,
boardId: destBoardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
'click .js-add-gen-move-action'(event, tpl) {
const desc = Utils.getTriggerActionDesc(event, tpl);
const boardId = Session.get('currentBoard');
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const actionSelected = tpl.find('#move-gen-action').value;
if (actionSelected === 'top') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'moveCardToTop',
listTitle: '*',
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
if (actionSelected === 'bottom') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'moveCardToBottom',
listTitle: '*',
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
'click .js-add-arch-action'(event, tpl) {
const desc = Utils.getTriggerActionDesc(event, tpl);
const boardId = Session.get('currentBoard');
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const actionSelected = tpl.find('#arch-action').value;
if (actionSelected === 'archive') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'archive',
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
if (actionSelected === 'unarchive') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'unarchive',
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
'click .js-link-card-action'(event, tpl) {
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const swimlaneName = tpl.find('#swimlaneName-link').value || '*';
const listName = tpl.find('#listName-link').value || '*';
const boardId = Session.get('currentBoard');
const destBoardId = tpl.find('#board-id-link').value;
const desc = Utils.getTriggerActionDesc(event, tpl);
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'linkCard',
listName,
swimlaneName,
boardId: destBoardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
},
});
/* eslint-no-undef */
+217 -207
View File
@@ -1,20 +1,31 @@
import Actions from '/models/actions';
import { CARD_COLORS } from '/models/metadata/colors';
import Rules from '/models/rules';
import Triggers from '/models/triggers';
import { Utils } from '/client/lib/utils';
let cardColors;
Meteor.startup(() => {
cardColors = Cards.simpleSchema()._schema.color.allowedValues;
cardColors = CARD_COLORS;
});
BlazeComponent.extendComponent({
onCreated() {
this.subscribe('allRules');
this.cardColorButtonValue = new ReactiveVar('green');
},
// Module-level shared state so the color popup can read/write the
// cardColorButtonValue without relying on BlazeComponent.getOpenerComponent().
let sharedCardColorButtonValue;
Template.cardActions.onCreated(function () {
this.subscribe('allRules');
this.cardColorButtonValue = new ReactiveVar('green');
sharedCardColorButtonValue = this.cardColorButtonValue;
});
Template.cardActions.helpers({
cardColorButton() {
return this.cardColorButtonValue.get();
return Template.instance().cardColorButtonValue.get();
},
cardColorButtonText() {
return `color-${this.cardColorButtonValue.get()}`;
return `color-${Template.instance().cardColorButtonValue.get()}`;
},
labels() {
@@ -26,215 +37,214 @@ BlazeComponent.extendComponent({
}
return labels;
},
});
events() {
return [
{
'click .js-set-date-action'(event) {
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const triggerId = Triggers.insert(trigger);
const actionSelected = this.find('#setdate-action').value;
const dateFieldSelected = this.find('#setdate-datefield').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, this);
Template.cardActions.events({
'click .js-set-date-action'(event, tpl) {
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const triggerId = Triggers.insert(trigger);
const actionSelected = tpl.find('#setdate-action').value;
const dateFieldSelected = tpl.find('#setdate-datefield').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, tpl);
const actionId = Actions.insert({
actionType: actionSelected,
dateField: dateFieldSelected,
boardId,
desc,
});
const actionId = Actions.insert({
actionType: actionSelected,
dateField: dateFieldSelected,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
desc,
});
},
'click .js-remove-datevalue-action'(event) {
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const triggerId = Triggers.insert(trigger);
const dateFieldSelected = this.find('#setdate-removedatefieldvalue')
.value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, this);
const actionId = Actions.insert({
actionType: 'removeDate',
dateField: dateFieldSelected,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
desc,
});
},
'click .js-add-label-action'(event) {
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const actionSelected = this.find('#label-action').value;
const labelId = this.find('#label-id').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, this);
if (actionSelected === 'add') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'addLabel',
labelId,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
if (actionSelected === 'remove') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'removeLabel',
labelId,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
'click .js-add-member-action'(event) {
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const actionSelected = this.find('#member-action').value;
const username = this.find('#member-name').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, this);
if (actionSelected === 'add') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'addMember',
username,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
desc,
});
}
if (actionSelected === 'remove') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'removeMember',
username,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
'click .js-add-removeall-action'(event) {
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const triggerId = Triggers.insert(trigger);
const desc = Utils.getTriggerActionDesc(event, this);
const boardId = Session.get('currentBoard');
const actionId = Actions.insert({
actionType: 'removeMember',
// deepcode ignore NoHardcodedCredentials: it's no credential
username: '*',
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
},
'click .js-show-color-palette'(event) {
const funct = Popup.open('setCardActionsColor');
const colorButton = this.find('#color-action');
if (colorButton.value === '') {
colorButton.value = 'green';
}
funct.call(this, event);
},
'click .js-set-color-action'(event) {
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const selectedColor = this.cardColorButtonValue.get();
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, this);
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'setColor',
selectedColor,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
},
},
];
},
}).register('cardActions');
BlazeComponent.extendComponent({
onCreated() {
this.currentAction = this.currentData();
this.colorButtonValue = Popup.getOpenerComponent().cardColorButtonValue;
this.currentColor = new ReactiveVar(this.colorButtonValue.get());
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
desc,
});
},
'click .js-remove-datevalue-action'(event, tpl) {
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const triggerId = Triggers.insert(trigger);
const dateFieldSelected = tpl.find('#setdate-removedatefieldvalue')
.value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, tpl);
const actionId = Actions.insert({
actionType: 'removeDate',
dateField: dateFieldSelected,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
desc,
});
},
'click .js-add-label-action'(event, tpl) {
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const actionSelected = tpl.find('#label-action').value;
const labelId = tpl.find('#label-id').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, tpl);
if (actionSelected === 'add') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'addLabel',
labelId,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
if (actionSelected === 'remove') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'removeLabel',
labelId,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
'click .js-add-member-action'(event, tpl) {
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const actionSelected = tpl.find('#member-action').value;
const username = tpl.find('#member-name').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, tpl);
if (actionSelected === 'add') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'addMember',
username,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
desc,
});
}
if (actionSelected === 'remove') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'removeMember',
username,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
'click .js-add-removeall-action'(event, tpl) {
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const triggerId = Triggers.insert(trigger);
const desc = Utils.getTriggerActionDesc(event, tpl);
const boardId = Session.get('currentBoard');
const actionId = Actions.insert({
actionType: 'removeMember',
// deepcode ignore NoHardcodedCredentials: it's no credential
username: '*',
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
},
'click .js-show-color-palette'(event, tpl) {
const funct = Popup.open('setCardActionsColor');
const colorButton = tpl.find('#color-action');
if (colorButton.value === '') {
colorButton.value = 'green';
}
funct.call(this, event);
},
'click .js-set-color-action'(event, tpl) {
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const selectedColor = tpl.cardColorButtonValue.get();
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, tpl);
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'setColor',
selectedColor,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
},
});
Template.setCardActionsColorPopup.onCreated(function () {
this.currentColor = new ReactiveVar(
sharedCardColorButtonValue.get()
);
this.colorButtonValue = sharedCardColorButtonValue;
});
Template.setCardActionsColorPopup.helpers({
colors() {
return cardColors.map(color => ({ color, name: '' }));
},
isSelected(color) {
return this.currentColor.get() === color;
return Template.instance().currentColor.get() === color;
},
});
events() {
return [
{
'click .js-palette-color'() {
this.currentColor.set(this.currentData().color);
},
'click .js-submit'() {
this.colorButtonValue.set(this.currentColor.get());
Popup.back();
},
},
];
Template.setCardActionsColorPopup.events({
'click .js-palette-color'(event, tpl) {
tpl.currentColor.set(Template.currentData().color);
},
}).register('setCardActionsColorPopup');
'click .js-submit'(event, tpl) {
tpl.colorButtonValue.set(tpl.currentColor.get());
Popup.back();
},
});
@@ -1,150 +1,154 @@
BlazeComponent.extendComponent({
onCreated() {
this.subscribe('allRules');
import Actions from '/models/actions';
import Rules from '/models/rules';
import Triggers from '/models/triggers';
import { Utils } from '/client/lib/utils';
Template.checklistActions.onCreated(function () {
this.subscribe('allRules');
});
Template.checklistActions.events({
'click .js-add-checklist-items-action'(event, tpl) {
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const checklistName = tpl.find('#checklist-name-3').value;
const checklistItems = tpl.find('#checklist-items').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, tpl);
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'addChecklistWithItems',
checklistName,
checklistItems,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
},
events() {
return [
{
'click .js-add-checklist-items-action'(event) {
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const checklistName = this.find('#checklist-name-3').value;
const checklistItems = this.find('#checklist-items').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, this);
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'addChecklistWithItems',
checklistName,
checklistItems,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
},
'click .js-add-checklist-action'(event) {
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const actionSelected = this.find('#check-action').value;
const checklistName = this.find('#checklist-name').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, this);
if (actionSelected === 'add') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'addChecklist',
checklistName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
if (actionSelected === 'remove') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'removeChecklist',
checklistName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
'click .js-add-checkall-action'(event) {
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const actionSelected = this.find('#checkall-action').value;
const checklistName = this.find('#checklist-name2').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, this);
if (actionSelected === 'check') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'checkAll',
checklistName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
if (actionSelected === 'uncheck') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'uncheckAll',
checklistName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
'click .js-add-check-item-action'(event) {
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const checkItemName = this.find('#checkitem-name');
const checklistName = this.find('#checklist-name3');
const actionSelected = this.find('#check-item-action').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, this);
if (actionSelected === 'check') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'checkItem',
checklistName,
checkItemName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
if (actionSelected === 'uncheck') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'uncheckItem',
checklistName,
checkItemName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
},
];
'click .js-add-checklist-action'(event, tpl) {
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const actionSelected = tpl.find('#check-action').value;
const checklistName = tpl.find('#checklist-name').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, tpl);
if (actionSelected === 'add') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'addChecklist',
checklistName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
if (actionSelected === 'remove') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'removeChecklist',
checklistName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
}).register('checklistActions');
'click .js-add-checkall-action'(event, tpl) {
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const actionSelected = tpl.find('#checkall-action').value;
const checklistName = tpl.find('#checklist-name2').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, tpl);
if (actionSelected === 'check') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'checkAll',
checklistName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
if (actionSelected === 'uncheck') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'uncheckAll',
checklistName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
'click .js-add-check-item-action'(event, tpl) {
const data = Template.currentData();
const ruleName = data.ruleName.get();
const trigger = data.triggerVar.get();
const checkItemName = tpl.find('#checkitem-name');
const checklistName = tpl.find('#checklist-name3');
const actionSelected = tpl.find('#check-item-action').value;
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, tpl);
if (actionSelected === 'check') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'checkItem',
checklistName,
checkItemName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
if (actionSelected === 'uncheck') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'uncheckItem',
checklistName,
checkItemName,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
}
},
});
+30 -32
View File
@@ -1,34 +1,32 @@
BlazeComponent.extendComponent({
onCreated() {},
import Actions from '/models/actions';
import Rules from '/models/rules';
import Triggers from '/models/triggers';
import { Utils } from '/client/lib/utils';
events() {
return [
{
'click .js-mail-action'(event) {
const emailTo = this.find('#email-to').value;
const emailSubject = this.find('#email-subject').value;
const emailMsg = this.find('#email-msg').value;
const trigger = this.data().triggerVar.get();
const ruleName = this.data().ruleName.get();
const triggerId = Triggers.insert(trigger);
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, this);
const actionId = Actions.insert({
actionType: 'sendEmail',
emailTo,
emailSubject,
emailMsg,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
},
},
];
Template.mailActions.events({
'click .js-mail-action'(event, tpl) {
const emailTo = tpl.find('#email-to').value;
const emailSubject = tpl.find('#email-subject').value;
const emailMsg = tpl.find('#email-msg').value;
const data = Template.currentData();
const trigger = data.triggerVar.get();
const ruleName = data.ruleName.get();
const triggerId = Triggers.insert(trigger);
const boardId = Session.get('currentBoard');
const desc = Utils.getTriggerActionDesc(event, tpl);
const actionId = Actions.insert({
actionType: 'sendEmail',
emailTo,
emailSubject,
emailMsg,
boardId,
desc,
});
Rules.insert({
title: ruleName,
triggerId,
actionId,
boardId,
});
},
}).register('mailActions');
});
+1 -1
View File
@@ -19,6 +19,6 @@ template(name="ruleDetails")
div.trigger-text
= action
div.rules-back
button.js-goback
button.primary.wide.js-goback(type="submit")
i.fa.fa-arrow-left
| {{_ 'back'}}
+14 -14
View File
@@ -1,31 +1,31 @@
import { ReactiveCache } from '/imports/reactiveCache';
BlazeComponent.extendComponent({
onCreated() {
this.subscribe('allRules');
this.subscribe('allTriggers');
this.subscribe('allActions');
this.subscribe('boards');
},
Template.ruleDetails.onCreated(function () {
this.subscribe('allRules');
this.subscribe('allTriggers');
this.subscribe('allActions');
this.subscribe('boards');
});
Template.ruleDetails.helpers({
trigger() {
const ruleId = this.data().ruleId;
const ruleId = Template.currentData().ruleId;
const rule = ReactiveCache.getRule(ruleId.get());
if (!rule) return '';
const trigger = ReactiveCache.getTrigger(rule.triggerId);
if (!trigger) return '';
const desc = trigger.description();
const upperdesc = desc.charAt(0).toUpperCase() + desc.substr(1);
return upperdesc;
},
action() {
const ruleId = this.data().ruleId;
const ruleId = Template.currentData().ruleId;
const rule = ReactiveCache.getRule(ruleId.get());
if (!rule) return '';
const action = ReactiveCache.getAction(rule.actionId);
if (!action) return '';
const desc = action.description();
const upperdesc = desc.charAt(0).toUpperCase() + desc.substr(1);
return upperdesc;
},
events() {
return [{}];
},
}).register('ruleDetails');
});
+3
View File
@@ -59,6 +59,9 @@
margin: auto;
margin-right: 14px;
}
.rules-back .js-goback .fa-arrow-left {
color: #fff !important;
}
.flex,
.triggers-content .triggers-body .triggers-side-menu ul li a {
display: -webkit-box;
+1 -1
View File
@@ -24,6 +24,6 @@ template(name="rulesActions")
else if $eq currentActions.get 'mail'
+mailActions(ruleName=data.ruleName triggerVar=data.triggerVar)
div.rules-back
button.js-goback
button.primary.wide.js-goback(type="submit")
i.fa.fa-arrow-left
| {{_ 'back'}}
+60 -52
View File
@@ -1,12 +1,20 @@
import { ReactiveCache } from '/imports/reactiveCache';
BlazeComponent.extendComponent({
onCreated() {
this.currentActions = new ReactiveVar('board');
Template.rulesActions.onCreated(function () {
this.currentActions = new ReactiveVar('board');
});
Template.rulesActions.helpers({
currentActions() {
return Template.instance().currentActions;
},
data() {
return Template.currentData();
},
ruleNameStr() {
const rn = this.data() && this.data().ruleName;
const rn = Template.currentData() && Template.currentData().ruleName;
try {
return rn && typeof rn.get === 'function' ? rn.get() : '';
} catch (_) {
@@ -14,59 +22,59 @@ BlazeComponent.extendComponent({
}
},
setBoardActions() {
this.currentActions.set('board');
$('.js-set-card-actions').removeClass('active');
$('.js-set-board-actions').addClass('active');
$('.js-set-checklist-actions').removeClass('active');
$('.js-set-mail-actions').removeClass('active');
},
setCardActions() {
this.currentActions.set('card');
$('.js-set-card-actions').addClass('active');
$('.js-set-board-actions').removeClass('active');
$('.js-set-checklist-actions').removeClass('active');
$('.js-set-mail-actions').removeClass('active');
},
setChecklistActions() {
this.currentActions.set('checklist');
$('.js-set-card-actions').removeClass('active');
$('.js-set-board-actions').removeClass('active');
$('.js-set-checklist-actions').addClass('active');
$('.js-set-mail-actions').removeClass('active');
},
setMailActions() {
this.currentActions.set('mail');
$('.js-set-card-actions').removeClass('active');
$('.js-set-board-actions').removeClass('active');
$('.js-set-checklist-actions').removeClass('active');
$('.js-set-mail-actions').addClass('active');
},
rules() {
const ret = ReactiveCache.getRules({});
return ret;
},
name() {
// console.log(this.data());
// console.log(Template.currentData());
},
events() {
return [
{
'click .js-set-board-actions'() {
this.setBoardActions();
},
'click .js-set-card-actions'() {
this.setCardActions();
},
'click .js-set-mail-actions'() {
this.setMailActions();
},
'click .js-set-checklist-actions'() {
this.setChecklistActions();
},
},
];
});
function setBoardActions(tpl) {
tpl.currentActions.set('board');
$('.js-set-card-actions').removeClass('active');
$('.js-set-board-actions').addClass('active');
$('.js-set-checklist-actions').removeClass('active');
$('.js-set-mail-actions').removeClass('active');
}
function setCardActions(tpl) {
tpl.currentActions.set('card');
$('.js-set-card-actions').addClass('active');
$('.js-set-board-actions').removeClass('active');
$('.js-set-checklist-actions').removeClass('active');
$('.js-set-mail-actions').removeClass('active');
}
function setChecklistActions(tpl) {
tpl.currentActions.set('checklist');
$('.js-set-card-actions').removeClass('active');
$('.js-set-board-actions').removeClass('active');
$('.js-set-checklist-actions').addClass('active');
$('.js-set-mail-actions').removeClass('active');
}
function setMailActions(tpl) {
tpl.currentActions.set('mail');
$('.js-set-card-actions').removeClass('active');
$('.js-set-board-actions').removeClass('active');
$('.js-set-checklist-actions').removeClass('active');
$('.js-set-mail-actions').addClass('active');
}
Template.rulesActions.events({
'click .js-set-board-actions'(event, tpl) {
setBoardActions(tpl);
},
}).register('rulesActions');
'click .js-set-card-actions'(event, tpl) {
setCardActions(tpl);
},
'click .js-set-mail-actions'(event, tpl) {
setMailActions(tpl);
},
'click .js-set-checklist-actions'(event, tpl) {
setChecklistActions(tpl);
},
});
+6 -6
View File
@@ -1,5 +1,5 @@
template(name="rulesList")
.rules
.rules(class=currentBoard.colorClass)
h2
i.fa.fa-magic
| {{_ 'r-board-rules' }}
@@ -10,28 +10,28 @@ template(name="rulesList")
p
= title
div.rules-btns-group
button.js-goto-details
button.primary.wide.js-goto-details(type="submit")
i.fa.fa-eye
| {{_ 'r-view-rule'}}
if currentUser.isAdmin
button.js-delete-rule
button.primary.wide.js-delete-rule(type="submit")
i.fa.fa-trash
| {{_ 'r-delete-rule'}}
else if currentUser.isBoardAdmin
button.js-delete-rule
button.primary.wide.js-delete-rule(type="submit")
i.fa.fa-trash
| {{_ 'r-delete-rule'}}
else
li.no-items-message {{_ 'r-no-rules' }}
if currentUser.isAdmin
div.rules-add
button.js-goto-trigger
button.primary.wide.rules-add-trigger.js-goto-trigger(type="submit")
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
button.primary.wide.rules-add-trigger.js-goto-trigger(type="submit")
i.fa.fa-plus
| {{_ 'r-add-rule'}}
input(type=text,placeholder="{{_ 'r-new-rule-name' }}",id="ruleTitle")
+5 -8
View File
@@ -1,10 +1,10 @@
import { ReactiveCache } from '/imports/reactiveCache';
BlazeComponent.extendComponent({
onCreated() {
this.subscribe('allRules');
},
Template.rulesList.onCreated(function () {
this.subscribe('allRules');
});
Template.rulesList.helpers({
rules() {
const boardId = Session.get('currentBoard');
const ret = ReactiveCache.getRules({
@@ -12,7 +12,4 @@ BlazeComponent.extendComponent({
});
return ret;
},
events() {
return [{}];
},
}).register('rulesList');
});
Loaded 100 of 973 files, more files were not shown because too many files have changed in this diff. Show more