Compare commits

..

No commits in common. "main" and "v8.17" have entirely different histories.
main ... v8.17

315 changed files with 5908 additions and 26659 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
[main]
host = https://www.transifex.com
lang_map = te_IN: te-IN, es_AR: es-AR, es_419: es-LA, es_TX: es-TX, he_IL: he-IL, zh_CN: zh-CN, ar_EG: ar-EG, cs_CZ: cs-CZ, fa_IR: fa-IR, ms_MY: ms-MY, nl_NL: nl-NL, de_CH: de-CH, en_IT: en-IT, uz_UZ: uz-UZ, fr_CH: fr-CH, hi_IN: hi-IN, et_EE: et-EE, es_PE: es-PE, es_MX: es-MX, gl_ES: gl-ES, mn_MN: mn, zh_TW: zh-TW, ast_ES: ast-ES, es_CL: es-CL, ja_JP: ja, lv_LV: lv, ro_RO: ro-RO, az_AZ: az-AZ, cy_GB: cy-GB, gu_IN: gu-IN, pl_PL: pl-PL, vep: ve-PP, en_BR: en-BR, en@ysv: en-YS, hu_HU: hu, ko_KR: ko-KR, pt_BR: pt-BR, zh_HK: zh-HK, zu_ZA: zu-ZA, en_MY: en-MY, ja-Hira: ja-HI, fi_FI: fi, vec: ve-CC, vi_VN: vi-VN, fr_FR: fr-FR, id_ID: id, zh_Hans: zh-Hans, en_DE: en-DE, en_GB: en-GB, el_GR: el-GR, uk_UA: uk-UA, az@latin: az-LA, de_AT: de-AT, uz@Latn: uz-LA, vls: vl-SS, ar_DZ: ar-DZ, bg_BG: bg, es_PY: es-PY, fy_NL: fy-NL, uz@Arab: uz-AR, ru_UA: ru-UA, war: wa-RR, zh_CN.GB2312: zh-GB
lang_map = te_IN: te-IN, es_AR: es-AR, es_419: es-LA, es_TX: es-TX, he_IL: he-IL, zh_CN: zh-CN, ar_EG: ar-EG, cs_CZ: cs-CZ, fa_IR: fa-IR, ms_MY: ms-MY, nl_NL: nl-NL, de_CH: de-CH, en_IT: en-IT, uz_UZ: uz-UZ, fr_CH: fr-CH, hi_IN: hi-IN, et_EE: et-EE, es_PE: es-PE, es_MX: es-MX, gl_ES: gl-ES, mn_MN: mn, sl_SI: sl, zh_TW: zh-TW, ast_ES: ast-ES, es_CL: es-CL, ja_JP: ja, lv_LV: lv, ro_RO: ro-RO, az_AZ: az-AZ, cy_GB: cy-GB, gu_IN: gu-IN, pl_PL: pl-PL, vep: ve-PP, en_BR: en-BR, en@ysv: en-YS, hu_HU: hu, ko_KR: ko-KR, pt_BR: pt-BR, zh_HK: zh-HK, zu_ZA: zu-ZA, en_MY: en-MY, ja-Hira: ja-HI, fi_FI: fi, vec: ve-CC, vi_VN: vi-VN, fr_FR: fr-FR, id_ID: id, zh_Hans: zh-Hans, en_DE: en-DE, en_GB: en-GB, el_GR: el-GR, uk_UA: uk-UA, az@latin: az-LA, de_AT: de-AT, uz@Latn: uz-LA, vls: vl-SS, ar_DZ: ar-DZ, bg_BG: bg, es_PY: es-PY, fy_NL: fy-NL, uz@Arab: uz-AR, ru_UA: ru-UA, war: wa-RR, zh_CN.GB2312: zh-GB
[o:wekan:p:wekan:r:application]
file_filter = imports/i18n/data/<lang>.i18n.json

View file

@ -22,179 +22,6 @@ Fixing other platforms In Progress.
WeKan 8.00-8.06 had wrong raw database directory setting /var/snap/wekan/common/wekan and some cards were not visible.
Those are fixed at WeKan 8.07 where database directory is back to /var/snap/wekan/common and all cards are visible.
# Upcoming WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUES of [Snowbleed](https://wekan.fi/hall-of-fame/snowbleed/):
- [Security Fix 1: There was not enough permission checks. Moved migrations to Admin Panel/Settings/Cron](https://github.com/wekan/wekan/commit/cbb1cd78de3e40264a5e047ace0ce27f8635b4e6).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 2: ](https://github.com/wekan/wekan/commit/).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 3: ](https://github.com/wekan/wekan/commit/).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 4: ](https://github.com/wekan/wekan/commit/).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 5: ](https://github.com/wekan/wekan/commit/).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 6: ](https://github.com/wekan/wekan/commit/).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 7: ](https://github.com/wekan/wekan/commit/).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 8: ](https://github.com/wekan/wekan/commit/).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 9: ](https://github.com/wekan/wekan/commit/).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 10: ](https://github.com/wekan/wekan/commit/).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 11: ](https://github.com/wekan/wekan/commit/).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 12: ](https://github.com/wekan/wekan/commit/).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 13: ](https://github.com/wekan/wekan/commit/).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 14: ](https://github.com/wekan/wekan/commit/).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
and adds the following updates:
- [Updated dependencies](https://github.com/wekan/wekan/pull/6059).
Thanks to dependabot.
- [Updated dependencies and published as @wekanteam npm packages to npmjs.com](https://github.com/wekan/wekan/commit/a9a89b501a91ffcdbdd611a05029d9483c59e4db).
Thanks to xet7.
- Added FerretDB2/PostgreSQL Docs.
[Part 1](https://github.com/wekan/wekan/commit/9fb1aeb8272b011c3d0b6b2c26ff7cb498c7b37f),
[Part 2](https://github.com/wekan/wekan/commit/f198421f10dd3be9d58f64a242d12ea1ef45fee3).
Thanks to juri_ at WeKan Libera.Chat IRC and xet7.
and fixes the following bugs:
- [Fix attachment download error with non-ASCII filenames](https://github.com/wekan/wekan/pull/6056).
Thanks to brlin-tw.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.19 2025-12-29 WeKan ® release
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.
- [Security Fix 2: Private-only board setting can be bypassed](https://github.com/wekan/wekan/commit/7ed76c180ede46ab1dac6b8ad27e9128a272c2c8).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 3: Card comment author spoofing (IDOR) via API](https://github.com/wekan/wekan/commit/67cb47173c1a152d9eaf5469740992b2dacdf62d).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 4: Cross-board card move without destination authorization](https://github.com/wekan/wekan/commit/198509e7600981400353aec6259247b3c04e043e).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 5: Read-only roles can still update cards](https://github.com/wekan/wekan/commit/181f837d8cbae96bdf9dcbd31beaa3653c2c0285).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 6: Checklist delete IDOR: checklist not verified against board/card](https://github.com/wekan/wekan/commit/08a6f084eba09487743a7c807fb4a9000fcfa9ac).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 7: Checklist create IDOR: cardId not verified against boardId](https://github.com/wekan/wekan/commit/5cd875813fdec5a3c40a0358b30a347967c85c14).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 8: Attachments publication leaks metadata without auth](https://github.com/wekan/wekan/commit/6dfa3beb2b6ab23438d0f4395b84bf0749eb4820).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 9: Attachment upload not scoped to card/board relationship](https://github.com/wekan/wekan/commit/1d16955b6d4f0a0282e89c2c1b0415c7597019b8).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 10: LDAP filter injection in LDAP auth](https://github.com/wekan/wekan/commit/0b0e16c3eae28bbf453d33a81a9c58ce7db6d5bb).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
and adds the following new features:
- [Opened card Checklist menu: Hide finished tasks. Show Checklist at Minicard](https://github.com/wekan/wekan/commit/fbfde81bc8208b718c070a6eeba4b2e2d2ce83ba).
Thanks to C0rn3j and xet7.
and adds the following updates:
- [Helm Chart: Updated MongoDB to 7.0.28 at artifacthub.io](https://github.com/wekan/charts/commit/5e6d344e0b976ce683116b66a1fb8417590115aa).
Thanks to xet7 and titver968.
and fixes the following bugs:
- [Re-add JS closing class to unicode close announcement symbol](https://github.com/wekan/wekan/pull/6050).
Thanks to Chostakovitch.
- [Cannot re-arrange lists within swimlanes](https://github.com/wekan/wekan/pull/6052).
Thanks to Chostakovitch.
- Converted Gantt from js to Jade, and made card title to render markdown at Gantt view.
[Part 1](https://github.com/wekan/wekan/commit/2d3bef9033134c3b62cf22179bbee4b6fea81444),
[Part 2](https://github.com/wekan/wekan/commit/3af3c9a89d8a4020b6f1ccada7da2ccbec1a8562).
Thanks to xet7.
- [Fix find.sh work with spaces, for example: ./find.sh "Some text"](https://github.com/wekan/wekan/commit/db4b04d8377523440fd2c36c1633ee74d7b05146).
Thanks to xet7.
- [Fix copy move card at board and MultiSelect to have numbered target of board, card above or below. Added MultiSelect change color](https://github.com/wekan/wekan/commit/74f1dfde72b9448645552ae28ba8d989d3e823d8).
Thanks to mimZD and xet7.
- [Fix move card last selection is gone](https://github.com/wekan/wekan/commit/2d87ba18b31ab5d8dc91dce01199cf7b313bd560).
Thanks to mimZD and xet7.
- [Fix Unable to delete Checklist. Added confirm delete to Checklist and Chekclist Item](https://github.com/wekan/wekan/commit/cf62807ad5d056ce9b8045c55f7cf6c29044967b).
Thanks to C0rn3j and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.18 2025-12-28 WeKan ® release
This release adds the following CRITICAL SECURITY FIXES:
- [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:
- [Gantt chart view to one board view menu Swimlanes/Lists/Calendar/Gantt](https://github.com/wekan/wekan/commit/f34e4c0e363e386dbcce8e6ee8933b2d50491c58).
Thanks to xet7.
- [Number of cards per list and sum of custom number field in list head](https://github.com/wekan/wekan/commit/e569c2957ecc2b5fbf65ddcf0793b97c3ed5da81).
Thanks to xet7.
- [New Board Permissions: NormalAssignedOnly, CommentAssignedOnly, ReadOnly, ReadAssignedOnly](https://github.com/wekan/wekan/commit/c1168d181b3ff34f5ee7794a5740281c4ab5e253).
Thanks to xet7.
- [More translations. Added support page to Admin Panel / Settings / Layout](https://github.com/wekan/wekan/commit/a7400dca4503961267cc5fd6a1c8efaa78668f77).
Thanks to xet7.
- [Right top User Settings / Grey Icons. Also fixed Change Language popup](https://github.com/wekan/wekan/commit/300b653ea3416892faf2d08f5e0be3752e2041d6).
Thanks to xet7.
- [Collapse Swimlane, List, Opened Card. Opened Card window X and Y position can be moved freely from drag handle. Fix some dragging not possible. Fix iPhone Safari](https://github.com/wekan/wekan/commit/58f4884ad603e4f8c68a8819dfb1440234da70b6).
Thanks to xet7.
- Per-User and Board-level data save fixes. Per-User is collapse, width, height. Per-Board is Swimlanes, Lists, Cards etc.
[Part 1](https://github.com/wekan/wekan/commit/414b8dbf41ecf368d54aeceb6a78ccd0aa58f6a6),
[Part 2](https://github.com/wekan/wekan/commit/58e970d68508a76a1b9333941eb1696fb8fb7727).
Thanks to xet7.
and adds the following updates:
- [Update GitHub docker/metadata-action from 5.8.0 to 5.9.0](https://github.com/wekan/wekan/pull/6012).
Thanks to dependabot.
- [Updated security.md](https://github.com/wekan/wekan/commit/7ff1649d8909917cae590c68def6eecac0442f91).
Thanks to xet7.
- [Updated build script for Linux arm64 bundle](https://github.com/wekan/wekan/commit/3db1305e58168f7417023ccd8d54995026844b18).
Thanks to xet7.
- Update Backup docs about migrating to newest WeKan.
[Part 1](https://github.com/wekan/wekan/commit/e669b1b9c72278c8debbc9de74d3fa02224a66d8),
[Part 2](https://github.com/wekan/wekan/commit/19fa12bb26a0444acffd49f24123ed993c425f6a),
[Part 3](https://github.com/wekan/wekan/commit/4e346c0ab7fbfb39544063cbd0e095307b26648f),
[Part 4](https://github.com/wekan/wekan/commit/59fc756a0bda8e11b9d86961daa35bb755110a68),
[Part 5](https://github.com/wekan/wekan/commit/30541260f0f979662889bc40b4db461af1583a07),
[Part 6](https://github.com/wekan/wekan/commit/784c5c6b0c83397ab4344d1a0fa231f33ff26564),
[Part 7](https://github.com/wekan/wekan/commit/5686c92e05452a5d91c10ed436fae71103ecfb1f),
[Part 8](https://github.com/wekan/wekan/commit/b7ff370561153bbfbb07426f9bd8b4d2977b1d0c),
[Part 9](https://github.com/wekan/wekan/commit/fe4b36b85d4ac8efddb2c7148bc5d2413cd643e1),
[Part 10](https://github.com/wekan/wekan/commit/9ebdc82d46d86029df12adaafba95c0ecfc9d2c2),
[Part 11](https://github.com/wekan/wekan/commit/3ef0a3e685657eba1cc07314ac8d195f89dbef74),
[Part 12](https://github.com/wekan/wekan/commit/2cbf64da33aff2d0b77ee91e7e9ac360cd1edb99),
[Part 13](https://github.com/wekan/wekan/commit/3c578403404084ae10e4349b5570b0d50ecd8eb4),
[Part 14](https://github.com/wekan/wekan/commit/451e9f78705dbbac2ed6ce123fd5440a871b6dcc),
[Part 15](https://github.com/wekan/wekan/commit/e07e461e482f54c8ddaebc63373c93dc4aa0d956).
and fixes the following bugs:
- [Fix Broken Strikethroughs in Markdown to HTML conversion](https://github.com/wekan/wekan/pull/6009).
Thanks to brlin-tw.
- [Updated Mac docs for Applite](https://github.com/wekan/wekan/commit/400eb81206f346a973d871a8aaa55d4ac5d48753).
Thanks to xet7.
- [Fix checklist delete action (issue #6020), link-card popup defaults, and stabilize due-cards ordering](https://github.com/wekan/wekan/pull/5967).
Thanks to seve12.
- [Improve rules UI board dropdowns/loading, rule header titles, and ensure card move updates attachment metadata](https://github.com/wekan/wekan/pull/5967).
Thanks to seve12.
- [Improve imports: normalize id → _id, add default swimlane fallback, and add regression test](https://github.com/wekan/wekan/pull/5967).
Thanks to seve12.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.17 2025-11-06 WeKan ® release
This release adds the following new feature:
@ -233,7 +60,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 SpaceBleed that is the following CRITICAL SECURITY ISSUES:
- [Fix SECURITY ISSUE 1: File Attachments enables stored XSS (High)](https://github.com/wekan/wekan/commit/e9a727301d7b4f1689a703503df668c0f4f4cab8).
Thanks to Siam Thanat Hack (STH) and xet7.

View file

@ -249,9 +249,9 @@ cd /home/wekan/app
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
#rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
#mv /home/wekan/app_build/bundle /build
wget "https://github.com/wekan/wekan/releases/download/v8.19/wekan-8.19-amd64.zip"
unzip wekan-8.19-amd64.zip
rm wekan-8.19-amd64.zip
wget "https://github.com/wekan/wekan/releases/download/v8.17/wekan-8.17-amd64.zip"
unzip wekan-8.17-amd64.zip
rm wekan-8.17-amd64.zip
mv /home/wekan/app/bundle /build
# Put back the original tar

View file

@ -1,20 +1,12 @@
About money, see [CONTRIBUTING.md](CONTRIBUTING.md)
## Responsible Security Disclosure
Security is very important to us. If you discover any issue regarding security, please disclose
the information responsibly by sending an email from Protonmail to security@wekan.fi
that is Protomail email address, or by using this PGP key
[security-at-wekan.fi.asc](security-at-wekan.fi.asc) to security@wekan.fi
and not by creating a GitHub issue. We will respond swiftly to fix verifiable security issues.
- To send email, use [ProtonMail](https://proton.me) email address or use PGP key [security-at-wekan.fi.asc](security-at-wekan.fi.asc)
- Send info about security issue ONLY to security@wekan.fi (that is Protomail email address). NOT TO ANYWHERE ELSE. NO CC, NO BCC.
- Wait for new WeKan release that fixes security issue
- If you approve, we thank you by adding you to Hall of Fame: https://wekan.fi/hall-of-fame/
## Bonus Points
- If you include code for fixing security issue
## Losing Points
- If you ask about [bounty](CONTRIBUTING.md). There is no bounty. WeKan is NOT Big Tech. WeKan is FLOSS.
- If you forget to include vulnerability details.
- If you send info about security issue to somewhere else than security@wekan.fi
We thank you with a place at our hall of fame page, that is at https://wekan.fi/hall-of-fame
## How should reports be formatted?
@ -34,7 +26,7 @@ CWSS (optional): %cwss
Anyone who reports a unique security issue in scope and does not disclose it to
a third party before we have patched and updated may be upon their approval
added to the WeKan Hall of Fame https://wekan.fi/hall-of-fame/
added to the Wekan Hall of Fame.
## Which domains are in scope?
@ -71,6 +63,11 @@ and by by companies that have 30k users.
- If you are thinking about TLS MITM, look at https://github.com/caddyserver/caddy/issues/2530
- Let's Encrypt TLS requires publicly accessible webserver, that Let's Encrypt TLS validation servers check.
- If firewall limits to only allowed IP addresses, you may need non-Let's Encrypt TLS cert.
- For On Premise:
- https://caddyserver.com/docs/automatic-https#local-https
- https://github.com/wekan/wekan/wiki/Caddy-Webserver-Config
- https://github.com/wekan/wekan/wiki/Azure
- https://github.com/wekan/wekan/wiki/Traefik-and-self-signed-SSL-certs
## XSS
@ -272,4 +269,9 @@ Typical already known or "no impact" bugs such as:
- Email spoofing, SPF, DMARC & DKIM. Wekan does not include email server.
Wekan is Open Source with MIT license, and free to use also for commercial use.
We welcome all fixes to improve security by email to security@wekan.fi
We welcome all fixes to improve security by email to security@wekan.team
## Bonus Points
If your Responsible Security Disclosure includes code for fixing security issue,
you get bonus points, as seen on [Hall of Fame](https://wekan.github.io/hall-of-fame).

View file

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

View file

@ -10,8 +10,8 @@ import '/client/lib/boardConverter';
import '/client/components/boardConversionProgress';
// Import migration manager and progress UI
import '/client/lib/attachmentMigrationManager';
import '/client/components/settings/migrationProgress';
import '/client/lib/migrationManager';
import '/client/components/migrationProgress';
// Import cron settings
import '/client/components/settings/cronSettings';
@ -62,20 +62,3 @@ Meteor.startup(() => {
}
});
});
// Subscribe to per-user small publications
Meteor.startup(() => {
Tracker.autorun(() => {
if (Meteor.userId()) {
Meteor.subscribe('userGreyIcons');
}
});
// Initialize mobile mode on startup for iOS devices
// This ensures mobile mode is applied correctly on page load
Tracker.afterFlush(() => {
if (typeof Utils !== 'undefined' && Utils.initializeUserSettings) {
Utils.initializeUserSettings();
}
});
});

View file

@ -108,12 +108,15 @@
text-decoration: none;
height: 24px;
}
.comments .comment .comment-desc .reactions .open-comment-reaction-popup span {
display: inline-block;
font-size: clamp(14px, 2vw, 18px);
.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-smile-o {
font-size: 17px;
font-weight: 500;
line-height: 1;
margin-left: 4px;
margin-left: 2px;
}
.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-plus {
font-size: 8px;
margin-top: -7px;
margin-left: 1px;
}
.comments .comment .comment-desc .reactions .reaction {
cursor: pointer;

View file

@ -25,7 +25,7 @@ template(name="comment")
= text
.edit-controls
button.primary(type="submit") {{_ 'edit'}}
a.js-close-inlined-form(title="{{_ 'close' }}") ❌
.fa.fa-times-thin.js-close-inlined-form
else
.comment-text
+viewer
@ -55,8 +55,8 @@ template(name="commentReactions")
span.reaction-count #{reaction.userIds.length}
if (currentUser.isBoardMember)
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
span(title="{{_ 'reaction' }}") 😀
span(title="{{_ 'add' }}")
i.fa.fa-smile-o
i.fa.fa-plus
template(name="addReactionPopup")
.reactions-popup

View file

@ -57,9 +57,8 @@ BlazeComponent.extendComponent({
BlazeComponent.extendComponent({
getComments() {
const data = this.data();
if (!data || typeof data.comments !== 'function') return [];
return data.comments();
const ret = this.data().comments();
return ret;
},
}).register("comments");

View file

@ -1,6 +1,6 @@
template(name="archivedBoards")
h2
span(title="{{_ 'archived-boards'}}") 📦
i.fa.fa-archive
| {{_ 'archived-boards'}}
ul.archived-lists
@ -8,10 +8,10 @@ template(name="archivedBoards")
li.archived-lists-item
div.board-header-btns
button.board-header-btn.js-delete-board
| 🗑️
i.fa.fa-trash-o
| {{_ 'delete-board'}}
button.board-header-btn.js-restore-board
| ↩️
i.fa.fa-undo
| {{_ 'restore-board'}}
= title
span {{ moment archivedAt 'LLL' }}

View file

@ -231,30 +231,6 @@
font-size: 1em !important; /* Keep original icon size */
}
/* Mobile iPhone: scale card details text and icons to 2x */
body.mobile-mode.iphone-device .card-details {
font-size: 2em !important;
}
body.mobile-mode.iphone-device .card-details .fa,
body.mobile-mode.iphone-device .card-details .icon,
body.mobile-mode.iphone-device .card-details i,
body.mobile-mode.iphone-device .card-details .emoji-icon,
body.mobile-mode.iphone-device .card-details a,
body.mobile-mode.iphone-device .card-details p,
body.mobile-mode.iphone-device .card-details span,
body.mobile-mode.iphone-device .card-details div,
body.mobile-mode.iphone-device .card-details button,
body.mobile-mode.iphone-device .card-details input,
body.mobile-mode.iphone-device .card-details select,
body.mobile-mode.iphone-device .card-details textarea {
font-size: inherit !important;
}
/* Section titles slightly larger than content but not as big as card title */
body.mobile-mode.iphone-device .card-details .card-details-item-title {
font-size: 1.1em !important;
font-weight: bold;
}
/* Ensure scrollbars are positioned correctly */
#content[style*="overflow-x: auto"]::-webkit-scrollbar:vertical {
width: 12px;
@ -287,35 +263,6 @@ body.mobile-mode.iphone-device .card-details .card-details-item-title {
animation: fadeIn 0.2s;
z-index: 16;
}
/* Fix for mobile Safari: ensure overlay stays behind card details */
@media screen and (max-width: 800px) {
.board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;
}
/* In desktop mode on small screens, still keep overlay behind card */
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;
}
}
/* In mobile mode, lower the overlay z-index to stay behind card details */
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;
}
/* iPhone in desktop mode: remove overlay to avoid blocking card */
body.desktop-mode.iphone-device .board-wrapper .board-canvas .board-overlay {
display: none !important;
pointer-events: none !important;
}
/* Desktop mode: hide overlay to allow multiple cards and board interaction */
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
display: none !important;
pointer-events: none !important;
}
.board-wrapper .board-canvas.is-dragging-active .open-minicard-composer,
.board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked {
display: none;

View file

@ -1,6 +1,8 @@
template(name="board")
if isConverting.get
if isMigrating.get
+migrationProgress
else if isConverting.get
+boardConversionProgress
else if isBoardReady.get
if currentBoard
@ -22,7 +24,7 @@ template(name="boardBody")
// Debug information (remove in production)
if debugBoardState
.debug-info(style="position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.8); color: white; padding: 10px; z-index: 9999; font-size: 12px;")
| {{_ 'board'}}: {{currentBoard.title}} | {{_ 'view'}}: {{boardView}} | {{_ 'has-swimlanes'}}: {{hasSwimlanes}} | {{_ 'swimlanes'}}: {{currentBoard.swimlanes.length}}
| Board: {{currentBoard.title}} | View: {{boardView}} | HasSwimlanes: {{hasSwimlanes}} | Swimlanes: {{currentBoard.swimlanes.length}}
.board-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
.board-canvas.js-swimlanes(
class="{{#if hasSwimlanes}}dragscroll{{/if}}"
@ -47,8 +49,6 @@ template(name="boardBody")
+listsGroup(currentBoard)
else if isViewCalendar
+calendarView
else if isViewGantt
+ganttView
else
// Default view - show swimlanes if they exist, otherwise show lists
if hasSwimlanes
@ -56,10 +56,6 @@ template(name="boardBody")
+swimlane(this)
else
+listsGroup(currentBoard)
//- Render multiple open cards in desktop mode
unless isMiniScreen
each openCards
+cardDetails(this cardIndex=@index)
+sidebar
template(name="calendarView")
@ -67,4 +63,4 @@ template(name="calendarView")
.calendar-view.swimlane
if currentCard
+cardDetails(currentCard)
+fullcalendar(calendarOptions)
+fullcalendar(calendarOptions)

View file

@ -1,9 +1,10 @@
import { ReactiveCache } from '/imports/reactiveCache';
import '../gantt/gantt.js';
import { TAPi18n } from '/imports/i18n';
import dragscroll from '@wekanteam/dragscroll';
import { boardConverter } from '/client/lib/boardConverter';
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
import { migrationManager } from '/client/lib/migrationManager';
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
import { migrationProgressManager } from '/client/components/migrationProgress';
import Swimlanes from '/models/swimlanes';
import Lists from '/models/lists';
@ -15,6 +16,7 @@ BlazeComponent.extendComponent({
onCreated() {
this.isBoardReady = new ReactiveVar(false);
this.isConverting = new ReactiveVar(false);
this.isMigrating = new ReactiveVar(false);
this._swimlaneCreated = new Set(); // Track boards where we've created swimlanes
this._boardProcessed = false; // Track if board has been processed
this._lastProcessedBoardId = null; // Track last processed board ID
@ -32,6 +34,7 @@ BlazeComponent.extendComponent({
// Use a separate autorun for subscription ready state to avoid reactive loops
this.subscriptionReadyAutorun = Tracker.autorun(() => {
if (handle.ready()) {
// Only run conversion/migration logic once per board
if (!this._boardProcessed || this._lastProcessedBoardId !== currentBoardId) {
this._boardProcessed = true;
this._lastProcessedBoardId = currentBoardId;
@ -96,31 +99,422 @@ BlazeComponent.extendComponent({
return;
}
// Automatic migration disabled - migrations must be run manually from sidebar
// Board admins can run migrations from the sidebar Migrations menu
this.isBoardReady.set(true);
} catch (error) {
console.error('Error during board conversion check:', error);
this.isConverting.set(false);
this.isMigrating.set(false);
this.isBoardReady.set(true); // Show board even if conversion check failed
}
},
/**
* Check if board needs comprehensive migration
*/
async checkComprehensiveMigration(boardId) {
try {
return new Promise((resolve, reject) => {
Meteor.call('comprehensiveBoardMigration.needsMigration', boardId, (error, result) => {
if (error) {
console.error('Error checking comprehensive migration:', error);
reject(error);
} else {
resolve(result);
}
});
});
} catch (error) {
console.error('Error checking comprehensive migration:', error);
return false;
}
},
/**
* Execute comprehensive migration for a board
*/
async executeComprehensiveMigration(boardId) {
try {
// Start progress tracking
migrationProgressManager.startMigration();
// Simulate progress updates since we can't easily pass callbacks through Meteor methods
const progressSteps = [
{ step: 'analyze_board_structure', name: 'Analyze Board Structure', duration: 1000 },
{ step: 'fix_orphaned_cards', name: 'Fix Orphaned Cards', duration: 2000 },
{ step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 3000 },
{ step: 'ensure_per_swimlane_lists', name: 'Ensure Per-Swimlane Lists', duration: 1500 },
{ step: 'validate_migration', name: 'Validate Migration', duration: 1000 },
{ step: 'fix_avatar_urls', name: 'Fix Avatar URLs', duration: 1000 },
{ step: 'fix_attachment_urls', name: 'Fix Attachment URLs', duration: 1000 }
];
// Start the actual migration
const migrationPromise = new Promise((resolve, reject) => {
Meteor.call('comprehensiveBoardMigration.execute', boardId, (error, result) => {
if (error) {
console.error('Error executing comprehensive migration:', error);
migrationProgressManager.failMigration(error);
reject(error);
} else {
if (process.env.DEBUG === 'true') {
console.log('Comprehensive migration completed for board:', boardId, result);
}
resolve(result.success);
}
});
});
// Simulate progress updates
const progressPromise = this.simulateMigrationProgress(progressSteps);
// Wait for both to complete
const [migrationResult] = await Promise.all([migrationPromise, progressPromise]);
migrationProgressManager.completeMigration();
return migrationResult;
} catch (error) {
console.error('Error executing comprehensive migration:', error);
migrationProgressManager.failMigration(error);
return false;
}
},
/**
* Simulate migration progress updates
*/
async simulateMigrationProgress(progressSteps) {
const totalSteps = progressSteps.length;
for (let i = 0; i < progressSteps.length; i++) {
const step = progressSteps[i];
const stepProgress = Math.round(((i + 1) / totalSteps) * 100);
// Update progress for this step
migrationProgressManager.updateProgress({
overallProgress: stepProgress,
currentStep: i + 1,
totalSteps,
stepName: step.step,
stepProgress: 0,
stepStatus: `Starting ${step.name}...`,
stepDetails: null,
boardId: Session.get('currentBoard')
});
// Simulate step progress
const stepDuration = step.duration;
const updateInterval = 100; // Update every 100ms
const totalUpdates = stepDuration / updateInterval;
for (let j = 0; j < totalUpdates; j++) {
const stepStepProgress = Math.round(((j + 1) / totalUpdates) * 100);
migrationProgressManager.updateProgress({
overallProgress: stepProgress,
currentStep: i + 1,
totalSteps,
stepName: step.step,
stepProgress: stepStepProgress,
stepStatus: `Processing ${step.name}...`,
stepDetails: { progress: `${stepStepProgress}%` },
boardId: Session.get('currentBoard')
});
await new Promise(resolve => setTimeout(resolve, updateInterval));
}
// Complete the step
migrationProgressManager.updateProgress({
overallProgress: stepProgress,
currentStep: i + 1,
totalSteps,
stepName: step.step,
stepProgress: 100,
stepStatus: `${step.name} completed`,
stepDetails: { status: 'completed' },
boardId: Session.get('currentBoard')
});
}
},
async startBackgroundMigration(boardId) {
try {
// Start background migration using the cron system
Meteor.call('boardMigration.startBoardMigration', boardId, (error, result) => {
if (error) {
console.error('Failed to start background migration:', error);
} else {
if (process.env.DEBUG === 'true') {
console.log('Background migration started for board:', boardId);
}
}
});
} catch (error) {
console.error('Error starting background migration:', error);
}
},
async convertSharedListsToPerSwimlane(boardId) {
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) return;
// Check if board has already been processed for shared lists conversion
if (board.hasSharedListsConverted) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been processed for shared lists conversion`);
}
return;
}
// Get all lists for this board
const allLists = board.lists();
const swimlanes = board.swimlanes();
if (swimlanes.length === 0) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has no swimlanes, skipping shared lists conversion`);
}
return;
}
// Find shared lists (lists with empty swimlaneId or null swimlaneId)
const sharedLists = allLists.filter(list => !list.swimlaneId || list.swimlaneId === '');
if (sharedLists.length === 0) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has no shared lists to convert`);
}
// Mark as processed even if no shared lists
Boards.update(boardId, { $set: { hasSharedListsConverted: true } });
return;
}
if (process.env.DEBUG === 'true') {
console.log(`Converting ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`);
}
// Convert each shared list to per-swimlane lists
for (const sharedList of sharedLists) {
// Create a copy of the list for each swimlane
for (const swimlane of swimlanes) {
// Check if this list already exists in this swimlane
const existingList = Lists.findOne({
boardId: boardId,
swimlaneId: swimlane._id,
title: sharedList.title
});
if (!existingList) {
// Double-check to avoid race conditions
const doubleCheckList = ReactiveCache.getList({
boardId: boardId,
swimlaneId: swimlane._id,
title: sharedList.title
});
if (!doubleCheckList) {
// Create a new list in this swimlane
const newListData = {
title: sharedList.title,
boardId: boardId,
swimlaneId: swimlane._id,
sort: sharedList.sort || 0,
archived: sharedList.archived || false, // Preserve archived state from original list
createdAt: new Date(),
modifiedAt: new Date()
};
// Copy other properties if they exist
if (sharedList.color) newListData.color = sharedList.color;
if (sharedList.wipLimit) newListData.wipLimit = sharedList.wipLimit;
if (sharedList.wipLimitEnabled) newListData.wipLimitEnabled = sharedList.wipLimitEnabled;
if (sharedList.wipLimitSoft) newListData.wipLimitSoft = sharedList.wipLimitSoft;
Lists.insert(newListData);
if (process.env.DEBUG === 'true') {
const archivedStatus = sharedList.archived ? ' (archived)' : ' (active)';
console.log(`Created list "${sharedList.title}"${archivedStatus} for swimlane ${swimlane.title || swimlane._id}`);
}
} else {
if (process.env.DEBUG === 'true') {
console.log(`List "${sharedList.title}" already exists in swimlane ${swimlane.title || swimlane._id} (double-check), skipping`);
}
}
} else {
if (process.env.DEBUG === 'true') {
console.log(`List "${sharedList.title}" already exists in swimlane ${swimlane.title || swimlane._id}, skipping`);
}
}
}
// Remove the original shared list completely
Lists.remove(sharedList._id);
if (process.env.DEBUG === 'true') {
console.log(`Removed shared list "${sharedList.title}"`);
}
}
// Mark board as processed
Boards.update(boardId, { $set: { hasSharedListsConverted: true } });
if (process.env.DEBUG === 'true') {
console.log(`Successfully converted ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`);
}
} catch (error) {
console.error('Error converting shared lists to per-swimlane:', error);
}
},
async fixMissingLists(boardId) {
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) return;
// Check if board has already been processed for missing lists fix
if (board.fixMissingListsCompleted) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been processed for missing lists fix`);
}
return;
}
// Check if migration is needed
const needsMigration = await new Promise((resolve, reject) => {
Meteor.call('fixMissingListsMigration.needsMigration', boardId, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
if (!needsMigration) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} does not need missing lists fix`);
}
return;
}
if (process.env.DEBUG === 'true') {
console.log(`Starting fix missing lists migration for board ${boardId}`);
}
// Execute the migration
const result = await new Promise((resolve, reject) => {
Meteor.call('fixMissingListsMigration.execute', boardId, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
if (result && result.success) {
if (process.env.DEBUG === 'true') {
console.log(`Successfully fixed missing lists for board ${boardId}: created ${result.createdLists} lists, updated ${result.updatedCards} cards`);
}
}
} catch (error) {
console.error('Error fixing missing lists:', error);
}
},
async fixDuplicateLists(boardId) {
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) return;
// Check if board has already been processed for duplicate lists fix
if (board.fixDuplicateListsCompleted) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been processed for duplicate lists fix`);
}
return;
}
if (process.env.DEBUG === 'true') {
console.log(`Starting duplicate lists fix for board ${boardId}`);
}
// Execute the duplicate lists fix
const result = await new Promise((resolve, reject) => {
Meteor.call('fixDuplicateLists.fixBoard', boardId, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
if (result && result.fixed > 0) {
if (process.env.DEBUG === 'true') {
console.log(`Successfully fixed ${result.fixed} duplicate lists for board ${boardId}: ${result.fixedSwimlanes} swimlanes, ${result.fixedLists} lists`);
}
// Mark board as processed
Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } });
} else if (process.env.DEBUG === 'true') {
console.log(`No duplicate lists found for board ${boardId}`);
// Still mark as processed to avoid repeated checks
Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } });
} else {
// Still mark as processed to avoid repeated checks
Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } });
}
} catch (error) {
console.error('Error fixing duplicate lists:', error);
}
},
async startAttachmentMigrationIfNeeded(boardId) {
try {
// Check if board has already been migrated
if (attachmentMigrationManager.isBoardMigrated(boardId)) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been migrated, skipping`);
}
return;
}
// Check if there are unconverted attachments
const unconvertedAttachments = attachmentMigrationManager.getUnconvertedAttachments(boardId);
if (unconvertedAttachments.length > 0) {
if (process.env.DEBUG === 'true') {
console.log(`Starting attachment migration for ${unconvertedAttachments.length} attachments in board ${boardId}`);
}
await attachmentMigrationManager.startAttachmentMigration(boardId);
} else {
// No attachments to migrate, mark board as migrated
// This will be handled by the migration manager itself
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has no attachments to migrate`);
}
}
} catch (error) {
console.error('Error starting attachment migration:', error);
}
},
onlyShowCurrentCard() {
const isMiniScreen = Utils.isMiniScreen();
const currentCardId = Utils.getCurrentCardId(true);
return isMiniScreen && currentCardId;
},
openCards() {
// In desktop mode, return array of all open cards
const isMobile = Utils.getMobileMode();
if (!isMobile) {
const openCardIds = Session.get('openCards') || [];
return openCardIds.map(id => ReactiveCache.getCard(id)).filter(card => card);
}
return [];
},
goHome() {
FlowRouter.go('home');
},
@ -129,6 +523,10 @@ BlazeComponent.extendComponent({
return this.isConverting.get();
},
isMigrating() {
return this.isMigrating.get();
},
isBoardReady() {
return this.isBoardReady.get();
},
@ -580,19 +978,6 @@ BlazeComponent.extendComponent({
return boardView === 'board-view-cal';
},
isViewGantt() {
const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) {
boardView = (currentUser.profile || {}).boardView;
} else {
boardView = window.localStorage.getItem('boardView');
}
return boardView === 'board-view-gantt';
},
hasSwimlanes() {
const currentBoard = Utils.getCurrentBoard();
if (!currentBoard) {
@ -636,6 +1021,7 @@ BlazeComponent.extendComponent({
const currentBoardId = Session.get('currentBoard');
const isBoardReady = this.isBoardReady.get();
const isConverting = this.isConverting.get();
const isMigrating = this.isMigrating.get();
const boardView = Utils.boardView();
if (process.env.DEBUG === 'true') {
@ -644,6 +1030,7 @@ BlazeComponent.extendComponent({
console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none');
console.log('isBoardReady:', isBoardReady);
console.log('isConverting:', isConverting);
console.log('isMigrating:', isMigrating);
console.log('boardView:', boardView);
console.log('========================');
}
@ -654,6 +1041,7 @@ BlazeComponent.extendComponent({
currentBoardTitle: currentBoard ? currentBoard.title : 'none',
isBoardReady,
isConverting,
isMigrating,
boardView
};
},
@ -1020,8 +1408,3 @@ BlazeComponent.extendComponent({
}
},
}).register('calendarView');
/**
* Gantt View Component
* Displays cards as a Gantt chart with start/due dates
*/

View file

@ -109,7 +109,7 @@ template(name="boardHeaderBar")
| ❌
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
span.emoji-icon 🔍
| 🔍
unless currentBoard.isTemplatesBoard
a.board-header-btn.js-toggle-board-view(
@ -121,8 +121,6 @@ template(name="boardHeaderBar")
| 📋
if $eq boardView 'board-view-cal'
| 📅
if $eq boardView 'board-view-gantt'
| 📊
if canModifyBoard
a.board-header-btn.js-multiselection-activate(
@ -210,13 +208,6 @@ template(name="boardChangeViewPopup")
| {{_ 'board-view-cal'}}
if $eq Utils.boardView "board-view-cal"
| ✅
li
with "board-view-gantt"
a.js-open-gantt-view
| 📊
| {{_ 'board-view-gantt'}}
if $eq Utils.boardView "board-view-gantt"
| ✅
template(name="createBoard")
form

View file

@ -208,10 +208,6 @@ Template.boardChangeViewPopup.events({
Utils.setBoardView('board-view-cal');
Popup.back();
},
'click .js-open-gantt-view'() {
Utils.setBoardView('board-view-gantt');
Popup.back();
},
});
const CreateBoard = BlazeComponent.extendComponent({

View file

@ -583,9 +583,9 @@
}
.board-list .board-list-item .multi-selection-checkbox.is-checked {
background: #3cb500;
border-color: #3cb500;
box-shadow: 0 2px 8px rgba(60, 181, 0, 0.6);
background: #2196F3;
border-color: #2196F3;
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.6);
width: 24px !important;
height: 24px !important;
top: auto !important;
@ -601,22 +601,10 @@
font-weight: bold;
}
/* Grey checkboxes when grey icons setting is enabled */
body.grey-icons-enabled .board-list .board-list-item .multi-selection-checkbox.is-checked {
background: #7a7a7a;
border-color: #7a7a7a;
box-shadow: 0 2px 8px rgba(122, 122, 122, 0.6);
}
body.grey-icons-enabled .board-list.is-multiselection-active .js-board.is-checked {
outline: 4px solid #7a7a7a;
box-shadow: 0 4px 12px rgba(122, 122, 122, 0.4);
}
.board-list.is-multiselection-active .js-board.is-checked {
outline: 4px solid #3cb500;
outline: 4px solid #2196F3;
outline-offset: -4px;
box-shadow: 0 4px 12px rgba(60, 181, 0, 0.4);
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4);
}
/* Visual hint when multiselection is active */
@ -657,19 +645,8 @@ body.grey-icons-enabled .board-list.is-multiselection-active .js-board.is-checke
}
.board-backgrounds-list .board-background-select .background-box i.fa-check {
font-size: 25px;
color: #3cb500;
color: #fff;
}
/* Grey check icons when grey icons setting is enabled */
body.grey-icons-enabled .board-backgrounds-list .board-background-select .background-box i.fa-check {
color: #7a7a7a;
}
/* Prevent Grey Icons from affecting checkmarks in background color list */
body.grey-icons-enabled .checkmark-no-grey {
filter: none !important;
-webkit-filter: none !important;
}
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
.board-list.mobile-view {
height: calc(100vh - 120px);

View file

@ -8,26 +8,18 @@ template(name="boardList")
ul.menu
li(class="menu-item {{#if isSelectedMenu 'starred'}}active{{/if}}")
a.js-select-menu(data-type="starred")
span.menu-label
span.emoji-icon ⭐
| {{_ 'allboards.starred'}}
span.menu-label ⭐ {{_ 'allboards.starred'}}
span.menu-count {{menuItemCount 'starred'}}
li(class="menu-item {{#if isSelectedMenu 'templates'}}active{{/if}}")
a.js-select-menu(data-type="templates")
span.menu-label
span.emoji-icon 📋
| {{_ 'allboards.templates'}}
span.menu-label 📋 {{_ 'allboards.templates'}}
span.menu-count {{menuItemCount 'templates'}}
li(class="menu-item {{#if isSelectedMenu 'remaining'}}active{{/if}}")
a.js-select-menu(data-type="remaining")
span.menu-label
span.emoji-icon 📂
| {{_ 'allboards.remaining'}}
span.menu-label 📂 {{_ 'allboards.remaining'}}
span.menu-count {{menuItemCount 'remaining'}}
.workspaces-header
span
span.emoji-icon 🗂️
| {{_ 'allboards.workspaces'}}
span 🗂️ {{_ 'allboards.workspaces'}}
a.js-add-workspace(title="{{_ 'allboards.add-workspace'}}") +
// Workspaces tree
+workspaceTree(nodes=workspacesTree selectedWorkspaceId=selectedWorkspaceId)
@ -51,49 +43,44 @@ template(name="boardList")
li.AllBoardBtns
div.AllBoardButtonsContainer
if userHasOrgsOrTeams
span.emoji-icon 🔍
span 🔍
input#filterBtn(type="button" value="{{_ 'filter'}}")
button#resetBtn.filter-reset-btn
span.reset-icon
span.emoji-icon ❌
span.reset-icon ❌
span {{_ 'filter-clear'}}
// Right boards grid
.boards-right-grid
.boards-path-header
.path-left
span.path-icon.emoji-icon {{currentMenuPath.icon}}
span.path-icon {{currentMenuPath.icon}}
span.path-text {{currentMenuPath.text}}
if BoardMultiSelection.isActive
span.multiselection-hint
span.emoji-icon 📌
| {{_ 'multi-selection-active'}}
span.multiselection-hint 📌 {{_ 'multi-selection-active'}}
.path-right
if canModifyBoards
if hasBoardsSelected
button.js-archive-selected-boards.board-header-btn
span.emoji-icon 📦
span 📦
span {{_ 'archive-board'}}
button.js-duplicate-selected-boards.board-header-btn
span.emoji-icon 📋
span 📋
span {{_ 'duplicate-board'}}
a.board-header-btn.js-multiselection-activate(
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
span.emoji-icon ☑️
| ☑️
if BoardMultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
span.emoji-icon
|
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
li.js-add-board
if isSelectedMenu 'templates'
a.board-list-item.label(title="{{_ 'add-template-container'}}")
span.emoji-icon
| {{_ 'add-template-container'}}
| {{_ 'add-template-container'}}
else
a.board-list-item.label(title="{{_ 'add-board'}}")
span.emoji-icon
| {{_ 'add-board'}}
| {{_ 'add-board'}}
each boards
li.js-board(class="{{_id}} {{#if isStarred}}starred{{/if}} {{colorClass}} {{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}", draggable="true")
if isInvited
@ -106,8 +93,7 @@ template(name="boardList")
span.js-star-board(
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
title="{{_ 'star-board-title'}}")
span.emoji-icon
| {{#if isStarred}}⭐{{else}}☆{{/if}}
| {{#if isStarred}}⭐{{else}}☆{{/if}}
p.board-list-item-desc {{_ 'just-invited'}}
button.js-accept-invite.primary {{_ 'accept'}}
button.js-decline-invite {{_ 'decline'}}
@ -117,9 +103,7 @@ template(name="boardList")
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.board-handle(title="{{_ 'drag-board'}}")
span.emoji-icon ↕️
span.board-handle(title="{{_ 'drag-board'}}") ↕️
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
span.details
span.board-list-item-name(title="{{_ 'template-container'}}")
@ -132,20 +116,17 @@ template(name="boardList")
span.js-has-spenttime-cards(
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
span.emoji-icon ⏱️
| ⏱️
span.js-star-board(
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
title="{{_ 'star-board-title'}}")
span.emoji-icon
| {{#if isStarred}}⭐{{else}}☆{{/if}}
| {{#if isStarred}}⭐{{else}}☆{{/if}}
else
.board-list-item
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.board-handle(title="{{_ 'drag-board'}}")
span.emoji-icon ↕️
span.board-handle(title="{{_ 'drag-board'}}") ↕️
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
span.details
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
@ -170,12 +151,11 @@ template(name="boardList")
span.js-has-spenttime-cards(
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
span.emoji-icon ⏱️
| ⏱️
a.js-star-board(
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
title="{{_ 'star-board-title'}}")
span.emoji-icon
| {{#if isStarred}}⭐{{else}}☆{{/if}}
| {{#if isStarred}}⭐{{else}}☆{{/if}}
template(name="boardListHeaderBar")
h1 {{_ title }}
@ -194,19 +174,16 @@ template(name="workspaceTree")
each nodes
li.workspace-node(class="{{#if $eq id selectedWorkspaceId}}active{{/if}}" data-workspace-id="{{id}}" draggable="true")
.workspace-node-content
span.workspace-drag-handle
span.emoji-icon ↕️
span.workspace-drag-handle ↕️
a.js-select-workspace(data-id="{{id}}")
span.workspace-icon
if icon
+viewer
= icon
else
span.emoji-icon 📁
| 📁
span.workspace-name= name
a.js-edit-workspace(data-id="{{id}}" title="{{_ 'allboards.edit-workspace'}}")
span.emoji-icon ✏️
a.js-edit-workspace(data-id="{{id}}" title="{{_ 'allboards.edit-workspace'}}") ✏️
span.workspace-count {{workspaceCount id}}
a.js-add-subworkspace(data-id="{{id}}" title="{{_ 'allboards.add-subworkspace'}}") +
if children

View file

@ -232,18 +232,20 @@ BlazeComponent.extendComponent({
},
boards() {
let query = {
// { type: 'board' },
// { type: { $in: ['board','template-container'] } },
$and: [
{ archived: false },
{ type: { $in: ['board', 'template-container'] } },
{ $or: [] },
{ title: { $not: { $regex: /^\^.*\^$/ } } }
]
};
const membershipOrs = [];
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
if (FlowRouter.getRouteName() === 'home') {
membershipOrs.push({ 'members.userId': Meteor.userId() });
query.$and[2].$or.push({ 'members.userId': Meteor.userId() });
if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue) {
query.$and.push({ 'permission': 'private' });
@ -258,7 +260,7 @@ BlazeComponent.extendComponent({
// }
//query.$and[2].$or.push({'orgs': {$elemMatch : {orgId: orgsIds[0]}}});
membershipOrs.push({ 'orgs.orgId': { $in: orgsIds } });
query.$and[2].$or.push({ 'orgs.orgId': { $in: orgsIds } });
}
let teamIdsUserBelongs = currUser?.teamIdsUserBelongs() || '';
@ -268,11 +270,8 @@ BlazeComponent.extendComponent({
// query.$or[2].$or.push({'teams.teamId': teamsIds[i]});
// }
//query.$and[2].$or.push({'teams': { $elemMatch : {teamId: teamsIds[0]}}});
membershipOrs.push({ 'teams.teamId': { $in: teamsIds } });
query.$and[2].$or.push({ 'teams.teamId': { $in: teamsIds } });
}
if (membershipOrs.length) {
query.$and.splice(2, 0, { $or: membershipOrs });
}
}
else if (allowPrivateVisibilityOnly !== undefined && !allowPrivateVisibilityOnly.booleanValue) {
query = {
@ -546,18 +545,15 @@ BlazeComponent.extendComponent({
const query = {
$and: [
{ archived: false },
{ type: 'board' }
{ type: 'board' },
{ $or: [] }
]
};
const ors = [];
if (selectedTeamsValues.length > 0) {
ors.push({ 'teams.teamId': { $in: selectedTeamsValues } });
query.$and[2].$or.push({ 'teams.teamId': { $in: selectedTeamsValues } });
}
if (selectedOrgsValues.length > 0) {
ors.push({ 'orgs.orgId': { $in: selectedOrgsValues } });
}
if (ors.length) {
query.$and.push({ $or: ors });
query.$and[2].$or.push({ 'orgs.orgId': { $in: selectedOrgsValues } });
}
let filteredBoards = ReactiveCache.getBoards(query, {});

View file

@ -3,6 +3,6 @@ template(name="miniboard")
class="minicard-{{colorClass}}")
.minicard-title
.handle
span.drag-handle(title="{{_ 'dragBoard'}}") ↕️
.fa.fa-arrows
+viewer
= title

View file

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

View file

@ -55,9 +55,10 @@ template(name="cardCustomField-number")
template(name="cardCustomField-checkbox")
.js-checklist-item.checklist-item(class="{{#if data.value }}is-checked{{/if}}")
if canModifyCard
span.check-box-unicode {{#if data.value }}✅{{else}}⬜{{/if}}
.check-box-container
.check-box.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
else
span.check-box-unicode {{#if data.value }}✅{{else}}⬜{{/if}}
.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
template(name="cardCustomField-currency")
if canModifyCard

View file

@ -112,7 +112,6 @@ CardCustomField.register('cardCustomField');
events() {
return [
{
'click .js-checklist-item .check-box-unicode': this.toggleItem,
'click .js-checklist-item .check-box-container': this.toggleItem,
},
];

View file

@ -31,8 +31,8 @@
display: block;
position: relative;
float: left;
height: clamp(24px, 3.5vw, 36px);
width: clamp(24px, 3.5vw, 36px);
height: 30px;
width: 30px;
margin: .3vh;
cursor: pointer;
user-select: none;
@ -118,65 +118,6 @@
transition: flex-basis 0.1s;
box-sizing: border-box;
}
/* Desktop mode: position card below board header */
body.desktop-mode .card-details:not(.card-details-popup) {
position: fixed;
width: auto;
max-width: 800px;
flex-basis: auto;
border-radius: 8px;
z-index: 100;
}
/* 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;
left: 20px;
right: 20px;
bottom: 20px;
}
/* Stagger positions for multiple cards using nth-of-type */
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(1) {
top: 50px;
left: 20px;
}
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(2) {
top: 80px;
left: 50px;
}
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(3) {
top: 110px;
left: 80px;
}
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(4) {
top: 140px;
left: 110px;
}
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(5) {
top: 170px;
left: 140px;
}
/* For expanded cards, set dimensions */
body.desktop-mode .card-details:not(.card-details-popup):not(.card-details-collapsed) {
right: 20px;
bottom: 20px;
}
/* Collapsed card state - hide content and set height to title row only */
.card-details.card-details-collapsed .card-details-canvas > *:not(.card-details-header) {
display: none;
}
.card-details.card-details-collapsed {
height: auto !important;
bottom: auto !important;
overflow: visible;
}
body.desktop-mode .card-details.card-details-collapsed {
bottom: auto !important;
}
.card-details .mCustomScrollBox {
padding-left: 0;
}
@ -198,30 +139,6 @@ body.desktop-mode .card-details.card-details-collapsed {
display: inline-block;
margin-right: 5px;
}
/* Collapse toggle triangle */
.card-details .card-details-header .card-collapse-toggle {
float: left;
font-size: 20px;
padding: 7px 10px;
margin-left: -10px;
margin-right: 5px;
cursor: pointer;
user-select: none;
color: #000;
}
/* Drag handle */
.card-details .card-details-header .card-drag-handle {
font-size: 20px;
padding: 8px 10px;
margin-right: 10px;
cursor: move;
user-select: none;
display: inline-block;
float: right;
}
.card-details .card-details-header .close-card-details,
.card-details .card-details-header .maximize-card-details,
.card-details .card-details-header .minimize-card-details,
@ -239,16 +156,11 @@ body.desktop-mode .card-details.card-details-collapsed {
font-size: 24px;
padding: 5px 10px 5px 10px;
margin-right: -8px;
cursor: pointer;
user-select: none;
}
.card-details .card-details-header .close-card-details-mobile-web,
.card-details .card-details-header .card-mobile-desktop-toggle {
.card-details .card-details-header .close-card-details-mobile-web {
font-size: 24px;
padding: 5px;
margin-right: 5px;
cursor: pointer;
user-select: none;
margin-right: 40px;
}
.card-details .card-details-header .card-copy-button {
font-size: 17px;
@ -269,36 +181,6 @@ body.desktop-mode .card-details.card-details-collapsed {
padding: 10px;
margin-right: 30px;
}
.card-details .card-details-header .card-mobile-desktop-toggle,
.card-details .card-details-header .card-zoom-in,
.card-details .card-details-header .card-zoom-out {
font-size: 24px;
padding: 5px 10px 5px 10px;
margin-right: 5px;
cursor: pointer;
user-select: none;
float: right;
}
/* Unify all card text to match title size */
.card-details {
font-size: 1em;
}
.card-details p,
.card-details span,
.card-details div,
.card-details a,
.card-details label,
.card-details input,
.card-details textarea,
.card-details select,
.card-details button,
.card-details .card-details-item-title,
.card-details .card-label,
.card-details .viewer {
font-size: inherit;
line-height: 1.4;
}
.card-details .card-details-header .card-details-watch {
font-size: 17px;
padding-left: 7px;
@ -402,19 +284,6 @@ body.desktop-mode .card-details.card-details-collapsed {
position: fixed;
resize: both;
}
/* Override for mobile mode even on larger screens */
body.mobile-mode .card-details {
width: 100vw !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
height: 100vh !important;
max-height: 100vh !important;
resize: none !important;
}
.card-details-maximized {
padding: 0;
flex-shrink: 0;
@ -466,53 +335,19 @@ input[type="submit"].attachment-add-link-submit {
}
@media screen and (max-width: 800px) {
.card-details {
width: 100% !important;
padding: 0px 0px 0px 0px !important;
margin: 0px !important;
width: calc(100% - 1px);
padding: 0px 20px 0px 20px;
margin: 0px;
transition: none;
overflow-y: auto;
overflow-x: hidden;
/* iOS Safari specific fixes */
-webkit-overflow-scrolling: touch;
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
z-index: 100 !important;
height: 100vh !important;
max-height: 100vh !important;
border-radius: 0 !important;
box-shadow: none !important;
}
/* Ensure card details are above everything on mobile */
body.mobile-mode .card-details {
z-index: 100 !important;
width: 100vw !important;
left: 0 !important;
right: 0 !important;
overflow-y: revert;
overflow-x: revert;
}
.card-details .card-details-canvas {
width: 100%;
padding-left: 0px;
padding: 0 15px;
}
.card-details .card-details-header .close-card-details {
margin-right: 0px;
display: block !important;
}
.card-details .card-details-header .close-card-details-mobile-web {
display: block !important;
margin-right: 5px !important;
}
.card-details .card-details-header .card-mobile-desktop-toggle {
display: block !important;
margin-right: 5px !important;
}
.card-details .card-details-header .card-mobile-desktop-toggle {
display: block !important;
margin-right: 5px !important;
}
.card-details .card-details-header .card-details-menu {
margin-right: 40px;
@ -538,62 +373,6 @@ input[type="submit"].attachment-add-link-submit {
.pop-over > .content-wrapper > .popup-container-depth-0 .card-details-header {
margin: 0;
}
/* iPhone mobile: enlarge header buttons and increase spacing */
body.mobile-mode.iphone-device .card-details .card-details-header {
padding-right: 16px;
}
body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details,
body.mobile-mode.iphone-device .card-details .card-details-header .maximize-card-details,
body.mobile-mode.iphone-device .card-details .card-details-header .minimize-card-details,
body.mobile-mode.iphone-device .card-details .card-details-header .card-details-menu-mobile-web,
body.mobile-mode.iphone-device .card-details .card-details-header .card-copy-mobile-button,
body.mobile-mode.iphone-device .card-details .card-details-header .card-mobile-desktop-toggle,
body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-in,
body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-out {
font-size: 2em !important; /* 2x bigger */
padding: 0.3em !important;
margin-right: 0.75em !important; /* 2x space compared to default */
margin-left: 0 !important;
}
/* Avoid clipping of the close button on the right edge */
body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details {
margin-right: 0.75em !important;
}
/* Enlarge the header title too */
body.mobile-mode.iphone-device .card-details .card-details-header .card-details-title {
font-size: 1.2em !important;
font-weight: bold;
}
}
/* Mobile mode styles - apply when body has mobile-mode class regardless of screen size */
body.mobile-mode .card-details {
width: 100vw !important;
padding: 0px !important;
margin: 0px !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
z-index: 100 !important;
height: 100vh !important;
max-height: 100vh !important;
border-radius: 0 !important;
box-shadow: none !important;
overflow-y: auto !important;
overflow-x: hidden !important;
-webkit-overflow-scrolling: touch;
}
body.mobile-mode .card-details .card-details-canvas {
width: 100% !important;
padding: 0 15px !important;
}
body.mobile-mode .card-details .card-details-header .close-card-details,
body.mobile-mode .card-details .card-details-header .close-card-details-mobile-web {
display: block !important;
}
.card-details-white {
background: #fff !important;

View file

@ -5,18 +5,13 @@ template(name="cardDetails")
+attachmentViewer
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}' class='{{#if cardCollapsed}}card-details-collapsed{{/if}}'): .card-details-canvas
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}'): .card-details-canvas
.card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
+inlinedForm(classNames="js-card-details-title")
+editCardTitleForm
else
unless isMiniScreen
unless isPopup
span.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
if cardCollapsed
| ▶
else
| 🔽
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
| ❌
if canModifyCard
@ -31,40 +26,24 @@ template(name="cardDetails")
| ☰
a.card-copy-button.js-copy-link(
id="cardURL_copy"
class="fa-link"
title="{{_ 'copy-card-link-to-clipboard'}}"
href="{{ originRelativeUrl }}"
)
span.emoji-icon 🔗
span.card-drag-handle.js-card-drag-handle(title="Drag card")
| ↕️
span.copied-tooltip {{_ 'copied'}}
else
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
| ❌
a.card-zoom-out.js-card-zoom-out(title="{{_ 'zoom-out'}}")
| 🔍➖
a.card-zoom-in.js-card-zoom-in(title="{{_ 'zoom-in'}}")
| 🔍➕
a.card-mobile-desktop-toggle.js-card-mobile-desktop-toggle(title="{{_ 'mobile-desktop-toggle'}}")
if mobileMode
| 🖥️
else
| 📱
unless isPopup
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
| ❌
if canModifyCard
if cardMaximized
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
| 🔽
else
a.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
| 🔼
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
| ☰
a.card-copy-mobile-button.js-copy-link(
id="cardURL_copy"
class="fa-link"
title="{{_ 'copy-card-link-to-clipboard'}}"
href="{{ originRelativeUrl }}"
)
span.emoji-icon 🔗
span.copied-tooltip {{_ 'copied'}}
h2.card-details-title.js-card-title(
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
@ -325,7 +304,7 @@ template(name="cardDetails")
hr
.card-details-item.card-details-item-customfield
h3.card-details-item-title
| 📋
| 📋-alt
= definition.name
+cardCustomField
@ -699,7 +678,7 @@ template(name="cardDetailsActionsPopup")
| 👁️
| {{_ 'unwatch'}}
else
| 👁️
| 👁️-slash
| {{_ 'watch'}}
hr
if canModifyCard
@ -719,7 +698,7 @@ template(name="cardDetailsActionsPopup")
if currentUser.isBoardAdmin
li
a.js-custom-fields
| 📋
| 📋-alt
| {{_ 'card-edit-custom-fields'}}
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
@ -739,7 +718,7 @@ template(name="cardDetailsActionsPopup")
| 👁️
| {{_ 'hide-list-on-minicard'}}
else
| 👁️
| 👁️-slash
| {{_ 'show-list-on-minicard'}}
hr
ul.pop-over-list
@ -788,7 +767,7 @@ template(name="cardDetailsActionsPopup")
ul.pop-over-list
li
a.js-more
span.emoji-icon 🔗
| 🔗
| {{_ 'cardMorePopup-title'}}
template(name="exportCardPopup")
@ -824,29 +803,17 @@ template(name="copyAndMoveCard")
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each boards
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{title}}
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
each swimlanes
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'cards'}}:
select.js-select-cards
each cards
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
div
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
label(for="position-above") {{_ 'above-selected-card'}}
div
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
label(for="position-below") {{_ 'below-selected-card'}}
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{title}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}

View file

@ -31,7 +31,6 @@ import CardComments from '/models/cardComments';
import { ALLOWED_COLORS } from '/config/const';
import { UserAvatar } from '../users/userAvatar';
import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList';
import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard';
import { handleFileUpload } from './attachments';
import uploadProgressManager from '../../lib/uploadProgressManager';
@ -64,11 +63,7 @@ BlazeComponent.extendComponent({
const boardBody = this.parentComponent().parentComponent();
//in Miniview parent is Board, not BoardBody.
if (boardBody !== null) {
// Only show overlay in mobile mode, not in desktop mode
const isMobile = Utils.getMobileMode();
if (isMobile) {
boardBody.showOverlay.set(true);
}
boardBody.showOverlay.set(true);
boardBody.mouseHasEnterCardDetails = false;
}
}
@ -86,7 +81,6 @@ BlazeComponent.extendComponent({
isWatching() {
const card = this.currentData();
if (!card || typeof card.findWatcher !== 'function') return false;
return card.findWatcher(Meteor.userId());
},
@ -99,18 +93,6 @@ BlazeComponent.extendComponent({
return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized();
},
cardCollapsed() {
const user = ReactiveCache.getCurrentUser();
if (user && user.profile) {
return !!user.profile.cardCollapsed;
}
if (Users.getPublicCardCollapsed) {
const stored = Users.getPublicCardCollapsed();
if (typeof stored === 'boolean') return stored;
}
return false;
},
presentParentTask() {
let result = this.currentBoard.presentParentTask;
if (result === null || result === undefined) {
@ -163,9 +145,8 @@ BlazeComponent.extendComponent({
* @return is the list id the current list id ?
*/
isCurrentListId(listId) {
const data = this.data();
if (!data || typeof data.listId === 'undefined') return false;
return data.listId == listId;
const ret = this.data().listId == listId;
return ret;
},
onRendered() {
@ -315,74 +296,8 @@ BlazeComponent.extendComponent({
return [
{
...events,
'click .js-card-collapse-toggle'() {
const user = ReactiveCache.getCurrentUser();
const currentState = user && user.profile ? !!user.profile.cardCollapsed : !!Users.getPublicCardCollapsed();
if (user) {
Meteor.call('setCardCollapsed', !currentState);
} else if (Users.setPublicCardCollapsed) {
Users.setPublicCardCollapsed(!currentState);
}
},
'mousedown .js-card-drag-handle'(event) {
event.preventDefault();
const $card = $(event.target).closest('.card-details');
const startX = event.clientX;
const startY = event.clientY;
const startLeft = $card.offset().left;
const startTop = $card.offset().top;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
$card.css({
left: startLeft + deltaX + 'px',
top: startTop + deltaY + 'px'
});
};
const onMouseUp = () => {
$(document).off('mousemove', onMouseMove);
$(document).off('mouseup', onMouseUp);
};
$(document).on('mousemove', onMouseMove);
$(document).on('mouseup', onMouseUp);
},
'click .js-close-card-details'() {
// Get board ID from either the card data or current board in session
const card = this.currentData() || this.data();
const boardId = (card && card.boardId) || Utils.getCurrentBoard()._id;
const cardId = card && card._id;
if (boardId) {
// In desktop mode, remove from openCards array
const isMobile = Utils.getMobileMode();
if (!isMobile && cardId) {
const openCards = Session.get('openCards') || [];
const filtered = openCards.filter(id => id !== cardId);
Session.set('openCards', filtered);
// If this was the current card, clear it
if (Session.get('currentCard') === cardId) {
Session.set('currentCard', null);
}
// Don't navigate away in desktop mode - just close the card
return;
}
// Mobile mode: Clear the current card session to close the card
Session.set('currentCard', null);
// Navigate back to board without card
const board = ReactiveCache.getBoard(boardId);
if (board) {
FlowRouter.go('board', {
id: board._id,
slug: board.slug,
});
}
}
Utils.goBoardId(this.data().boardId);
},
'click .js-copy-link'(event) {
event.preventDefault();
@ -396,34 +311,6 @@ BlazeComponent.extendComponent({
Meteor.call('changeDateFormat', dateFormat);
},
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
// Mobile: switch to desktop popup view (maximize)
'click .js-mobile-switch-to-desktop'(event) {
event.preventDefault();
// Switch global mode to desktop so the card appears as desktop popup
Utils.setMobileMode(false);
},
'click .js-card-zoom-in'(event) {
event.preventDefault();
const current = Utils.getCardZoom();
const newZoom = Math.min(3.0, current + 0.1);
Utils.setCardZoom(newZoom);
},
'click .js-card-zoom-out'(event) {
event.preventDefault();
const current = Utils.getCardZoom();
const newZoom = Math.max(0.5, current - 0.1);
Utils.setCardZoom(newZoom);
},
'click .js-card-mobile-desktop-toggle'(event) {
event.preventDefault();
const currentMode = Utils.getMobileMode();
Utils.setMobileMode(!currentMode);
},
'click .js-card-mobile-desktop-toggle'(event) {
event.preventDefault();
const currentMode = Utils.getMobileMode();
Utils.setMobileMode(!currentMode);
},
'submit .js-card-description'(event) {
event.preventDefault();
const description = this.currentComponent().getValue();
@ -796,7 +683,6 @@ Template.editCardSortOrderForm.onRendered(function () {
Template.cardDetailsActionsPopup.helpers({
isWatching() {
if (!this || typeof this.findWatcher !== 'function') return false;
return this.findWatcher(Meteor.userId());
},
@ -950,42 +836,26 @@ Template.editCardAssignerForm.events({
});
/** Move Card Dialog */
(class extends DialogWithBoardSwimlaneListCard {
(class extends DialogWithBoardSwimlaneList {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret;
}
setDone(cardId, options) {
setDone(boardId, swimlaneId, listId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data();
let sortIndex = 0;
if (cardId) {
const targetCard = ReactiveCache.getCard(cardId);
if (targetCard) {
const position = this.$('input[name="position"]:checked').val();
if (position === 'above') {
sortIndex = targetCard.sort - 0.5;
} else {
sortIndex = targetCard.sort + 0.5;
}
}
} else {
// If no card selected, move to end
sortIndex = card.getMaxSort(options.listId, options.swimlaneId) + 1;
}
card.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
const minOrder = card.getMinSort(listId, swimlaneId);
card.move(boardId, swimlaneId, listId, minOrder - 1);
}
}).register('moveCardPopup');
/** Copy Card Dialog */
(class extends DialogWithBoardSwimlaneListCard {
(class extends DialogWithBoardSwimlaneList {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret;
}
setDone(cardId, options) {
setDone(boardId, swimlaneId, listId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data();
@ -994,30 +864,8 @@ Template.editCardAssignerForm.events({
const title = textarea.val().trim();
if (title) {
const newCardId = Meteor.call('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, true, {title: title});
// Position the copied card
if (newCardId) {
const newCard = ReactiveCache.getCard(newCardId);
let sortIndex = 0;
if (cardId) {
const targetCard = ReactiveCache.getCard(cardId);
if (targetCard) {
const position = this.$('input[name="position"]:checked').val();
if (position === 'above') {
sortIndex = targetCard.sort - 0.5;
} else {
sortIndex = targetCard.sort + 0.5;
}
}
} else {
// If no card selected, copy to end
sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1;
}
newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
}
// insert new card to the top of new list
const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, listId, true, {title: title});
// In case the filter is active we need to add the newly inserted card in
// the list of exceptions -- cards that are not filtered. Otherwise the
@ -1029,12 +877,12 @@ Template.editCardAssignerForm.events({
}).register('copyCardPopup');
/** Convert Checklist-Item to card dialog */
(class extends DialogWithBoardSwimlaneListCard {
(class extends DialogWithBoardSwimlaneList {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret;
}
setDone(cardId, options) {
setDone(boardId, swimlaneId, listId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data();
@ -1044,29 +892,14 @@ Template.editCardAssignerForm.events({
if (title) {
const _id = Cards.insert({
title: title,
listId: options.listId,
boardId: options.boardId,
swimlaneId: options.swimlaneId,
listId: listId,
boardId: boardId,
swimlaneId: swimlaneId,
sort: 0,
});
const newCard = ReactiveCache.getCard(_id);
let sortIndex = 0;
if (cardId) {
const targetCard = ReactiveCache.getCard(cardId);
if (targetCard) {
const position = this.$('input[name="position"]:checked').val();
if (position === 'above') {
sortIndex = targetCard.sort - 0.5;
} else {
sortIndex = targetCard.sort + 0.5;
}
}
} else {
sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1;
}
newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
const card = ReactiveCache.getCard(_id);
const minOrder = card.getMinSort();
card.move(card.boardId, card.swimlaneId, card.listId, minOrder - 1);
Filter.addException(_id);
}
@ -1074,12 +907,12 @@ Template.editCardAssignerForm.events({
}).register('convertChecklistItemToCardPopup');
/** Copy many cards dialog */
(class extends DialogWithBoardSwimlaneListCard {
(class extends DialogWithBoardSwimlaneList {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret;
}
setDone(cardId, options) {
setDone(boardId, swimlaneId, listId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data();
@ -1089,29 +922,7 @@ Template.editCardAssignerForm.events({
if (title) {
const titleList = JSON.parse(title);
for (const obj of titleList) {
const newCardId = Meteor.call('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, false, {title: obj.title, description: obj.description});
// Position the copied card
if (newCardId) {
const newCard = ReactiveCache.getCard(newCardId);
let sortIndex = 0;
if (cardId) {
const targetCard = ReactiveCache.getCard(cardId);
if (targetCard) {
const position = this.$('input[name="position"]:checked').val();
if (position === 'above') {
sortIndex = targetCard.sort - 0.5;
} else {
sortIndex = targetCard.sort + 0.5;
}
}
} else {
sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1;
}
newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
}
const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, listId, false, {title: obj.title, description: obj.description});
// In case the filter is active we need to add the newly inserted card in
// the list of exceptions -- cards that are not filtered. Otherwise the
@ -1161,51 +972,6 @@ BlazeComponent.extendComponent({
},
}).register('setCardColorPopup');
BlazeComponent.extendComponent({
onCreated() {
this.currentColor = new ReactiveVar(null);
},
colors() {
return ALLOWED_COLORS.map((color) => ({ color, name: '' }));
},
isSelected(color) {
return this.currentColor.get() === color;
},
events() {
return [
{
'click .js-palette-color'(event) {
// Extract color from class name like "card-details-red"
const classes = $(event.currentTarget).attr('class').split(' ');
const colorClass = classes.find(cls => cls.startsWith('card-details-'));
const color = colorClass ? colorClass.replace('card-details-', '') : null;
this.currentColor.set(color);
},
'click .js-submit'(event) {
event.preventDefault();
const color = this.currentColor.get();
// Use MultiSelection to get selected cards and set color on each
ReactiveCache.getCards(MultiSelection.getMongoSelector()).forEach(card => {
card.setColor(color);
});
Popup.back();
},
'click .js-remove-color'(event) {
event.preventDefault();
// Use MultiSelection to get selected cards and remove color from each
ReactiveCache.getCards(MultiSelection.getMongoSelector()).forEach(card => {
card.setColor(null);
});
Popup.back();
},
},
];
},
}).register('setSelectionColorPopup');
BlazeComponent.extendComponent({
onCreated() {
this.currentCard = this.currentData();

View file

@ -37,23 +37,14 @@ textarea.js-edit-checklist-item {
.checklist-progress-bar-container .checklist-progress-bar {
width: 80%;
height: 10px;
background-color: #d6ebff !important;
border-radius: 16px;
}
.checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
color: #fff !important;
background-color: #3cb500 !important;
background-color: #2196f3 !important;
padding: 0.01em 16px;
border-radius: 16px;
height: 100%;
}
/* Grey progress bar when grey icons setting is enabled */
body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-bar {
background-color: #d9d9d9;
}
body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
background-color: #7a7a7a !important;
}
.checklist-title {
padding: 10px;
}
@ -76,14 +67,14 @@ body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-ba
.checklist-title .checklist-stat.is-finished {
color: #3cb500;
}
.checklist-title span.checklist-handle {
.checklist-title span.fa.checklist-handle {
padding-right: 20px;
padding-top: 3px;
float: left;
display: inline-block;
width: 1.2em;
text-align: center;
color: #999;
}
.checklist-title span.fa.checklist-handle.fa-arrows::before {
content: "↕️" !important;
font-family: inherit !important;
}
#card-details-overlay {
top: 0;
@ -114,25 +105,6 @@ body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-ba
height: auto;
overflow: hidden;
}
/* iPhone mobile: larger checklist titles and more spacing between items */
body.mobile-mode.iphone-device .checklist-title .title {
font-size: 1.3em !important;
font-weight: bold;
}
body.mobile-mode.iphone-device .checklist-item {
margin-top: 12px !important;
margin-bottom: 8px !important;
padding: 8px 4px !important;
min-height: 44px; /* iOS recommended touch target size */
}
body.mobile-mode.iphone-device .checklist-item span.checklistitem-handle {
font-size: 1.5em !important;
padding-right: 15px !important;
width: 1.5em !important;
}
.checklist-item.is-checked.invisible {
opacity: 0;
height: 0;
@ -162,27 +134,6 @@ body.mobile-mode.iphone-device .checklist-item span.checklistitem-handle {
border-bottom: 2px solid #3cb500;
border-right: 2px solid #3cb500;
}
/* Unicode checkbox icons styling */
.checklist-item .check-box-unicode,
.cardCustomField-checkbox .check-box-unicode {
font-size: 1.3em;
margin-right: 8px;
cursor: pointer;
display: inline-block;
vertical-align: middle;
line-height: 1;
}
/* Grey checkmarks when grey icons setting is enabled */
body.grey-icons-enabled .checklist-item .check-box.is-checked {
border-bottom: 2px solid #7a7a7a;
border-right: 2px solid #7a7a7a;
}
body.grey-icons-enabled .checklist-item .check-box-unicode,
body.grey-icons-enabled .cardCustomField-checkbox .check-box-unicode {
filter: grayscale(100%);
-webkit-filter: grayscale(100%);
opacity: 0.85;
}
.checklist-item .item-title {
flex: 1;
}
@ -197,14 +148,13 @@ body.grey-icons-enabled .cardCustomField-checkbox .check-box-unicode {
word-wrap: break-word;
max-width: 420px;
}
.checklist-item span.checklistitem-handle {
.checklist-item span.fa.checklistitem-handle {
padding-top: 2px;
padding-right: 10px;
display: inline-block;
width: 1.2em;
text-align: center;
color: #999;
cursor: pointer;
}
.checklist-item span.fa.checklistitem-handle.fa-arrows::before {
content: "↕️" !important;
font-family: inherit !important;
}
.js-delete-checklist-item,
.js-convert-checklist-item-to-card {

View file

@ -9,10 +9,19 @@ template(name="checklists")
else
a.add-checklist-top.js-open-inlined-form(title="{{_ 'add-checklist'}}")
|
if currentUser.isBoardMember
.material-toggle-switch(title="{{_ 'hide-finished-checklist'}}")
//span.toggle-switch-title
if card.hideFinishedChecklistIfItemsAreHidden
input.toggle-switch(type="checkbox" id="toggleHideFinishedChecklist" checked="checked")
else
input.toggle-switch(type="checkbox" id="toggleHideFinishedChecklist")
label.toggle-label(for="toggleHideFinishedChecklist")
.card-checklist-items
each checklist in checklists
+checklistDetail(checklist = checklist card = card)
if checklist.showChecklist card.hideFinishedChecklistIfItemsAreHidden
+checklistDetail(checklist = checklist card = card)
if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId)
@ -29,12 +38,12 @@ template(name="checklistDetail")
.checklist-title
span
if canModifyCard
a.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
a.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
if canModifyCard
h4.title.js-open-inlined-form.is-editable
if isTouchScreenOrShowDesktopDragHandles
span.checklist-handle(title="{{_ 'dragChecklist'}}") ↕️
span.fa.checklist-handle(class="fa-arrows" title="{{_ 'dragChecklist'}}")
+viewer
= checklist.title
else
@ -53,10 +62,6 @@ template(name="checklistDeletePopup")
p {{_ 'confirm-checklist-delete-popup'}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
template(name="checklistItemDeletePopup")
p {{_ 'confirm-checklist-delete-popup'}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
template(name="addChecklistItemForm")
a(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
@ -90,13 +95,10 @@ template(name="editChecklistItemForm")
| ❌
span(title=createdAt) {{ moment createdAt }}
if canModifyCard
if $eq type 'item'
a.js-delete-checklist-item {{_ "delete"}}...
a.js-convert-checklist-item-to-card
| 📋
| {{_ 'convertChecklistItemToCardPopup-title'}}
else
a.js-delete-checklist {{_ "delete"}}...
a.js-delete-checklist-item {{_ "delete"}}...
a.js-convert-checklist-item-to-card
| 📋
| {{_ 'convertChecklistItemToCardPopup-title'}}
template(name="checklistItems")
if checklist.items.length
@ -123,13 +125,14 @@ template(name='checklistItemDetail')
.js-checklist-item.checklist-item(class="{{#if item.isFinished }}is-checked{{#if checklist.hideCheckedChecklistItems}} invisible{{/if}}{{/if}}{{#if checklist.hideAllChecklistItems}} is-checked invisible{{/if}}"
role="checkbox" aria-checked="{{#if item.isFinished }}true{{else}}false{{/if}}" tabindex="0")
if canModifyCard
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
span.checklistitem-handle(title="{{_ 'dragChecklistItem'}}") ↕️
.check-box-container
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer
= item.title
else
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer
= item.title
@ -164,62 +167,34 @@ template(name="checklistActionsPopup")
else
input.toggle-switch(type="checkbox" id="toggleHideAllChecklistItems_{{checklist._id}}")
label.toggle-label(for="toggleHideAllChecklistItems_{{checklist._id}}")
a.js-toggle-show-checklist-at-minicard
| 📋
| {{_ "showChecklistAtMinicard"}} ...
.material-toggle-switch(title="{{_ 'showChecklistAtMinicard'}}")
if checklist.showChecklistAtMinicard
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}" checked="checked")
else
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}")
label.toggle-label(for="toggleShowChecklistAtMinicard_{{checklist._id}}")
template(name="copyChecklistPopup")
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
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}}. {{title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'card'}}:
select.js-select-cards
each cards
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
+copyAndMoveChecklist
template(name="moveChecklistPopup")
+copyAndMoveChecklist
template(name="copyAndMoveChecklist")
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each boards
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{title}}
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
each swimlanes
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{title}}
label {{_ 'card'}}:
label {{_ 'cards'}}:
select.js-select-cards
each cards
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{title}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}

View file

@ -65,7 +65,7 @@ BlazeComponent.extendComponent({
$(self.itemsDom).sortable('option', 'disabled', !userIsMember());
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$(self.itemsDom).sortable({
handle: 'span.checklistitem-handle',
handle: 'span.fa.checklistitem-handle',
});
}
}
@ -157,6 +157,14 @@ BlazeComponent.extendComponent({
textarea.focus();
},
deleteItem() {
const checklist = this.currentData().checklist;
const item = this.currentData().item;
if (checklist && item && item._id) {
ChecklistItems.remove(item._id);
}
},
editChecklist(event) {
event.preventDefault();
const textarea = this.find('textarea.js-edit-checklist-item');
@ -208,28 +216,14 @@ BlazeComponent.extendComponent({
'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'(event) {
const item = this.currentData().item;
const confirmFunc = Popup.afterConfirm('checklistItemDelete', function () {
if (item && item._id) {
ChecklistItems.remove(item._id);
}
});
confirmFunc.call(this, event);
},
'click .js-delete-checklist'(event) {
const checklist = this.currentData().checklist;
const confirmFunc = Popup.afterConfirm('checklistDelete', function () {
Popup.back(2);
if (checklist && checklist._id) {
Checklists.remove(checklist._id);
}
});
confirmFunc.call(this, event);
},
'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,
},
];
@ -281,8 +275,8 @@ BlazeComponent.extendComponent({
Template.checklists.helpers({
checklists() {
const card = ReactiveCache.getCard(this.cardId);
if (!card || typeof card.checklists !== 'function') return [];
return card.checklists();
const ret = card.checklists();
return ret;
},
});
@ -309,16 +303,13 @@ BlazeComponent.extendComponent({
events() {
return [
{
'click .js-delete-checklist'(event) {
const checklist = this.data().checklist;
const confirmFunc = Popup.afterConfirm('checklistDelete', function () {
Popup.back(2);
if (checklist && checklist._id) {
Checklists.remove(checklist._id);
}
});
confirmFunc.call(this, event);
},
'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) {
@ -331,12 +322,6 @@ BlazeComponent.extendComponent({
this.data().checklist.toggleHideAllChecklistItems();
Popup.back();
},
'click .js-toggle-show-checklist-at-minicard'(event) {
event.preventDefault();
const checklist = this.data().checklist;
checklist.toggleShowChecklistAtMinicard();
Popup.back();
},
}
]
}
@ -375,7 +360,6 @@ BlazeComponent.extendComponent({
events() {
return [
{
'click .js-checklist-item .check-box-unicode': this.toggleItem,
'click .js-checklist-item .check-box-container': this.toggleItem,
},
];
@ -390,12 +374,7 @@ BlazeComponent.extendComponent({
}
setDone(cardId, options) {
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
const checklist = this.data().checklist;
Meteor.call('moveChecklist', checklist._id, cardId, (error) => {
if (error) {
console.error('Error moving checklist:', error);
}
});
this.data().checklist.move(cardId);
}
}).register('moveChecklistPopup');

View file

@ -223,13 +223,9 @@
.card-label-edit-button:hover {
background: #dbdbdb;
}
ul.edit-labels-pop-over span.label-handle {
ul.edit-labels-pop-over span.fa.label-handle {
padding-right: 10px;
display: inline-block;
width: 1.2em;
text-align: center;
color: #999;
}
ul.edit-labels-pop-over span.label-handle + .card-label {
ul.edit-labels-pop-over span.fa.label-handle + .card-label {
max-width: 180px;
}

View file

@ -31,7 +31,7 @@ template(name="cardLabelsPopup")
a.card-label-edit-button.js-edit-label
| ✏️
if isTouchScreenOrShowDesktopDragHandles
span.label-handle(title="{{_ 'dragLabel'}}") ↕️
span.fa.label-handle(class="fa-arrows" title="{{_ 'dragLabel'}}")
span.card-label.card-label-selectable.js-select-label.card-label-wrapper(class="card-label-{{color}}"
class="{{# if isLabelSelected ../_id }}active{{/if}}")
+viewer

View file

@ -142,12 +142,9 @@
display: block;
}
}
.minicard .handle .drag-handle {
.minicard .handle .fa-arrows {
font-size: clamp(16px, 3vw, 20px);
color: #ccc;
display: inline-block;
width: 1.4em;
text-align: center;
}
.minicard .minicard-title .card-number {
color: #b3b3b3;
@ -300,6 +297,19 @@
background-color: #1976d2 !important;
}
/* Font Awesome icons in minicard dates */
.minicard .card-date i.fa {
margin-right: 0.3vw;
font-size: 0.9em;
vertical-align: middle;
}
/* Font Awesome icons in minicard spent time */
.minicard .card-time i.fa {
margin-right: 0.3vw;
font-size: 0.9em;
vertical-align: middle;
}
.minicard .badges {
float: left;
margin-top: 1vh;
@ -731,80 +741,7 @@
gap: 0.3vw;
}
/* Checklist display on minicard */
.minicard-checklist {
width: 100%;
margin-top: 0.5vh;
margin-bottom: 0.5vh;
padding: 0.3vh 0.5vw;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 0.3vw;
border: 1px solid #e0e0e0;
}
.minicard-checklist .checklist-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.3vh;
}
.minicard-checklist .checklist-title {
.minicard-list-name i.fa {
font-size: 0.8em;
font-weight: bold;
color: #4d4d4d;
flex: 1;
}
.minicard-checklist .checklist-menu {
font-size: 1.2em;
color: #666;
cursor: pointer;
padding: 0 0.3vw;
border-radius: 0.2vw;
transition: background-color 0.2s;
}
.minicard-checklist .checklist-menu:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.minicard-checklist .checklist-item {
font-size: 0.75em;
color: #666;
margin-bottom: 0.2vh;
display: flex;
align-items: flex-start;
gap: 0.3vw;
line-height: 1.2;
cursor: pointer;
padding: 0.2vh 0;
border-radius: 0.2vw;
transition: background-color 0.2s;
}
.minicard-checklist .checklist-item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.minicard-checklist .checklist-item.is-checked {
text-decoration: line-through;
color: #999;
}
.minicard-checklist .checklist-item .check-box-unicode {
flex-shrink: 0;
font-size: 0.8em;
margin-top: 0.1vh;
transition: transform 0.2s;
}
.minicard-checklist .checklist-item:hover .check-box-unicode {
transform: scale(1.1);
}
.minicard-checklist .checklist-item .item-title {
flex: 1;
word-wrap: break-word;
overflow-wrap: break-word;
opacity: 0.7;
}

View file

@ -167,6 +167,10 @@ template(name="minicard")
.badge
span.badge-icon 📎
span.badge-text= attachments.length
if checklists.length
.badge(class="{{#if checklistFinished}}is-finished{{/if}}")
span.badge-icon ☑️
span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
if allSubtasks.count
.badge
span.badge-icon 🌐
@ -177,9 +181,6 @@ template(name="minicard")
.badge
span.badge-icon 🔢
span.badge-text.check-list-sort {{ sort }}
if shouldShowChecklistAtMinicard
each shouldShowChecklistAtMinicard
+minicardChecklist(checklist=. card=..)
if currentBoard.allowsDescriptionTextOnMinicard
if getDescription
.minicard-description
@ -201,12 +202,55 @@ template(name="editCardSortOrderPopup")
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-sort-popup(type="submit") {{_ 'save'}}
template(name="minicardChecklist")
.minicard-checklist
.checklist-header
.checklist-title= checklist.title
if canModifyCard
a.checklist-menu.js-open-checklist-menu(title="{{_ 'checklistActionsPopup-title'}}") ☰
each visibleItems
+checklistItemDetail(item = . checklist = checklist card = card)
template(name="minicardDetailsActionsPopup")
ul.pop-over-list
if canModifyCard
li
a.js-move-card
| ➡️
| {{_ 'moveCardPopup-title'}}
li
a.js-copy-card
| 📋
| {{_ 'copyCardPopup-title'}}
hr
li
a.js-archive
| ➡️
| 📦
| {{_ 'archive-card'}}
hr
li
a.js-move-card-to-top
| ⬆️
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
| ⬇️
| {{_ 'moveCardToBottom-title'}}
hr
li
a.js-add-labels
| 🏷️
| {{_ 'card-edit-labels'}}
li
a.js-due-date
| 📥
| {{_ 'editCardDueDatePopup-title'}}
li
a.js-set-card-color
| 🎨
| {{_ 'setCardColorPopup-title'}}
li
a.js-link
| 🔗
| {{_ 'link-card'}}
li
a.js-toggle-watch-card
if isWatching
| 👁️
| {{_ 'unwatch'}}
else
| 👁️-slash
| {{_ 'watch'}}

View file

@ -91,13 +91,6 @@ BlazeComponent.extendComponent({
}
},
toggleChecklistItem() {
const item = this.currentData();
if (item && item._id) {
item.toggleItem();
}
},
events() {
return [
{
@ -115,7 +108,7 @@ BlazeComponent.extendComponent({
},
'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': Popup.open('cardDetailsActions'),
'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
// Drag and drop file upload handlers
'dragover .minicard'(event) {
// Only prevent default for file drags to avoid interfering with sortable
@ -177,43 +170,6 @@ BlazeComponent.extendComponent({
},
}).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();
@ -253,29 +209,9 @@ Template.minicard.helpers({
// Show list name if either:
// 1. Board-wide setting is enabled, OR
// 2. This specific card has the setting enabled
const currentBoard = this.board();
const currentBoard = this.currentBoard;
if (!currentBoard) return false;
return currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
},
shouldShowChecklistAtMinicard() {
// Return checklists that should be shown on minicard
const currentBoard = this.board();
if (!currentBoard) return [];
const checklists = this.checklists();
const visibleChecklists = [];
checklists.forEach(checklist => {
// Show checklist if either:
// 1. Board-wide setting is enabled, OR
// 2. This specific checklist has the setting enabled
if (currentBoard.allowsChecklistAtMinicard || checklist.showChecklistAtMinicard) {
visibleChecklists.push(checklist);
}
});
return visibleChecklists;
}
});
@ -306,7 +242,7 @@ BlazeComponent.extendComponent({
}
}).register('editCardSortOrderPopup');
Template.cardDetailsActionsPopup.events({
Template.minicardDetailsActionsPopup.events({
'click .js-due-date': Popup.open('editCardDueDate'),
'click .js-move-card': Popup.open('moveCard'),
'click .js-copy-card': Popup.open('copyCard'),

View file

@ -11,7 +11,7 @@ template(name="resultCard")
= getBoard.title
else
.broken-cards-null
| {{_ 'no-name'}}
| NULL
if getBoard.archived
| 📦
li.result-card-context.result-card-context-separator
@ -25,7 +25,7 @@ template(name="resultCard")
= getSwimlane.title
else
.broken-cards-null
| {{_ 'no-name'}}
| NULL
if getSwimlane.archived
| 📦
li.result-card-context.result-card-context-separator
@ -39,6 +39,6 @@ template(name="resultCard")
= getList.title
else
.broken-cards-null
| {{_ 'no-name'}}
| NULL
if getList.archived
| 📦

View file

@ -87,15 +87,6 @@ textarea.js-edit-subtask-item {
top: 0;
bottom: -600px;
right: 0;
z-index: 15;
}
/* Fix for mobile Safari: ensure this doesn't block card interaction */
@media screen and (max-width: 800px) {
#card-details-overlay {
z-index: 15;
pointer-events: none;
}
}
.subtasks {
background: #f7f7f7;
@ -136,25 +127,6 @@ textarea.js-edit-subtask-item {
border-bottom: 2px solid #3cb500;
border-right: 2px solid #3cb500;
}
/* Unicode checkbox icons styling */
.subtasks-item .check-box-unicode {
font-size: 1.3em;
margin-right: 8px;
cursor: pointer;
display: inline-block;
vertical-align: middle;
line-height: 1;
}
/* Grey checkmarks when grey icons setting is enabled */
body.grey-icons-enabled .subtasks-item .check-box.is-checked {
border-bottom: 2px solid #7a7a7a;
border-right: 2px solid #7a7a7a;
}
body.grey-icons-enabled .subtasks-item .check-box-unicode {
filter: grayscale(100%);
-webkit-filter: grayscale(100%);
opacity: 0.85;
}
.subtasks-item .item-title {
flex: 1;
padding-left: 10px;

View file

@ -74,12 +74,12 @@ template(name="subtasksItems")
template(name='subtaskItemDetail')
.js-subtasks-item.subtasks-item
if canModifyCard
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer
= item.title
else
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer
= item.title

View file

@ -104,19 +104,7 @@ BlazeComponent.extendComponent({
}).register('subtasks');
BlazeComponent.extendComponent({
toggleItem() {
const item = this.currentData().item;
if (item && item._id) {
item.toggleItem();
}
},
events() {
return [
{
'click .js-subtasks-item .check-box-unicode': this.toggleItem,
},
];
},
// ...
}).register('subtaskItemDetail');
BlazeComponent.extendComponent({

View file

@ -2,17 +2,19 @@
<div class="original-position-info">
{{#if isLoading}}
<div class="original-position-loading">
Loading original position...
<i class="fa fa-spinner fa-spin"></i> Loading original position...
</div>
{{else if showOriginalPosition}}
<div class="original-position-details">
{{#if hasMovedFromOriginal}}
<div class="original-position-moved">
<span class="original-position-text"> {{getOriginalPositionDescription}}</span>
<i class="fa fa-info-circle"></i>
<span class="original-position-text">{{getOriginalPositionDescription}}</span>
</div>
{{else}}
<div class="original-position-unchanged">
<span class="original-position-text">✅ In original position</span>
<i class="fa fa-check-circle"></i>
<span class="original-position-text">In original position</span>
</div>
{{/if}}

View file

@ -315,18 +315,11 @@ textarea::-moz-placeholder {
margin-right: 6px;
border-top: 2px solid transparent;
border-left: 2px solid transparent;
border-bottom: 2px solid #3cb500;
border-right: 2px solid #3cb500;
transform: rotate(40deg);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
transform-origin: 100% 100%;
}
/* Grey checkmarks when grey icons setting is enabled */
body.grey-icons-enabled .materialCheckBox.is-checked {
border-bottom: 2px solid #7a7a7a;
border-right: 2px solid #7a7a7a;
}
.button-link {
background: #fff;
background: linear-gradient(#fff, #f5f5f5);

View file

@ -1,203 +0,0 @@
/* Gantt chart cell background colors for Received, Start, Due, End (matching cardDetails) */
.ganttview-received {
background-color: #dbdbdb !important;
color: #000 !important;
font-size: 18px !important;
font-weight: bold !important;
}
.ganttview-start {
background-color: #90ee90 !important;
color: #000 !important;
font-size: 18px !important;
font-weight: bold !important;
}
.ganttview-due {
background-color: #ffd700 !important;
color: #000 !important;
font-size: 18px !important;
font-weight: bold !important;
}
.ganttview-end {
background-color: #ffb3b3 !important;
color: #000 !important;
font-size: 18px !important;
font-weight: bold !important;
}
/* Gantt View Styles */
.gantt-view {
width: 100%;
height: auto;
overflow: visible;
background-color: #fff;
}
.gantt-view.swimlane {
background-color: #fff;
padding: 10px;
}
.gantt-container {
overflow-x: auto;
overflow-y: visible;
background-color: #fff;
display: block;
width: 100%;
}
.gantt-container table,
.gantt-table {
border-collapse: collapse;
width: 100%;
min-width: 800px;
border: 2px solid #666;
font-family: sans-serif;
font-size: 13px;
background-color: #fff;
}
.gantt-container thead {
background-color: #e8e8e8;
border-bottom: 2px solid #666;
font-weight: bold;
position: sticky;
top: 0;
z-index: 10;
}
.gantt-container thead th,
.gantt-container thead tr > td:first-child {
border-right: 2px solid #666;
padding: 4px; /* half of 8px */
width: 100px; /* half of 200px */
text-align: left;
font-weight: bold;
background-color: #e8e8e8;
min-width: 100px; /* half of 200px */
}
.gantt-container thead td {
border-right: 1px solid #999;
padding: 2px 1px; /* half */
text-align: center;
background-color: #f5f5f5;
font-size: 11px;
min-width: 15px; /* half of 30px */
font-weight: bold;
height: auto;
line-height: 1.2;
white-space: normal;
word-break: break-word;
}
.gantt-container tbody tr {
border-bottom: 1px solid #999;
height: 32px;
}
.gantt-container tbody tr:hover {
background-color: #f9f9f9;
}
.gantt-container tbody tr:hover td {
background-color: #f9f9f9 !important;
}
.gantt-container tbody td {
border-right: 1px solid #ccc;
padding: 1px; /* half */
text-align: center;
min-width: 15px; /* half of 30px */
height: 32px;
vertical-align: middle;
line-height: 28px;
background-color: #ffffff;
font-size: 18px;
font-weight: bold;
}
.gantt-container tbody td:nth-child(even) {
background-color: #fafafa;
}
.gantt-container tbody td:first-child {
border-right: 2px solid #666;
padding: 4px; /* half of 8px */
font-weight: 500;
cursor: pointer;
background-color: #fafafa !important;
text-align: left;
width: 100px; /* half of 200px */
min-width: 100px; /* half of 200px */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
height: auto;
line-height: normal;
}
.gantt-container tbody td:first-child:hover {
background-color: #f0f0f0 !important;
text-decoration: underline;
}
.js-gantt-task-cell {
cursor: pointer;
}
.js-gantt-date-icon {
cursor: pointer;
}
.gantt-container .ganttview-weekend {
background-color: #efefef;
}
.gantt-container .ganttview-today {
background-color: #fcf8e3;
border-right: 2px solid #ffb347;
}
/* Task bar styling - VERY VISIBLE */
.gantt-container tbody td.ganttview-block {
background-color: #4CAF50 !important;
color: #fff !important;
font-size: 18px !important;
font-weight: bold !important;
padding: 2px !important;
border-radius: 2px;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.gantt-container table {
font-size: 11px;
}
.gantt-container thead td {
min-width: 20px;
padding: 2px;
}
.gantt-container tbody td {
min-width: 20px;
padding: 1px;
height: 20px;
}
.gantt-container tbody td:first-child {
width: 100px;
font-size: 12px;
}
}
/* Print styles */
@media print {
.gantt-container {
overflow: visible;
}
.gantt-container table {
page-break-inside: avoid;
}
}

View file

@ -1,27 +0,0 @@
//- Gantt Chart View Template
template(name="ganttView")
link(rel="stylesheet" href="/client/components/gantt/gantt.css")
link(rel="stylesheet" href="/client/components/gantt/ganttCard.css")
.gantt-view
h2 {{_ 'board-view-gantt'}}
if hasSelectedCard
+ganttCard(selectedCard)
each weeks
table.gantt-table
thead
tr
th {{_ 'task'}} {{_ 'predicate-week'}} {{week}}
each weekDays this
th
| {{formattedDate .}} {{weekdayLabel .}}
tbody
each cardsInWeek this
tr(data-card-id="{{cardId .}}")
td.js-gantt-task-cell
a.js-gantt-card-title(href="#")
+viewer
| {{cardTitle .}}
each weekDays ..
td(class="{{cellClasses .. .}}" data-card-id="{{cardId ..}}" data-date-type="{{cellContentClass .. .}}")
| {{cellContent .. .}}

View file

@ -1,217 +0,0 @@
// Add click handler to ganttView for card titles
Template.ganttView.events({
'click .js-gantt-card-title'(event, template) {
event.preventDefault();
// Get card ID from the closest row's data attribute
const $row = template.$(event.currentTarget).closest('tr');
const cardId = $row.data('card-id');
if (cardId) {
template.selectedCardId.set(cardId);
}
},
});
import { Template } from 'meteor/templating';
// Blaze template helpers for ganttView
function getISOWeekInfo(d) {
const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
const dayNum = date.getUTCDay() || 7;
date.setUTCDate(date.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
const week = Math.ceil((((date - yearStart) / 86400000) + 1) / 7);
return { year: date.getUTCFullYear(), week };
}
function startOfISOWeek(d) {
const date = new Date(d);
const day = date.getDay() || 7;
if (day !== 1) date.setDate(date.getDate() - (day - 1));
date.setHours(0,0,0,0);
return date;
}
Template.ganttView.helpers({
weeks() {
const board = Utils.getCurrentBoard();
if (!board) return [];
const cards = Cards.find({ boardId: board._id }, { sort: { startAt: 1, dueAt: 1 } }).fetch();
const weeksMap = new Map();
const relevantCards = cards.filter(c => c.receivedAt || c.startAt || c.dueAt || c.endAt);
relevantCards.forEach(card => {
['receivedAt','startAt','dueAt','endAt'].forEach(field => {
if (card[field]) {
const dt = new Date(card[field]);
const info = getISOWeekInfo(dt);
const key = `${info.year}-W${info.week}`;
if (!weeksMap.has(key)) {
weeksMap.set(key, { year: info.year, week: info.week, start: startOfISOWeek(dt) });
}
}
});
});
return Array.from(weeksMap.values()).sort((a,b) => a.start - b.start);
},
weekDays(week) {
const weekStart = new Date(week.start);
return Array.from({length:7}, (_,i) => {
const d = new Date(weekStart);
d.setDate(d.getDate() + i);
d.setHours(0,0,0,0);
return d;
});
},
weekdayLabel(day) {
const weekdayKeys = ['monday','tuesday','wednesday','thursday','friday','saturday','sunday'];
return TAPi18n.__(weekdayKeys[day.getDay() === 0 ? 6 : day.getDay() - 1]);
},
formattedDate(day) {
const currentUser = ReactiveCache.getCurrentUser && ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(day, dateFormat, false);
},
cardsInWeek(week) {
const board = Utils.getCurrentBoard();
if (!board) return [];
const cards = Cards.find({ boardId: board._id }).fetch();
return cards.filter(card => {
return ['receivedAt','startAt','dueAt','endAt'].some(field => {
if (card[field]) {
const dt = new Date(card[field]);
const info = getISOWeekInfo(dt);
return info.week === week.week && info.year === week.year;
}
return false;
});
});
},
cardTitle(card) {
return card.title;
},
cardId(card) {
return card._id;
},
cardUrl(card) {
if (!card) return '#';
const board = ReactiveCache.getBoard(card.boardId);
if (!board) return '#';
return FlowRouter.path('card', {
boardId: card.boardId,
slug: board.slug,
cardId: card._id,
});
},
cellContentClass(card, day) {
const cardDates = {
receivedAt: card.receivedAt ? new Date(card.receivedAt) : null,
startAt: card.startAt ? new Date(card.startAt) : null,
dueAt: card.dueAt ? new Date(card.dueAt) : null,
endAt: card.endAt ? new Date(card.endAt) : null,
};
if (cardDates.receivedAt && cardDates.receivedAt.toDateString() === day.toDateString()) return 'ganttview-received';
if (cardDates.startAt && cardDates.startAt.toDateString() === day.toDateString()) return 'ganttview-start';
if (cardDates.dueAt && cardDates.dueAt.toDateString() === day.toDateString()) return 'ganttview-due';
if (cardDates.endAt && cardDates.endAt.toDateString() === day.toDateString()) return 'ganttview-end';
return '';
},
cellContent(card, day) {
const cardDates = {
receivedAt: card.receivedAt ? new Date(card.receivedAt) : null,
startAt: card.startAt ? new Date(card.startAt) : null,
dueAt: card.dueAt ? new Date(card.dueAt) : null,
endAt: card.endAt ? new Date(card.endAt) : null,
};
if (cardDates.receivedAt && cardDates.receivedAt.toDateString() === day.toDateString()) return '📥';
if (cardDates.startAt && cardDates.startAt.toDateString() === day.toDateString()) return '🚀';
if (cardDates.dueAt && cardDates.dueAt.toDateString() === day.toDateString()) return '⏰';
if (cardDates.endAt && cardDates.endAt.toDateString() === day.toDateString()) return '🏁';
return '';
},
isToday(day) {
const today = new Date();
return day.toDateString() === today.toDateString();
},
isWeekend(day) {
const idx = day.getDay();
return idx === 0 || idx === 6;
},
hasSelectedCard() {
return Template.instance().selectedCardId.get() !== null;
},
selectedCard() {
const cardId = Template.instance().selectedCardId.get();
return cardId ? ReactiveCache.getCard(cardId) : null;
},
cellClasses(card, day) {
// Get the base class from cellContentClass logic
const cardDates = {
receivedAt: card.receivedAt ? new Date(card.receivedAt) : null,
startAt: card.startAt ? new Date(card.startAt) : null,
dueAt: card.dueAt ? new Date(card.dueAt) : null,
endAt: card.endAt ? new Date(card.endAt) : null,
};
let classes = '';
if (cardDates.receivedAt && cardDates.receivedAt.toDateString() === day.toDateString()) classes = 'ganttview-received';
else if (cardDates.startAt && cardDates.startAt.toDateString() === day.toDateString()) classes = 'ganttview-start';
else if (cardDates.dueAt && cardDates.dueAt.toDateString() === day.toDateString()) classes = 'ganttview-due';
else if (cardDates.endAt && cardDates.endAt.toDateString() === day.toDateString()) classes = 'ganttview-end';
// Add conditional classes
const today = new Date();
if (day.toDateString() === today.toDateString()) classes += ' ganttview-today';
const idx = day.getDay();
if (idx === 0 || idx === 6) classes += ' ganttview-weekend';
if (classes.trim()) classes += ' js-gantt-date-icon';
return classes.trim();
}
});
Template.ganttView.onCreated(function() {
this.selectedCardId = new ReactiveVar(null);
// Provide properties expected by cardDetails component
this.showOverlay = new ReactiveVar(false);
this.mouseHasEnterCardDetails = false;
});
// Blaze onRendered logic for ganttView
Template.ganttView.onRendered(function() {
const self = this;
this.autorun(() => {
// If you have legacy imperative rendering, keep it here
if (typeof renderGanttChart === 'function') {
renderGanttChart();
}
});
// Add click handler for date cells (Received, Start, Due, End)
this.$('.gantt-table').on('click', '.js-gantt-date-icon', function(e) {
e.preventDefault();
e.stopPropagation();
const $cell = self.$(this);
const cardId = $cell.data('card-id');
let dateType = $cell.data('date-type');
// Remove 'ganttview-' prefix to match popup map
if (typeof dateType === 'string' && dateType.startsWith('ganttview-')) {
dateType = dateType.replace('ganttview-', '');
}
const popupMap = {
received: 'editCardReceivedDate',
start: 'editCardStartDate',
due: 'editCardDueDate',
end: 'editCardEndDate',
};
const popupName = popupMap[dateType];
if (!popupName || typeof Popup === 'undefined' || typeof Popup.open !== 'function') return;
const card = ReactiveCache.getCard(cardId);
if (!card) return;
const openFn = Popup.open(popupName);
openFn.call({ currentData: () => card }, e, { dataContextIfCurrentDataIsUndefined: card });
});
});
import markdownit from 'markdown-it';
import { TAPi18n } from '/imports/i18n';
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
import { ReactiveCache } from '/imports/reactiveCache';
const md = markdownit({ breaks: true, linkify: true });

View file

@ -1,47 +0,0 @@
.gantt-card-wrapper {
background: white;
border: 1px solid #ddd;
border-radius: 5px;
margin: 1rem 0;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.gantt-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
border-bottom: 1px solid #eee;
padding-bottom: 0.5rem;
}
.gantt-card-header h3 {
margin: 0;
color: #333;
}
.close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
background-color: #f0f0f0;
color: #333;
}
.gantt-card-content {
max-height: 400px;
overflow-y: auto;
}

View file

@ -1,2 +0,0 @@
template(name="ganttCard")
+cardDetails(selectedCard)

View file

@ -1,45 +0,0 @@
BlazeComponent.extendComponent({
onCreated() {
// Provide the expected parent component properties for cardDetails
this.showOverlay = new ReactiveVar(false);
this.mouseHasEnterCardDetails = false;
},
selectedCard() {
// The selected card is now passed as a parameter to the component
return this.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;
}
},
},
];
},
}).register('ganttCard');
// Add click handler to ganttView for card titles
Template.ganttView.events({
'click .js-gantt-card-title'(event, template) {
event.preventDefault();
// Get card ID from the closest row's data attribute
const $row = template.$(event.currentTarget).closest('tr');
const cardId = $row.data('card-id');
if (cardId) {
template.selectedCardId.set(cardId);
}
},
});

View file

@ -1,7 +1,7 @@
template(name="importHeaderBar")
h1
a.back-btn(href="{{pathFor 'home'}}")
| ⬅️
i.fa.fa-chevron-left
| {{_ title}}
template(name="import")
@ -36,7 +36,7 @@ template(name="importMapMembers")
+userAvatar(userId=wekanId)
else
a.member.add-member
|
i.fa.fa-plus
//-
Due to the way the flewbox layout is working, we need to set some
invisible items so that the last row items have a consistent width.

View file

@ -282,7 +282,7 @@ body.list-resizing-active * {
margin: 0 auto;
}
.list.list-collapsed .list-header .js-collapse {
margin: 0 auto 0 auto;
margin: 0 auto 20px auto;
z-index: 10;
padding: 8px 12px;
font-size: 12px;
@ -290,12 +290,6 @@ body.list-resizing-active * {
display: block;
width: fit-content;
}
.list.list-collapsed .list-header .list-header-handle {
position: absolute !important;
top: 30px !important;
right: 1.5vw !important;
z-index: 15 !important;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
height: auto !important;
@ -303,6 +297,7 @@ body.list-resizing-active * {
position: relative !important;
overflow: visible !important;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
text-align: left;
overflow: visible;
@ -313,15 +308,15 @@ body.list-resizing-active * {
color: #333;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #ddd;
padding: 0;
padding: 8px 4px;
border-radius: 4px;
margin: 0;
width: 100vh;
height: 30px;
margin: 0 auto;
width: 25vh;
height: 60vh;
position: absolute;
left: 40px;
left: 50%;
top: 50%;
transform: translateY(calc(-50% + 20px)) rotate(0deg);
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
z-index: 10;
visibility: visible !important;
opacity: 1 !important;
@ -373,18 +368,6 @@ body.list-resizing-active * {
text-overflow: ellipsis;
word-wrap: break-word;
}
/* Sum badge shown before list title */
.list-header .list-sum-badge {
display: inline-block;
margin-right: 8px;
padding: 0;
border-radius: 0;
background: transparent;
color: #8c8c8c;
font-weight: bold;
font-size: 12px;
vertical-align: middle;
}
.list-rotated {
width: 1.3vw;
height: 35vh;
@ -420,42 +403,22 @@ body.list-resizing-active * {
color: #a6a6a6;
margin-right: 15px;
}
/* List header collapse button styling */
.list-header .list-header-collapse-container {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 10px;
flex: 1;
min-width: 0;
}
.list-header .js-collapse {
color: #a6a6a6;
margin-right: 15px;
display: inline-block;
vertical-align: middle;
padding: 5px 8px;
border: none;
border-radius: 0;
background-color: transparent;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f5f5f5;
cursor: pointer;
font-size: 18px;
line-height: 1;
min-width: 30px;
text-align: center;
flex-shrink: 0;
text-decoration: none;
margin: 0;
font-size: 14px;
}
.list-header .js-collapse:hover {
background-color: transparent;
background-color: #e0e0e0;
color: #333;
}
.list-header .list-header-collapse-container > div {
flex: 1;
min-width: 0;
}
.list.list-collapsed .list-header .js-collapse {
display: inline-block !important;
visibility: visible !important;
@ -484,18 +447,17 @@ body.list-resizing-active * {
position: relative !important;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: 100vh;
width: 15vh;
font-size: 12px;
height: 30px;
line-height: 1.2;
padding: 0;
margin: 0;
overflow: visible;
padding: 8px 4px;
margin: 0 auto;
position: absolute;
left: 40px;
left: 50%;
top: 50%;
transform: translateY(calc(-50% + 120px)) rotate(0deg);
text-align: center;
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
text-align: left;
visibility: visible !important;
opacity: 1 !important;
display: block !important;
@ -525,18 +487,17 @@ body.list-resizing-active * {
position: relative !important;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: 100vh;
width: 15vh;
font-size: 12px;
height: 30px;
line-height: 1.2;
padding: 0;
margin: 0;
overflow: visible;
padding: 8px 4px;
margin: 0 auto;
position: absolute;
left: 40px;
left: 50%;
top: 50%;
transform: translateY(calc(-50% + 120px)) rotate(0deg);
text-align: center;
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
text-align: left;
visibility: visible !important;
opacity: 1 !important;
display: block !important;
@ -566,17 +527,16 @@ body.list-resizing-active * {
position: relative !important;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: 100vh;
width: 15vh;
font-size: 12px;
height: 30px;
line-height: 1.2;
padding: 0;
margin: 0;
overflow: visible;
padding: 8px 4px;
margin: 0 auto;
position: absolute;
left: 40px;
left: 50%;
top: 50%;
transform: translateY(calc(-50% + 40px)) rotate(0deg);
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
text-align: left;
visibility: visible !important;
opacity: 1 !important;
@ -790,9 +750,6 @@ body.list-resizing-active * {
grid-row: 2;
grid-column: 2;
align-self: start;
text-align: left;
padding-left: 0;
margin-left: 0;
font-size: 16px !important;
line-height: 1.2;
}
@ -1007,9 +964,6 @@ body.list-resizing-active * {
grid-row: 2 !important;
grid-column: 2 !important;
align-self: start !important;
text-align: left !important;
padding-left: 0 !important;
margin-left: 0 !important;
font-size: 16px !important;
line-height: 1.2 !important;
}
@ -1081,23 +1035,6 @@ body.list-resizing-active * {
grid-row: 1/3 !important;
grid-column: 1 !important;
}
/* Allow long list titles to expand on desktop (non-mobile, non-collapsed) */
.list:not(.mobile-view):not(.list-collapsed) .list-header {
overflow: visible !important;
}
.list:not(.mobile-view):not(.list-collapsed) .list-header .list-header-name {
/* Permit wrapping and full visibility */
white-space: normal !important;
overflow: visible !important;
text-overflow: clip !important;
display: inline-block !important;
/* Reserve space for right-side controls (menu, handle, count) */
max-width: calc(100% - 120px) !important;
/* Break long words to avoid overflow */
word-break: break-word !important;
}
.link-board-wrapper {
display: flex;
align-items: baseline;

View file

@ -3,9 +3,8 @@ template(name='list')
style="{{#unless collapsed}}min-width:{{listWidth}}px;max-width:{{listConstraint}}px;{{/unless}}"
class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}} {{#if isMiniScreen}}mobile-view{{/if}}")
+listHeader
unless collapsed
+listBody
.list-resize-handle.js-list-resize-handle.nodragscroll
+listBody
.list-resize-handle.js-list-resize-handle.nodragscroll
template(name='miniList')
a.mini-list.js-select-list.js-list(id="js-list-{{_id}}" class="{{#if isMiniScreen}}mobile-view{{/if}}")

View file

@ -279,8 +279,7 @@ BlazeComponent.extendComponent({
// Only enable resize for non-collapsed, non-auto-width lists
const isAutoWidth = this.autoWidth();
const isCollapsed = Utils.getListCollapseState(list);
if (isCollapsed || isAutoWidth) {
if (list.collapsed || isAutoWidth) {
$resizeHandle.hide();
return;
}
@ -434,10 +433,9 @@ BlazeComponent.extendComponent({
});
// Reactively update resize handle visibility when auto-width or collapse changes
// Reactively update resize handle visibility when auto-width changes
component.autorun(() => {
const collapsed = Utils.getListCollapseState(list);
if (component.autoWidth() || collapsed) {
if (component.autoWidth()) {
$resizeHandle.hide();
} else {
$resizeHandle.show();
@ -454,12 +452,6 @@ BlazeComponent.extendComponent({
},
}).register('list');
Template.list.helpers({
collapsed() {
return Utils.getListCollapseState(this);
},
});
Template.miniList.events({
'click .js-select-list'() {
const listId = this._id;

View file

@ -85,19 +85,16 @@ template(name="linkCardPopup")
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
option(value="") {{_ 'custom-field-dropdown-none'}}
each swimlanes
option(value="{{_id}}") {{isTitleDefault title}}
label {{_ 'lists'}}:
select.js-select-lists
option(value="") {{_ 'custom-field-dropdown-none'}}
each lists
option(value="{{_id}}") {{isTitleDefault title}}
label {{_ 'cards'}}:
select.js-select-cards
option(value="") {{_ 'custom-field-dropdown-none'}}
each cards
option(value="{{getRealId}}") {{getTitle}}

View file

@ -16,50 +16,11 @@ BlazeComponent.extendComponent({
},
customFieldsSum() {
const list = Template.currentData();
if (!list) return [];
const boardId = Session.get('currentBoard');
const fields = ReactiveCache.getCustomFields({
boardIds: { $in: [boardId] },
const ret = ReactiveCache.getCustomFields({
boardIds: { $in: [Session.get('currentBoard')] },
showSumAtTopOfList: true,
});
if (!fields || !fields.length) return [];
const cards = ReactiveCache.getCards({
listId: list._id,
archived: false,
});
const result = fields.map(field => {
let sum = 0;
if (cards && cards.length) {
cards.forEach(card => {
const cfs = (card.customFields || []);
const cf = cfs.find(f => f && f._id === field._id);
if (!cf || cf.value === null || cf.value === undefined) return;
let v = cf.value;
if (typeof v === 'string') {
// try to parse string numbers, accept comma decimal
const parsed = parseFloat(v.replace(',', '.'));
if (isNaN(parsed)) return;
v = parsed;
}
if (typeof v === 'number' && isFinite(v)) {
sum += v;
}
});
}
return {
_id: field._id,
name: field.name,
type: field.type,
settings: field.settings || {},
value: sum,
};
});
return result;
return ret;
},
openForm(options) {
@ -293,22 +254,6 @@ BlazeComponent.extendComponent({
},
}).register('listBody');
// Helpers for listBody template context
Template.listBody.helpers({
formattedCurrencyCustomFieldValue(val) {
// `this` is the custom field sum object from customFieldsSum each-iteration
const field = this || {};
const code = (field.settings && field.settings.currencyCode) || 'USD';
try {
const n = typeof val === 'number' ? val : parseFloat(val);
if (!isFinite(n)) return val;
return new Intl.NumberFormat(undefined, { style: 'currency', currency: code }).format(n);
} catch (e) {
return `${code} ${val}`;
}
},
});
function toggleValueInReactiveArray(reactiveValue, value) {
const array = reactiveValue.get();
const valueIndex = array.indexOf(value);
@ -542,6 +487,8 @@ BlazeComponent.extendComponent({
{
sort: { sort: 1 },
});
if (swimlanes.length)
this.selectedSwimlaneId.set(swimlanes[0]._id);
return swimlanes;
},
@ -556,6 +503,7 @@ BlazeComponent.extendComponent({
{
sort: { sort: 1 },
});
if (lists.length) this.selectedListId.set(lists[0]._id);
return lists;
},
@ -564,17 +512,19 @@ BlazeComponent.extendComponent({
return [];
}
const ownCardsIds = this.board.cards().map(card => card.getRealId());
const selector = {
const ret = ReactiveCache.getCards(
{
boardId: this.selectedBoardId.get(),
swimlaneId: this.selectedSwimlaneId.get(),
listId: this.selectedListId.get(),
archived: false,
linkedId: { $nin: ownCardsIds },
_id: { $nin: ownCardsIds },
type: { $nin: ['template-card'] },
};
if (this.selectedBoardId.get()) selector.boardId = this.selectedBoardId.get();
if (this.selectedSwimlaneId.get()) selector.swimlaneId = this.selectedSwimlaneId.get();
if (this.selectedListId.get()) selector.listId = this.selectedListId.get();
const ret = ReactiveCache.getCards(selector, { sort: { sort: 1 } });
},
{
sort: { sort: 1 },
});
return ret;
},
@ -595,12 +545,8 @@ BlazeComponent.extendComponent({
return [
{
'change .js-select-boards'(evt) {
const val = $(evt.currentTarget).val();
subManager.subscribe('board', val, false);
// Clear selections to allow linking only board or re-choose swimlane/list
this.selectedSwimlaneId.set('');
this.selectedListId.set('');
this.selectedBoardId.set(val);
subManager.subscribe('board', $(evt.currentTarget).val(), false);
this.selectedBoardId.set($(evt.currentTarget).val());
},
'change .js-select-swimlanes'(evt) {
this.selectedSwimlaneId.set($(evt.currentTarget).val());

View file

@ -26,32 +26,24 @@ template(name="listHeader")
|/#{wipLimit.value})
if showCardsCountForList cards.length
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
if hasNumberFieldsSum
| &nbsp;
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
else
div.list-header-collapse-container
a.list-collapse-indicator.js-collapse(title="{{_ 'collapse'}}")
if collapsed
| ▶
else
| 🔽
div(class="{{#if collapsed}}list-rotated{{/if}}")
h2.list-header-name(
title="{{ moment modifiedAt 'LLL' }}"
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
+viewer
= title
if wipLimit.enabled
|&nbsp;(
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|/#{wipLimit.value})
if collapsed
a.js-collapse(title="{{_ 'uncollapse'}}")
| ⬅️
| ➡️
div(class="{{#if collapsed}}list-rotated{{/if}}")
h2.list-header-name(
title="{{ moment modifiedAt 'LLL' }}"
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
+viewer
= title
if wipLimit.enabled
|&nbsp;(
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|/#{wipLimit.value})
unless collapsed
if showCardsCountForList cards.length
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
if hasNumberFieldsSum
| &nbsp;
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
if isMiniScreen
if currentList
if isWatching
@ -66,10 +58,6 @@ template(name="listHeader")
unless currentUser.isWorker
a.list-header-handle.handle.js-list-handle ↕️
else if currentUser.isBoardMember
if currentUser.isBoardMember
unless currentUser.isCommentOnly
unless currentUser.isWorker
a.list-header-handle.handle.js-list-handle ↕️
if isWatching
i.list-header-watch-icon | 👁️
unless collapsed
@ -79,8 +67,14 @@ template(name="listHeader")
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
if canSeeAddCard
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
a.js-collapse(title="{{_ 'collapse'}}")
| ⬅️
| ➡️
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
if currentUser.isBoardMember
unless currentUser.isCommentOnly
unless currentUser.isWorker
a.list-header-handle.handle.js-list-handle ↕️
template(name="editListTitleForm")
.list-composer

View file

@ -34,14 +34,13 @@ BlazeComponent.extendComponent({
},
collapsed(check = undefined) {
const list = Template.currentData();
const status = Utils.getListCollapseState(list);
const status = list.isCollapsed();
if (check === undefined) {
// just check
return status;
} else {
const next = typeof check === 'boolean' ? check : !status;
Utils.setListCollapseState(list, next);
return next;
list.collapse(!status);
return !status;
}
},
editTitle(event) {
@ -143,48 +142,7 @@ BlazeComponent.extendComponent({
Template.listHeader.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
numberFieldsSum() {
const list = Template.currentData();
if (!list) return 0;
const boardId = Session.get('currentBoard');
const fields = ReactiveCache.getCustomFields({
boardIds: { $in: [boardId] },
showSumAtTopOfList: true,
type: 'number',
});
if (!fields || !fields.length) return 0;
const cards = ReactiveCache.getCards({ listId: list._id, archived: false });
let total = 0;
if (cards && cards.length) {
cards.forEach(card => {
const cfs = (card.customFields || []);
fields.forEach(field => {
const cf = cfs.find(f => f && f._id === field._id);
if (!cf || cf.value === null || cf.value === undefined) return;
let v = cf.value;
if (typeof v === 'string') {
const parsed = parseFloat(v.replace(',', '.'));
if (isNaN(parsed)) return;
v = parsed;
}
if (typeof v === 'number' && isFinite(v)) {
total += v;
}
});
});
}
return total;
},
hasNumberFieldsSum() {
const boardId = Session.get('currentBoard');
const fields = ReactiveCache.getCustomFields({
boardIds: { $in: [boardId] },
showSumAtTopOfList: true,
type: 'number',
});
return !!(fields && fields.length);
},
}
});
Template.listActionPopup.helpers({

View file

@ -3,6 +3,6 @@ template(name="minilist")
class="minicard-{{colorClass}}")
.minicard-title
.handle
span.drag-handle(title="{{_ 'dragList'}}") ↕️
.fa.fa-arrows
+viewer
= title

View file

@ -232,24 +232,6 @@ class DueCardsComponent extends BlazeComponent {
});
}
// Normalize dueAt to timestamps for stable client-side ordering
const future = new Date('2100-12-31').getTime();
const toTime = v => {
if (v === null || v === undefined || v === '') return future;
if (v instanceof Date) return v.getTime();
const t = new Date(v);
if (!isNaN(t.getTime())) return t.getTime();
return future;
};
filteredCards.sort((a, b) => {
const x = toTime(a.dueAt);
const y = toTime(b.dueAt);
if (x > y) return 1;
if (x < y) return -1;
return 0;
});
if (process.env.DEBUG === 'true') {
console.log('dueCards client: filtered to', filteredCards.length, 'cards');
}

View file

@ -339,20 +339,15 @@
width: 100%;
min-width: 3vw;
font-size: clamp(12px, 2vw, 14px);
box-sizing: border-box;
-webkit-appearance: none;
appearance: none;
flex: 0 0 auto;
}
/* Make zoom input wider on all mobile screens */
@media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
#header-quick-access .zoom-controls .zoom-input {
min-width: 80px !important; /* Wider on mobile to show 3 digits */
width: 80px !important; /* Fixed width to show 100 fully */
font-size: 16px !important; /* Slightly larger text */
flex: 0 0 80px !important; /* Prevent shrinking in flex */
min-width: 50px !important; /* Wider on mobile */
width: 50px !important; /* Fixed width to show all numbers */
font-size: 14px !important; /* Slightly larger text */
}
}
@ -855,9 +850,8 @@
#header-quick-access .zoom-controls .zoom-input {
font-size: 16px !important; /* Larger input text */
padding: 0.5vh 0.8vw !important;
min-width: 80px !important; /* Wider to fit 100 */
width: 80px !important; /* Fixed width to show 100 fully */
flex: 0 0 80px !important; /* Prevent shrinking in flex */
min-width: 6vw !important; /* Much wider for mobile */
width: 60px !important; /* Fixed width to show all numbers */
}
/* Make mobile mode toggle larger */

View file

@ -111,9 +111,7 @@ template(name="header")
| 📢
+viewer
| #{announcement}
a
.js-close-announcement
| ❌
| ❌
template(name="offlineWarning")
.offline-warning

View file

@ -81,27 +81,6 @@ body {
display: flex;
flex-direction: column;
height: 100vh;
/* iOS Safari fixes */
-webkit-overflow-scrolling: touch;
}
/* Mobile mode specific fixes for iOS Safari */
body.mobile-mode {
overflow-x: hidden;
position: fixed;
width: 100%;
height: 100vh;
/* Prevent iOS Safari bounce scroll */
overscroll-behavior: none;
-webkit-overflow-scrolling: touch;
}
/* Ensure content area is scrollable in mobile mode */
body.mobile-mode #content {
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
height: calc(100vh - 48px);
}
#content {
position: relative;
@ -548,7 +527,7 @@ a:not(.disabled).is-active i.fa {
/* Board canvas */
.board-canvas {
padding: 0 8px 8px 0;
padding: 8px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
@ -696,7 +675,7 @@ a:not(.disabled).is-active i.fa {
}
.board-canvas {
padding: 0 12px 12px 0;
padding: 12px;
}
#header {
@ -777,7 +756,7 @@ a:not(.disabled).is-active i.fa {
.inline-input {
height: 37px;
margin: 8px 10px 0 0;
width: 100px;
width: 50px;
}
.select-authentication {
width: 100%;
@ -920,40 +899,6 @@ a:not(.disabled).is-active i.fa {
height: 100%;
}
}
/* iOS Safari Mobile Mode Fixes */
@media screen and (max-width: 800px) {
/* Prevent scrolling issues on iOS Safari when card popup is open */
body.mobile-mode {
overflow: hidden;
position: fixed;
width: 100%;
height: 100vh;
}
/* Fix z-index stacking for mobile Safari */
body.mobile-mode .board-wrapper {
z-index: 1;
}
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;
}
body.mobile-mode .card-details {
z-index: 100 !important;
}
body.mobile-mode .pop-over {
z-index: 999;
}
/* Ensure smooth scrolling on iOS */
body.mobile-mode .card-details,
body.mobile-mode .pop-over .content-wrapper {
-webkit-overflow-scrolling: touch;
}
}
@-moz-keyframes lds-roller {
0% {
transform: rotate(0deg);

View file

@ -2,10 +2,8 @@ template(name="main")
html(lang="{{TAPi18n.getLanguage}}")
head
title
meta(name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes, viewport-fit=cover")
meta(name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes")
meta(http-equiv="X-UA-Compatible" content="IE=edge")
meta(name="apple-mobile-web-app-capable" content="yes")
meta(name="apple-mobile-web-app-status-bar-style" content="black-translucent")
//- XXX We should use pathFor in the following `href` to support the case
where the application is deployed with a path prefix, but it seems to be
difficult to do that cleanly with Blaze -- at least without adding extra
@ -79,6 +77,7 @@ template(name="defaultLayout")
| {{{afterBodyStart}}}
+Template.dynamic(template=content)
| {{{beforeBodyEnd}}}
+migrationProgress
+boardConversionProgress
if (Modal.isOpen)
#modal

View file

@ -94,24 +94,24 @@
}
/* 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="editUser"],
.pop-over[data-popup="editOrg"],
.pop-over[data-popup="editTeam"] {
height: calc(100vh - 20px) !important;
max-height: calc(100vh - 20px) !important;
}
.pop-over[data-popup="editUserPopup"] .content-wrapper,
.pop-over[data-popup="editOrgPopup"] .content-wrapper,
.pop-over[data-popup="editTeamPopup"] .content-wrapper {
.pop-over[data-popup="editUser"] .content-wrapper,
.pop-over[data-popup="editOrg"] .content-wrapper,
.pop-over[data-popup="editTeam"] .content-wrapper {
max-height: calc(100vh - 80px) !important; /* Subtract header height */
height: calc(100vh - 80px) !important;
overflow-y: auto !important;
}
.pop-over[data-popup="editUserPopup"] .content-container,
.pop-over[data-popup="editOrgPopup"] .content-container,
.pop-over[data-popup="editTeamPopup"] .content-container {
.pop-over[data-popup="editUser"] .content-container,
.pop-over[data-popup="editOrg"] .content-container,
.pop-over[data-popup="editTeam"] .content-container {
max-height: calc(100vh - 80px) !important; /* Subtract header height */
height: calc(100vh - 80px) !important;
}
@ -123,7 +123,7 @@
}
/* Specific styling for language popup list */
.pop-over[data-popup="changeLanguagePopup"] .pop-over-list {
.pop-over[data-popup="changeLanguage"] .pop-over-list {
max-height: none;
overflow: visible;
height: auto;
@ -131,69 +131,46 @@
}
/* Ensure content div in language popup contains all items */
.pop-over[data-popup="changeLanguagePopup"] .content {
.pop-over[data-popup="changeLanguage"] .content {
height: auto;
/* Remove forced min-height to avoid top gap */
min-height: 100%;
display: flex;
flex-direction: column;
}
/* Ensure hidden stack pages truly take no space */
.pop-over[data-popup="changeLanguagePopup"] .content.no-height {
min-height: 0 !important;
height: 0 !important;
padding: 0 !important;
margin: 0 !important;
visibility: hidden !important;
/* Allow dynamic height for Change Language popup */
.pop-over[data-popup="changeLanguage"] .content-wrapper {
max-height: inherit; /* Use dynamic height from JavaScript */
}
.pop-over[data-popup="changeLanguage"] .content-container {
max-height: inherit; /* Use dynamic height from JavaScript */
}
/* Make language popup extend to bottom of browser window */
.pop-over[data-popup="changeLanguagePopup"] {
position: fixed !important;
bottom: 0 !important;
top: auto !important;
left: auto !important;
right: 20px !important;
width: auto !important;
max-width: 450px !important;
height: 100vh !important;
max-height: 100vh !important;
min-height: 300px !important;
display: flex !important;
flex-direction: column !important;
margin: 0 !important;
.pop-over[data-popup="changeLanguage"] {
height: calc(100vh - 30px);
min-height: 300px;
/* Adjust positioning to move popup 30px higher */
transform: translateY(-30px);
}
/* Allow dynamic height for Change Language popup */
.pop-over[data-popup="changeLanguagePopup"] .header {
flex-shrink: 0 !important;
height: auto !important;
.pop-over[data-popup="changeLanguage"] .content-wrapper {
height: calc(100% - 50px); /* Subtract header height more precisely */
min-height: 250px;
overflow-y: auto;
max-height: none; /* Remove any max-height constraints */
display: flex;
flex-direction: column;
}
.pop-over[data-popup="changeLanguagePopup"] .content-wrapper {
flex: 1 !important;
overflow-y: auto !important;
overflow-x: hidden !important;
min-height: 0 !important;
max-height: none !important;
height: auto !important;
width: 100% !important;
}
.pop-over[data-popup="changeLanguagePopup"] .content-container {
height: auto !important;
max-height: none !important;
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
width: 100% !important;
}
.pop-over[data-popup="changeLanguagePopup"] .content {
height: auto !important;
max-height: none !important;
padding-bottom: 50px !important;
width: 100% !important;
.pop-over[data-popup="changeLanguage"] .content-container {
height: auto; /* Let content determine height */
min-height: 250px;
max-height: none; /* Remove any max-height constraints */
flex: 1;
display: flex;
flex-direction: column;
}
/* Date popup sizing for native HTML inputs */
@ -538,7 +515,6 @@
position: absolute;
top: 6px;
right: 12px;
color: #3cb500;
}
.pop-over-list .pop-over-list.checkable li.active a {
padding-right: 28px;
@ -546,10 +522,6 @@
.pop-over-list .pop-over-list.checkable li.active a .fa-check {
display: block;
}
/* Grey check icons when grey icons setting is enabled */
body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
color: #7a7a7a;
}
.pop-over.miniprofile .header {
border-bottom-color: transparent;
height: 30px;
@ -595,10 +567,6 @@ body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
overflow: hidden;
margin-top: 0px;
border: 0px solid #dbdbdb;
/* Ensure popups appear above card details on mobile */
z-index: 999999 !important;
/* iOS Safari scrolling fix */
-webkit-overflow-scrolling: touch;
}
.pop-over .header {
color: #fff;
@ -683,23 +651,3 @@ body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
transform: none !important;
}
}
/* Force full-screen popups in mobile mode regardless of screen width */
body.mobile-mode .pop-over {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
max-width: 100vw !important;
max-height: 100vh !important;
}
body.mobile-mode .pop-over .content-wrapper {
width: 100% !important;
height: calc(100vh - 48px) !important;
max-height: calc(100vh - 48px) !important;
overflow-y: auto !important;
overflow-x: hidden !important;
}

View file

@ -2,7 +2,6 @@
class="{{#unless title}}miniprofile{{/unless}}"
class=currentBoard.colorClass
class="{{#unless title}}no-title{{/unless}}"
data-popup="{{popupName}}"
style="left:{{offset.left}}px; top:{{offset.top}}px;{{#if offset.maxHeight}} max-height:{{offset.maxHeight}}px;{{/if}}")
.header
a.back-btn.js-back-view(class="{{#unless hasPopupParent}}is-hidden{{/unless}}")

View file

@ -1,29 +0,0 @@
template(name="supportHeaderBar")
h1
if isSupportEnabled
= supportTitle
else
| {{_ 'support'}}
template(name="support")
.support-page
if isSupportPublic
if isSupportEnabled
.support-page-content
+viewer
| {{supportContent}}
else
.support-page-content
| {{_ 'support-info-not-added-yet'}}
else
if currentUser
if isSupportEnabled
.support-page-content
+viewer
| {{supportContent}}
else
.support-page-content
| {{_ 'support-info-not-added-yet'}}
else
.support-page-content
| {{_ 'support-info-only-for-logged-in-users'}}

View file

@ -1,41 +0,0 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
// Shared helpers for both support templates
const supportHelpers = {
supportTitle() {
const setting = ReactiveCache.getCurrentSetting();
return setting && setting.supportTitle ? setting.supportTitle : TAPi18n.__('support');
},
supportContent() {
const setting = ReactiveCache.getCurrentSetting();
return setting && setting.supportPageText ? setting.supportPageText : TAPi18n.__('support-info-not-added-yet');
},
isSupportEnabled() {
const setting = ReactiveCache.getCurrentSetting();
return setting && setting.supportPageEnabled;
},
isSupportPublic() {
const setting = ReactiveCache.getCurrentSetting();
return setting && setting.supportPagePublic;
}
};
// Main support page component
BlazeComponent.extendComponent({
onCreated() {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
Meteor.subscribe('setting');
},
...supportHelpers
}).register('support');
// Header bar component
BlazeComponent.extendComponent({
onCreated() {
Meteor.subscribe('setting');
},
...supportHelpers
}).register('supportHeaderBar');

View file

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

View file

@ -4,14 +4,14 @@ template(name="migrationProgress")
.migration-progress-modal
.migration-progress-header
h3.migration-progress-title
| 🔄 {{_ 'migration-progress-title'}}
| 🔄 Board Migration in Progress
.migration-progress-close.js-close-migration-progress
| ❌
.migration-progress-content
.migration-progress-overall
.migration-progress-overall-label
| {{_ 'migration-progress-overall'}}: {{currentStep}} {{_ 'of'}} {{totalSteps}} {{_ 'steps'}}
| Overall Progress: {{currentStep}} of {{totalSteps}} steps
.migration-progress-overall-bar
.migration-progress-overall-fill(style="{{progressBarStyle}}")
.migration-progress-overall-percentage
@ -19,7 +19,7 @@ template(name="migrationProgress")
.migration-progress-current-step
.migration-progress-step-label
| {{_ 'migration-progress-current-step'}}: {{stepNameFormatted}}
| Current Step: {{stepNameFormatted}}
.migration-progress-step-bar
.migration-progress-step-fill(style="{{stepProgressBarStyle}}")
.migration-progress-step-percentage
@ -27,17 +27,17 @@ template(name="migrationProgress")
.migration-progress-status
.migration-progress-status-label
| {{_ 'migration-progress-status'}}:
| Status:
.migration-progress-status-text
| {{stepStatus}}
if stepDetailsFormatted
.migration-progress-details
.migration-progress-details-label
| {{_ 'migration-progress-details'}}:
| Details:
.migration-progress-details-text
| {{stepDetailsFormatted}}
.migration-progress-footer
.migration-progress-note
| {{_ 'migration-progress-note'}}
| Please wait while we migrate your board to the latest structure...

View file

@ -20,10 +20,10 @@
height: 3vw;
}
#notifications-drawer .notification .read-status .activity-type {
margin: 8px 0 0;
width: 1.2em;
height: 1.2em;
font-size: clamp(14px, 2vw, 17px);
margin: 2vh 0 0;
width: 2.2vw;
height: 2.2vw;
font-size: clamp(14px, 2.5vw, 17px);
display: block;
color: #bbb;
}

View file

@ -7,4 +7,4 @@ template(name='notification')
+activity(activity=activityData mode='none')
if read
.remove
a(title="{{_ 'delete'}}") 🗑️
a.fa.fa-trash

View file

@ -1,8 +1,8 @@
template(name='notificationIcon')
if($in activityType 'deleteAttachment' 'addAttachment')
span.activity-type(title="attachment") 📎
i.fa.fa-paperclip.activity-type(title="attachment")
else if($in activityType 'createBoard' 'importBoard')
span.activity-type(title="board") 🗂️
i.fa.fa-chalkboard.activity-type(title="board")
else if($in activityType 'createCard' 'importCard' 'moveCard')
+cardNotificationIcon
@ -19,17 +19,17 @@ template(name='notificationIcon')
//- DRY and consistant
else if($in activityType 'checkedItem' 'uncheckedItem' 'addChecklistItem' 'removedChecklistItem')
span.activity-type(title="checklist item") ☑️
i.fa.fa-check-square.activity-type(title="checklist item")
else if($in activityType 'addComment')
span.activity-type(title="comment") 💬
i.fa.fa-comment-o.activity-type(title="comment")
else if($in activityType 'createCustomField' 'setCustomField' 'unsetCustomField')
span.activity-type(title="custom field") 🧩
i.fa.fa-code.activity-type(title="custom field")
else if($in activityType 'addedLabel' 'removedLabel')
span.activity-type(title="label") 🏷️
i.fa.fa-tag.activity-type(title="label")
else if($in activityType 'a-startAt' 'a-receivedAt')
span.activity-type(title="date") ⏰
i.fa.fa-clock-o.activity-type(title="date")
else if($in activityType 'a-dueAt' 'a-endAt')
span.activity-type(title="date") ⏰
i.fa.fa-clock-o.activity-type(title="date")
else if($in activityType 'createList' 'removeList' 'archivedList')
+listNotificationIcon
@ -41,17 +41,17 @@ template(name='notificationIcon')
//- elswhere in the app we use fa-trello to indicate lists...
//- i personally like fa-columns a bit better
else if($in activityType 'unjoinMember' 'addBoardMember' 'joinMember' 'removeBoardMember')
span.activity-type(title="member") 👤
i.fa.fa-user.activity-type(title="member")
else if($in activityType 'createSwimlane' 'archivedSwimlane')
span.activity-type(title="swimlane") 🧭
i.fa.fa-th-large.activity-type(title="swimlane")
else
span.activity-type(title="can't find icon for #{activityType}") 🐞
i.fa.fa-bug.activity-type(title="can't find icon for #{activityType}")
template(name='cardNotificationIcon')
span.activity-type(title="card") 🗒️
i.fa.fa-clone.activity-type(title="card")
template(name='checklistNotificationIcon')
span.activity-type(title="checklist") 📝
i.fa.fa-list.activity-type(title="checklist")
template(name='listNotificationIcon')
span.activity-type(title="list") 📋
i.fa.fa-columns.activity-type(title="list")

View file

@ -55,6 +55,9 @@ section#notifications-drawer .remove-read {
section#notifications-drawer .remove-read:hover {
color: #eb4646 !important;
}
section#notifications-drawer .remove-read:hover i.fa {
color: inherit;
}
section#notifications-drawer ul.notifications {
display: block;
padding: 0px 16px 0px 16px;

View file

@ -8,7 +8,7 @@ template(name='notificationsDrawer')
h5 {{_ 'notifications'}}
if($gt unreadNotifications 0)
|(#{unreadNotifications})
a.close
a.fa.fa-times-thin.close
ul.notifications
each transformedProfile.notifications
+notification(activityData=activityObj index=dbIndex read=read)
@ -16,5 +16,5 @@ template(name='notificationsDrawer')
a.all-read {{_ 'mark-all-as-read'}}
if ($and ($.Session.get 'showReadNotifications') ($gt readNotifications 0))
a.remove-read
| 🗑️
i.fa.fa-trash
| {{_ 'remove-all-read'}}

View file

@ -24,7 +24,6 @@ template(name="boardActions")
| {{_'r-the-board'}}
div.trigger-dropdown
select(id="board-id")
option(value="" disabled selected if=not boards.length) {{loadingBoardsLabel}}
each boards
if $eq _id currentBoard._id
option(value="{{_id}}" selected) {{_ 'current'}}
@ -86,7 +85,6 @@ template(name="boardActions")
| {{_'r-the-board'}}
div.trigger-dropdown
select(id="board-id-link")
option(value="" disabled selected if=not boards.length) {{loadingBoardsLabel}}
each boards
if $eq _id currentBoard._id
option(value="{{_id}}" selected) {{_ 'current'}}

View file

@ -1,11 +1,7 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
BlazeComponent.extendComponent({
onCreated() {
// Ensure boards are available for action dropdowns
this.subscribe('boards');
},
onCreated() {},
boards() {
const ret = ReactiveCache.getBoards(
@ -23,16 +19,6 @@ BlazeComponent.extendComponent({
return ret;
},
loadingBoardsLabel() {
try {
const txt = TAPi18n.__('loading-boards');
if (txt && !txt.startsWith("key '")) return txt;
} catch (e) {
// ignore translation lookup errors
}
return 'Loading boards...';
},
events() {
return [
{

View file

@ -5,7 +5,6 @@ BlazeComponent.extendComponent({
this.subscribe('allRules');
this.subscribe('allTriggers');
this.subscribe('allActions');
this.subscribe('boards');
},
trigger() {

View file

@ -1,7 +1,7 @@
template(name="rulesActions")
h2
| ✨
| {{_ 'r-rule' }} "{{ruleNameStr}}" - {{_ 'r-add-action'}}
| {{_ 'r-rule' }} "#{data.ruleName.get}" - {{_ 'r-add-action'}}
.triggers-content
.triggers-body
.triggers-side-menu
@ -15,13 +15,13 @@ template(name="rulesActions")
li.js-set-mail-actions
| @
.triggers-main-body
if $eq currentActions.get 'board'
if ($eq currentActions.get 'board')
+boardActions(ruleName=data.ruleName triggerVar=data.triggerVar)
else if $eq currentActions.get 'card'
else if ($eq currentActions.get 'card')
+cardActions(ruleName=data.ruleName triggerVar=data.triggerVar)
else if $eq currentActions.get 'checklist'
else if ($eq currentActions.get 'checklist')
+checklistActions(ruleName=data.ruleName triggerVar=data.triggerVar)
else if $eq currentActions.get 'mail'
else if ($eq currentActions.get 'mail')
+mailActions(ruleName=data.ruleName triggerVar=data.triggerVar)
div.rules-back
button.js-goback

View file

@ -5,15 +5,6 @@ BlazeComponent.extendComponent({
this.currentActions = new ReactiveVar('board');
},
ruleNameStr() {
const rn = this.data() && this.data().ruleName;
try {
return rn && typeof rn.get === 'function' ? rn.get() : '';
} catch (_) {
return '';
}
},
setBoardActions() {
this.currentActions.set('board');
$('.js-set-card-actions').removeClass('active');

View file

@ -1,7 +1,7 @@
template(name="rulesTriggers")
h2
| ✨
| {{_ 'r-rule' }} "{{ruleNameStr}}" - {{_ 'r-add-trigger'}}
| {{_ 'r-rule' }} "#{data.ruleName.get}" - {{_ 'r-add-trigger'}}
.triggers-content
.triggers-body
.triggers-side-menu

View file

@ -7,15 +7,6 @@ BlazeComponent.extendComponent({
this.showChecklistTrigger = new ReactiveVar(false);
},
ruleNameStr() {
const rn = this.data() && this.data().ruleName;
try {
return rn && typeof rn.get === 'function' ? rn.get() : '';
} catch (_) {
return '';
}
},
setBoardTriggers() {
this.showBoardTrigger.set(true);
this.showCardTrigger.set(false);

View file

@ -105,7 +105,7 @@ template(name="boardTriggers")
template(name="boardCardTitlePopup")
form
label
| {{_ 'boardCardTitlePopup-title'}}
| Card Title Filter
input.js-card-filter-name(type="text" value=title autofocus)
input.js-card-filter-button.primary.wide(type="submit" value="{{_ 'set-filter'}}")

View file

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

View file

@ -48,7 +48,7 @@ template(name="people")
option(value="inactive") {{_ 'admin-people-filter-inactive'}}
option(value="admin") Admin
button#unlockAllUsers.unlock-all-btn
span.emoji-icon 🔓
| 🔓
| {{_ 'accounts-lockout-unlock-all'}}
.ext-box-right
span {{#unless isMiniScreen}}{{_ 'people-number'}}{{/unless}} #{peopleNumber}
@ -58,7 +58,7 @@ template(name="people")
| {{_ 'add'}} / {{_ 'delete'}} {{_ 'teams'}}
else if lockedUsersSetting.get
span
span.emoji-icon.text-red 🔒
span.text-red 🔒
unless isMiniScreen
| {{_ 'accounts-lockout-locked-users'}}
@ -79,7 +79,7 @@ template(name="people")
| {{_ 'people'}}
li
a.js-locked-users-menu(data-id="locked-users-setting")
span.emoji-icon.text-red 🔒
span.text-red 🔒
| {{_ 'accounts-lockout-locked-users'}}
.main-body
if loading.get
@ -247,9 +247,9 @@ template(name="peopleRow")
input.selectUserChkBox(type="checkbox", id="{{userData._id}}")
td.account-status
if isUserLocked
span.text-red.js-toggle-lock-status.emoji-icon(data-user-id=userData._id, data-is-locked="true", title="{{_ 'accounts-lockout-click-to-unlock'}}") 🔒
span.text-red.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="true", title="{{_ 'accounts-lockout-click-to-unlock'}}") 🔒
else
span.text-green.js-toggle-lock-status.emoji-icon(data-user-id=userData._id, data-is-locked="false", title="{{_ 'accounts-lockout-user-unlocked'}}") 🔓
span.text-green.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="false", title="{{_ 'accounts-lockout-user-unlocked'}}") 🔓
td.account-active-status
if userData.loginDisabled
span.text-red.js-toggle-active-status(data-user-id=userData._id, data-is-active="false", title="{{_ 'admin-people-user-inactive'}}") 🚫

View file

@ -137,13 +137,8 @@
padding: 0.5rem 0.5rem;
}
.setting-content .content-body .main-body ul li a .is-checked {
border-bottom: 2px solid #3cb500;
border-right: 2px solid #3cb500;
}
/* Grey checkmarks when grey icons setting is enabled */
body.grey-icons-enabled .setting-content .content-body .main-body ul li a .is-checked {
border-bottom: 2px solid #7a7a7a;
border-right: 2px solid #7a7a7a;
border-bottom: 2px solid #2980b9;
border-right: 2px solid #2980b9;
}
.setting-content .content-body .main-body ul li a span {
padding: 0 0.5rem;

View file

@ -6,87 +6,87 @@ template(name="setting")
.content-title.ext-box
if isGeneralSetting
span
span.emoji-icon 🔑
| 🔑
| {{_ 'registration'}}
else if isEmailSetting
span
span.emoji-icon ✉️
| ✉️
| {{_ 'email'}}
else if isAccountSetting
span
span.emoji-icon 👥
| 👥
| {{_ 'accounts'}}
else if isTableVisibilityModeSetting
span
span.emoji-icon 👁️
| 👁️
| {{_ 'tableVisibilityMode'}}
else if isAnnouncementSetting
span
span.emoji-icon 📢
| 📢
| {{_ 'admin-announcement'}}
else if isAccessibilitySetting
span
span.emoji-icon
|
| {{_ 'accessibility'}}
else if isLayoutSetting
span
span.emoji-icon 🔗
| 🔗
| {{_ 'layout'}}
else if isWebhookSetting
span
span.emoji-icon 🌐
| 🌐
| {{_ 'global-webhook'}}
else if isAttachmentSettings
span
span.emoji-iconpan.emoji-icon 📎
| 📎
| {{_ 'attachments'}}
else if isCronSettings
span
span.emoji-icon
|
| {{_ 'cron'}}
.content-body
.side-menu
ul
li(class="{{#if isGeneralSetting}}active{{/if}}")
a.js-setting-menu(data-id="registration-setting")
span.emoji-icon 🔑
| 🔑
| {{_ 'registration'}}
unless isSandstorm
li(class="{{#if isEmailSetting}}active{{/if}}")
a.js-setting-menu(data-id="email-setting")
span.emoji-icon ✉️
| ✉️
| {{_ 'email'}}
li(class="{{#if isAccountSetting}}active{{/if}}")
a.js-setting-menu(data-id="account-setting")
span.emoji-icon 👥
| 👥
| {{_ 'accounts'}}
li(class="{{#if isTableVisibilityModeSetting}}active{{/if}}")
a.js-setting-menu(data-id="tableVisibilityMode-setting")
span.emoji-icon 👁️
| 👁️
| {{_ 'tableVisibilityMode'}}
li(class="{{#if isAnnouncementSetting}}active{{/if}}")
a.js-setting-menu(data-id="announcement-setting")
span.emoji-icon 📢
| 📢
| {{_ 'admin-announcement'}}
li(class="{{#if isAccessibilitySetting}}active{{/if}}")
a.js-setting-menu(data-id="accessibility-setting")
span.emoji-icon
|
| {{_ 'accessibility'}}
li(class="{{#if isLayoutSetting}}active{{/if}}")
a.js-setting-menu(data-id="layout-setting")
span.emoji-icon 🔗
| 🔗
| {{_ 'layout'}}
li(class="{{#if isWebhookSetting}}active{{/if}}")
a.js-setting-menu(data-id="webhook-setting")
span.emoji-icon 🌐
| 🌐
| {{_ 'global-webhook'}}
li(class="{{#if isAttachmentSettings}}active{{/if}}")
a.js-setting-menu(data-id="attachment-settings")
span.emoji-icon 📎
| 📎
| {{_ 'attachments'}}
li(class="{{#if isCronSettings}}active{{/if}}")
a.js-setting-menu(data-id="cron-settings")
span.emoji-icon
|
| {{_ 'cron'}}
.main-body
if isLoading
@ -170,10 +170,7 @@ template(name="setting")
label {{_ 'migration-status'}}
.status-indicator
span.status-label {{_ 'status'}}:
span.status-value {{#if isMigrating}}{{migrationStatus}}{{else}}{{_ 'idle'}}{{/if}}
.current-step(class="{{#unless migrationCurrentStep}}hide{{/unless}}")
span.step-label {{_ 'current-step'}}:
span.step-value {{migrationCurrentStep}}
span.status-value {{migrationStatus}}
.progress-section
.progress
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
@ -182,13 +179,9 @@ template(name="setting")
| {{migrationProgress}}% {{_ 'complete'}}
.form-group
button.js-start-all-migrations.btn.btn-primary {{#if isMigrating}}disabled{{/if}} {{_ 'start-all-migrations'}}
button.js-pause-all-migrations.btn.btn-warning {{#unless isMigrating}}disabled{{/unless}} {{_ 'pause-all-migrations'}}
button.js-stop-all-migrations.btn.btn-danger {{#unless isMigrating}}disabled{{/unless}} {{_ 'stop-all-migrations'}}
li
h3 {{_ 'migration-steps'}}
p Migration steps section temporarily removed
button.js-start-all-migrations.btn.btn-primary {{_ 'start-all-migrations'}}
button.js-pause-all-migrations.btn.btn-warning {{_ 'pause-all-migrations'}}
button.js-stop-all-migrations.btn.btn-danger {{_ 'stop-all-migrations'}}
li
h3 {{_ 'board-operations'}}
@ -207,7 +200,7 @@ template(name="setting")
.job-info
.job-name {{name}}
.job-schedule {{schedule}}
.job-status {{status}}
.job-description {{description}}
.job-actions
button.js-pause-job.btn.btn-sm.btn-warning(data-job-id="{{_id}}") {{_ 'pause'}}
button.js-delete-job.btn.btn-sm.btn-danger(data-job-id="{{_id}}") {{_ 'delete'}}
@ -281,7 +274,7 @@ template(name='email')
// li.smtp-form
// .title {{_ 'smtp-username'}}
// .form-group
// input.wekan-form-control#mail-server-username(type="text", placeholder="{{_ 'username'}}" value="{{currentSetting.mailServer.username}}")
// input.wekan-form-control#mail-server-u"accounts-allowUserNameChange": "Allow Username Change",sername(type="text", placeholder="{{_ 'username'}}" value="{{currentSetting.mailServer.username}}")
// li.smtp-form
// .title {{_ 'smtp-password'}}
// .form-group
@ -382,29 +375,6 @@ template(name='layoutSettings')
ul#layout-setting.setting-detail
li
button.js-all-boards-hide-activities.primary {{_ 'hide-activities-of-all-boards'}}
li
a(href="/support" style="text-decoration: underline; color: blue;") {{_ 'support'}}
li
a.flex.js-toggle-support
.materialCheckBox(class="{{#if currentSetting.supportPageEnabled}}is-checked{{/if}}")
span {{_ 'support-page-enabled'}}
li
.support-content(class="{{#if currentSetting.supportPageEnabled}}{{else}}hide{{/if}}")
ul
li
a.flex.js-toggle-support-public
.materialCheckBox(class="{{#if currentSetting.supportPagePublic}}is-checked{{/if}}")
span {{_ 'public'}}
li
.title {{_ 'support-title'}}
textarea#support-title.wekan-form-control= currentSetting.supportTitle
li
.title {{_ 'support-content'}}
textarea#support-page-text.wekan-form-control= currentSetting.supportPageText
li
button.js-support-save.primary {{_ 'save'}}
li.layout-form
.title {{_ 'oidc-button-text'}}
.form-group

View file

@ -2,14 +2,6 @@ import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { ALLOWED_WAIT_SPINNERS } from '/config/const';
import LockoutSettings from '/models/lockoutSettings';
import {
cronMigrationProgress,
cronMigrationStatus,
cronMigrationCurrentStep,
cronMigrationSteps,
cronIsMigrating,
cronJobs
} from '/imports/cronMigrationClient';
BlazeComponent.extendComponent({
@ -123,27 +115,15 @@ BlazeComponent.extendComponent({
// Cron settings helpers
migrationStatus() {
return cronMigrationStatus.get() || TAPi18n.__('idle');
return TAPi18n.__('idle'); // Placeholder
},
migrationProgress() {
return cronMigrationProgress.get() || 0;
},
migrationCurrentStep() {
return cronMigrationCurrentStep.get() || '';
},
isMigrating() {
return cronIsMigrating.get() || false;
},
migrationSteps() {
return cronMigrationSteps.get() || [];
return 0; // Placeholder
},
cronJobs() {
return cronJobs.get() || [];
return []; // Placeholder
},
setLoading(w) {
@ -189,9 +169,7 @@ BlazeComponent.extendComponent({
// Event handlers for cron settings
'click button.js-start-all-migrations'(event) {
event.preventDefault();
this.setLoading(true);
Meteor.call('cron.startAllMigrations', (error, result) => {
this.setLoading(false);
Meteor.call('startAllMigrations', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
} else {
@ -202,9 +180,7 @@ BlazeComponent.extendComponent({
'click button.js-pause-all-migrations'(event) {
event.preventDefault();
this.setLoading(true);
Meteor.call('cron.pauseAllMigrations', (error, result) => {
this.setLoading(false);
Meteor.call('pauseAllMigrations', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
} else {
@ -216,9 +192,7 @@ BlazeComponent.extendComponent({
'click button.js-stop-all-migrations'(event) {
event.preventDefault();
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
this.setLoading(true);
Meteor.call('cron.stopAllMigrations', (error, result) => {
this.setLoading(false);
Meteor.call('stopAllMigrations', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
} else {
@ -230,28 +204,41 @@ BlazeComponent.extendComponent({
'click button.js-schedule-board-cleanup'(event) {
event.preventDefault();
// Placeholder - board cleanup scheduling
alert(TAPi18n.__('board-cleanup-scheduled'));
Meteor.call('scheduleBoardCleanup', (error, result) => {
if (error) {
alert(TAPi18n.__('board-cleanup-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('board-cleanup-scheduled'));
}
});
},
'click button.js-schedule-board-archive'(event) {
event.preventDefault();
// Placeholder - board archive scheduling
alert(TAPi18n.__('board-archive-scheduled'));
Meteor.call('scheduleBoardArchive', (error, result) => {
if (error) {
alert(TAPi18n.__('board-archive-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('board-archive-scheduled'));
}
});
},
'click button.js-schedule-board-backup'(event) {
event.preventDefault();
// Placeholder - board backup scheduling
alert(TAPi18n.__('board-backup-scheduled'));
Meteor.call('scheduleBoardBackup', (error, result) => {
if (error) {
alert(TAPi18n.__('board-backup-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('board-backup-scheduled'));
}
});
},
'click button.js-pause-job'(event) {
event.preventDefault();
const jobId = $(event.target).data('job-id');
this.setLoading(true);
Meteor.call('cron.pauseJob', jobId, (error, result) => {
this.setLoading(false);
Meteor.call('pauseCronJob', jobId, (error, result) => {
if (error) {
alert(TAPi18n.__('cron-job-pause-failed') + ': ' + error.reason);
} else {
@ -264,9 +251,7 @@ BlazeComponent.extendComponent({
event.preventDefault();
const jobId = $(event.target).data('job-id');
if (confirm(TAPi18n.__('cron-job-delete-confirm'))) {
this.setLoading(true);
Meteor.call('cron.removeJob', jobId, (error, result) => {
this.setLoading(false);
Meteor.call('deleteCronJob', jobId, (error, result) => {
if (error) {
alert(TAPi18n.__('cron-job-delete-failed') + ': ' + error.reason);
} else {
@ -533,45 +518,6 @@ BlazeComponent.extendComponent({
DocHead.setTitle(productName);
},
toggleSupportPage() {
this.setLoading(true);
const supportPageEnabled = !$('.js-toggle-support .materialCheckBox').hasClass('is-checked');
$('.js-toggle-support .materialCheckBox').toggleClass('is-checked');
$('.support-content').toggleClass('hide');
Settings.update(Settings.findOne()._id, {
$set: { supportPageEnabled },
});
this.setLoading(false);
},
toggleSupportPublic() {
this.setLoading(true);
const supportPagePublic = !$('.js-toggle-support-public .materialCheckBox').hasClass('is-checked');
$('.js-toggle-support-public .materialCheckBox').toggleClass('is-checked');
Settings.update(Settings.findOne()._id, {
$set: { supportPagePublic },
});
this.setLoading(false);
},
saveSupportSettings() {
this.setLoading(true);
const supportTitle = ($('#support-title').val() || '').trim();
const supportPageText = ($('#support-page-text').val() || '').trim();
try {
Settings.update(Settings.findOne()._id, {
$set: {
supportTitle,
supportPageText,
},
});
} catch (e) {
return;
} finally {
this.setLoading(false);
}
},
sendSMTPTestEmail() {
Meteor.call('sendSMTPTestEmail', (err, ret) => {
if (!err && ret) {
@ -600,9 +546,6 @@ BlazeComponent.extendComponent({
'click a.js-toggle-hide-card-counter-list': this.toggleHideCardCounterList,
'click a.js-toggle-hide-board-member-list': this.toggleHideBoardMemberList,
'click button.js-save-layout': this.saveLayout,
'click a.js-toggle-support': this.toggleSupportPage,
'click a.js-toggle-support-public': this.toggleSupportPublic,
'click button.js-support-save': this.saveSupportSettings,
'click a.js-toggle-display-authentication-method': this
.toggleDisplayAuthenticationMethod,
},

View file

@ -5,31 +5,31 @@ template(name="settingHeaderBar")
.setting-header-btns.left
if currentUser
a.setting-header-btn.settings(href="{{pathFor 'setting'}}")
span.emoji-icon ⚙️
| ⚙️
span {{_ 'settings'}}
a.setting-header-btn.people(href="{{pathFor 'people'}}")
span.emoji-icon 👥
| 👥
span {{_ 'people'}}
a.setting-header-btn.informations(href="{{pathFor 'admin-reports'}}")
span.emoji-icon 📋
| 📋
span {{_ 'reports'}}
a.setting-header-btn.informations(href="{{pathFor 'attachments'}}")
span.emoji-icon 📎
| 📎
span {{_ 'attachments'}}
a.setting-header-btn.informations(href="{{pathFor 'translation'}}")
span.emoji-icon 🔤
| 🔤
span {{_ 'translation'}}
a.setting-header-btn.informations(href="{{pathFor 'information'}}")
span.emoji-icon
|
span {{_ 'info'}}
else
a.setting-header-btn.js-log-in(
title="{{_ 'log-in'}}")
span.emoji-icon 🚪
| 🚪
span {{_ 'log-in'}}

View file

@ -208,7 +208,7 @@ Template.newTranslationPopup.events({
Template.settingsTranslationPopup.events({
'click #deleteButton'(event) {
event.preventDefault();
Meteor.call('deleteTranslation', this.translationId);
Translation.remove(this.translationId);
Popup.back();
}
});

View file

@ -68,14 +68,6 @@
transform-origin: 100% 100% !important;
}
/* Grey checkmarks when grey icons setting is enabled */
body.grey-icons-enabled .sidebar .materialCheckBox.is-checked,
body.grey-icons-enabled .boardCardSettingsPopup .materialCheckBox.is-checked,
body.grey-icons-enabled .boardSubtaskSettingsPopup .materialCheckBox.is-checked {
border-bottom: 2px solid #7a7a7a !important;
border-right: 2px solid #7a7a7a !important;
}
/* Card Settings 3-column grid layout */
.card-settings-grid {
display: grid;
@ -138,11 +130,6 @@ body.grey-icons-enabled .boardSubtaskSettingsPopup .materialCheckBox.is-checked
}
.sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-check {
margin: 0 4px;
color: #3cb500;
}
/* Grey check icons when grey icons setting is enabled */
body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-check {
color: #7a7a7a;
}
.sidebar .sidebar-content ul.sidebar-list li .minicard {
padding: 6px 8px 4px;
@ -162,9 +149,6 @@ body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-
border-radius: 3px;
background: #e6e6e6;
}
.sidebar .sidebar-content .sidebar-btn * {
color: #fff;
}
.sidebar .sidebar-content .sidebar-btn:hover * {
color: #fff;
}

View file

@ -155,7 +155,7 @@ template(name="boardChangeColorPopup")
span.background-box(class="board-color-{{this}}")
span {{this}}
if isSelected
span.checkmark-no-grey
|
template(name="boardChangeBackgroundImagePopup")
form
@ -575,8 +575,7 @@ template(name="boardMenuPopup")
if currentUser.isBoardAdmin
li
a.js-open-rules-view(title="{{_ 'rules'}}")
span.emoji-icon
| ✨
| ✨
| {{_ 'rules'}}
if currentUser.isBoardAdmin
li
@ -588,6 +587,10 @@ template(name="boardMenuPopup")
| 📦
| {{_ 'archived-items'}}
if currentUser.isBoardAdmin
li
a.js-open-migrations
| 🔧
| {{_ 'migrations'}}
li
a.js-change-board-color
| 🎨
@ -634,8 +637,7 @@ template(name="boardMenuPopup")
// | {{_ 'delete-duplicate-lists'}}
li
a.js-archive-board
span.emoji-icon
| ➡️📦
| ➡️📦
| {{_ 'archive-board'}}
template(name="exportBoard")
@ -807,12 +809,6 @@ template(name="changePermissionsPopup")
if isNormal
| ✅
span.sub-name {{_ 'normal-desc'}}
li
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-normal-assigned-only{{/if}}")
| {{_ 'normal-assigned-only'}}
if isNormalAssignedOnly
| ✅
span.sub-name {{_ 'normal-assigned-only-desc'}}
li
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-no-comments{{/if}}")
| {{_ 'no-comments'}}
@ -825,24 +821,6 @@ template(name="changePermissionsPopup")
if isCommentOnly
| ✅
span.sub-name {{_ 'comment-only-desc'}}
li
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-comment-assigned-only{{/if}}")
| {{_ 'comment-assigned-only'}}
if isCommentAssignedOnly
| ✅
span.sub-name {{_ 'comment-assigned-only-desc'}}
li
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-read-only{{/if}}")
| {{_ 'read-only'}}
if isReadOnly
| ✅
span.sub-name {{_ 'read-only-desc'}}
li
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-read-assigned-only{{/if}}")
| {{_ 'read-assigned-only'}}
if isReadAssignedOnly
| ✅
span.sub-name {{_ 'read-assigned-only-desc'}}
li
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-worker{{/if}}")
| {{_ 'worker'}}

View file

@ -13,6 +13,7 @@ const viewTitles = {
multiselection: 'multi-selection',
customFields: 'custom-fields',
archives: 'archives',
migrations: 'migrations',
};
BlazeComponent.extendComponent({
@ -238,20 +239,8 @@ Template.memberPopup.helpers({
const commentOnly = currentBoard.hasCommentOnly(this.userId);
const noComments = currentBoard.hasNoComments(this.userId);
const worker = currentBoard.hasWorker(this.userId);
const normalAssignedOnly = currentBoard.hasNormalAssignedOnly(this.userId);
const commentAssignedOnly = currentBoard.hasCommentAssignedOnly(this.userId);
const readOnly = currentBoard.hasReadOnly(this.userId);
const readAssignedOnly = currentBoard.hasReadAssignedOnly(this.userId);
if (readAssignedOnly) {
return TAPi18n.__('read-assigned-only');
} else if (readOnly) {
return TAPi18n.__('read-only');
} else if (commentAssignedOnly) {
return TAPi18n.__('comment-assigned-only');
} else if (commentOnly) {
if (commentOnly) {
return TAPi18n.__('comment-only');
} else if (normalAssignedOnly) {
return TAPi18n.__('normal-assigned-only');
} else if (noComments) {
return TAPi18n.__('no-comments');
} else if (worker) {
@ -283,6 +272,10 @@ Template.boardMenuPopup.events({
Sidebar.setView('archives');
Popup.back();
},
'click .js-open-migrations'() {
Sidebar.setView('migrations');
Popup.back();
},
'click .js-change-board-color': Popup.open('boardChangeColor'),
'click .js-change-background-image': Popup.open('boardChangeBackgroundImage'),
'click .js-board-info-on-my-boards': Popup.open('boardInfoOnMyBoards'),
@ -401,7 +394,6 @@ Template.memberPopup.events({
FlowRouter.go('home');
});
}),
});
Template.removeMemberPopup.helpers({
@ -1933,7 +1925,7 @@ Template.removeBoardTeamPopup.helpers({
});
Template.changePermissionsPopup.events({
'click .js-set-admin, click .js-set-normal, click .js-set-normal-assigned-only, click .js-set-no-comments, click .js-set-comment-only, click .js-set-comment-assigned-only, click .js-set-read-only, click .js-set-read-assigned-only, click .js-set-worker'(
'click .js-set-admin, click .js-set-normal, click .js-set-no-comments, click .js-set-comment-only, click .js-set-worker'(
event,
) {
const currentBoard = Utils.getCurrentBoard();
@ -1942,14 +1934,6 @@ Template.changePermissionsPopup.events({
const isCommentOnly = $(event.currentTarget).hasClass(
'js-set-comment-only',
);
const isNormalAssignedOnly = $(event.currentTarget).hasClass(
'js-set-normal-assigned-only',
);
const isCommentAssignedOnly = $(event.currentTarget).hasClass(
'js-set-comment-assigned-only',
);
const isReadOnly = $(event.currentTarget).hasClass('js-set-read-only');
const isReadAssignedOnly = $(event.currentTarget).hasClass('js-set-read-assigned-only');
const isNoComments = $(event.currentTarget).hasClass('js-set-no-comments');
const isWorker = $(event.currentTarget).hasClass('js-set-worker');
currentBoard.setMemberPermission(
@ -1958,10 +1942,6 @@ Template.changePermissionsPopup.events({
isNoComments,
isCommentOnly,
isWorker,
isNormalAssignedOnly,
isCommentAssignedOnly,
isReadOnly,
isReadAssignedOnly,
);
Popup.back(1);
},
@ -1979,22 +1959,10 @@ Template.changePermissionsPopup.helpers({
!currentBoard.hasAdmin(this.userId) &&
!currentBoard.hasNoComments(this.userId) &&
!currentBoard.hasCommentOnly(this.userId) &&
!currentBoard.hasNormalAssignedOnly(this.userId) &&
!currentBoard.hasCommentAssignedOnly(this.userId) &&
!currentBoard.hasReadOnly(this.userId) &&
!currentBoard.hasReadAssignedOnly(this.userId) &&
!currentBoard.hasWorker(this.userId)
);
},
isNormalAssignedOnly() {
const currentBoard = Utils.getCurrentBoard();
return (
!currentBoard.hasAdmin(this.userId) &&
currentBoard.hasNormalAssignedOnly(this.userId)
);
},
isNoComments() {
const currentBoard = Utils.getCurrentBoard();
return (
@ -2011,30 +1979,6 @@ Template.changePermissionsPopup.helpers({
);
},
isCommentAssignedOnly() {
const currentBoard = Utils.getCurrentBoard();
return (
!currentBoard.hasAdmin(this.userId) &&
currentBoard.hasCommentAssignedOnly(this.userId)
);
},
isReadOnly() {
const currentBoard = Utils.getCurrentBoard();
return (
!currentBoard.hasAdmin(this.userId) &&
currentBoard.hasReadOnly(this.userId)
);
},
isReadAssignedOnly() {
const currentBoard = Utils.getCurrentBoard();
return (
!currentBoard.hasAdmin(this.userId) &&
currentBoard.hasReadAssignedOnly(this.userId)
);
},
isWorker() {
const currentBoard = Utils.getCurrentBoard();
return (

View file

@ -36,7 +36,7 @@ template(name="filterSidebar")
else
span.quiet {{_ "label-default" (_ (concat "color-" color))}}
if Filter.labelIds.isSelected _id
| ✅
i.fa.fa-check
hr
h3
| 👥
@ -68,7 +68,7 @@ template(name="filterSidebar")
span.sidebar-list-item-description
| {{_ 'filter-no-assignee'}}
if Filter.assignees.isSelected undefined
| ✅
i.fa.fa-check
each currentBoard.activeMembers
with getUser userId
li(class="{{#if Filter.assignees.isSelected _id}}active{{/if}}")
@ -90,37 +90,37 @@ template(name="filterSidebar")
span.sidebar-list-item-description
| {{_ 'filter-no-due-date' }}
if Filter.dueAt.isSelected 'noDate'
| ✅
i.fa.fa-check
li(class="{{#if Filter.dueAt.isSelected 'past'}}active{{/if}}")
a.name.js-toggle-overdue-filter
span.sidebar-list-item-description
| {{_ 'filter-overdue' }}
if Filter.dueAt.isSelected 'past'
| ✅
i.fa.fa-check
li(class="{{#if Filter.dueAt.isSelected 'today'}}active{{/if}}")
a.name.js-toggle-due-today-filter
span.sidebar-list-item-description
| {{_ 'filter-due-today' }}
if Filter.dueAt.isSelected 'today'
| ✅
i.fa.fa-check
li(class="{{#if Filter.dueAt.isSelected 'tomorrow'}}active{{/if}}")
a.name.js-toggle-due-tomorrow-filter
span.sidebar-list-item-description
| {{_ 'filter-due-tomorrow' }}
if Filter.dueAt.isSelected 'tomorrow'
| ✅
i.fa.fa-check
li(class="{{#if Filter.dueAt.isSelected 'thisweek'}}active{{/if}}")
a.name.js-toggle-due-this-week-filter
span.sidebar-list-item-description
| {{_ 'filter-due-this-week' }}
if Filter.dueAt.isSelected 'thisweek'
| ✅
i.fa.fa-check
li(class="{{#if Filter.dueAt.isSelected 'nextweek'}}active{{/if}}")
a.name.js-toggle-due-next-week-filter
span.sidebar-list-item-description
| {{_ 'filter-due-next-week' }}
if Filter.dueAt.isSelected 'nextweek'
| ✅
i.fa.fa-check
hr
h3
| 📋
@ -138,7 +138,7 @@ template(name="filterSidebar")
span.sidebar-list-item-description
| {{ name }}
if Filter.customFields.isSelected _id
| ✅
i.fa.fa-check
hr
h3
| {{_ 'other-filters-label'}}
@ -148,14 +148,14 @@ template(name="filterSidebar")
span.sidebar-list-item-description
| {{_ 'filter-show-archive'}}
if Filter.archive.isSelected _id
| ✅
i.fa.fa-check
ul.sidebar-list
li(class="{{#if Filter.hideEmpty.isSelected _id}}active{{/if}}")
a.name.js-toggle-hideEmpty-filter
span.sidebar-list-item-description
| {{_ 'filter-hide-empty'}}
if Filter.hideEmpty.isSelected _id
| ✅
i.fa.fa-check
hr
h3 {{_ 'advanced-filter-label'}}
input.js-field-advanced-filter(type="text")
@ -206,12 +206,6 @@ template(name="multiselectionSidebar")
| ⋯
if currentUser.isBoardAdmin
hr
a.sidebar-btn.js-selection-color
| 🎨
span {{_ 'selection-color'}}
a.sidebar-btn.js-copy-selection
| 📋
span {{_ 'copy-selection'}}
a.sidebar-btn.js-move-selection
| 📤
span {{_ 'move-selection'}}
@ -230,76 +224,4 @@ template(name="disambiguateMultiMemberPopup")
button.wide.js-assign-member {{_ 'assign-member'}}
template(name="moveSelectionPopup")
h3 {{_ 'moveSelectionPopup-title'}}
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}}. {{title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'cards'}}:
select.js-select-cards
each cards
option(value="{{_id}}") {{add @index 1}}. {{title}}
div
input(type="radio" name="position" value="above" checked id="position-above-move" style="display: inline")
label(for="position-above-move") {{_ 'above-selected-card'}}
div
input(type="radio" name="position" value="below" id="position-below-move" style="display: inline")
label(for="position-below-move") {{_ 'below-selected-card'}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
template(name="copySelectionPopup")
h3 {{_ 'copySelectionPopup-title'}}
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}}. {{title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'cards'}}:
select.js-select-cards
each cards
option(value="{{_id}}") {{add @index 1}}. {{title}}
div
input(type="radio" name="position" value="above" checked id="position-above-copy" style="display: inline")
label(for="position-above-copy") {{_ 'above-selected-card'}}
div
input(type="radio" name="position" value="below" id="position-below-copy" style="display: inline")
label(for="position-below-copy") {{_ 'below-selected-card'}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
template(name="setSelectionColorPopup")
h3 {{_ 'setSelectionColorPopup-title'}}
form.edit-label
.palette-colors: each colors
unless $eq color 'white'
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color)
| ✅
button.primary.confirm.js-submit {{_ 'save'}}
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
+boardLists

View file

@ -162,8 +162,6 @@ BlazeComponent.extendComponent({
}
},
'click .js-move-selection': Popup.open('moveSelection'),
'click .js-copy-selection': Popup.open('copySelection'),
'click .js-selection-color': Popup.open('setSelectionColor'),
'click .js-archive-selection'() {
mutateSelectedCards('archive');
EscapeActions.executeUpTo('multiselection');
@ -204,267 +202,10 @@ Template.disambiguateMultiMemberPopup.events({
},
});
Template.moveSelectionPopup.onCreated(function() {
this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
this.selectedSwimlaneId = new ReactiveVar('');
this.selectedListId = new ReactiveVar('');
this.selectedCardId = new ReactiveVar('');
this.position = new ReactiveVar('above');
this.getBoardData = function(boardId) {
const self = this;
Meteor.subscribe('board', boardId, false, {
onReady() {
const sameBoardId = self.selectedBoardId.get() === boardId;
self.selectedBoardId.set(boardId);
if (!sameBoardId) {
self.setFirstSwimlaneId();
self.setFirstListId();
}
},
});
};
this.setFirstSwimlaneId = function() {
try {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
const swimlaneId = board.swimlanes()[0]._id;
this.selectedSwimlaneId.set(swimlaneId);
} catch (e) {}
};
this.setFirstListId = function() {
try {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
const listId = board.lists()[0]._id;
this.selectedListId.set(listId);
} catch (e) {}
};
this.getBoardData(Session.get('currentBoard'));
this.setFirstSwimlaneId();
this.setFirstListId();
});
Template.moveSelectionPopup.helpers({
boards() {
return ReactiveCache.getBoards(
{
archived: false,
'members.userId': Meteor.userId(),
_id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() },
},
{
sort: { sort: 1 },
},
);
},
swimlanes() {
const board = ReactiveCache.getBoard(Template.instance().selectedBoardId.get());
return board ? board.swimlanes() : [];
},
lists() {
const board = ReactiveCache.getBoard(Template.instance().selectedBoardId.get());
return board ? board.lists() : [];
},
cards() {
const instance = Template.instance();
const list = ReactiveCache.getList(instance.selectedListId.get());
if (!list) return [];
return list.cards(instance.selectedSwimlaneId.get()).sort((a, b) => a.sort - b.sort);
},
isDialogOptionBoardId(boardId) {
return Template.instance().selectedBoardId.get() === boardId;
},
isDialogOptionSwimlaneId(swimlaneId) {
return Template.instance().selectedSwimlaneId.get() === swimlaneId;
},
isDialogOptionListId(listId) {
return Template.instance().selectedListId.get() === listId;
},
});
Template.moveSelectionPopup.events({
'change .js-select-boards'(event) {
const boardId = $(event.currentTarget).val();
Template.instance().getBoardData(boardId);
},
'change .js-select-swimlanes'(event) {
Template.instance().selectedSwimlaneId.set($(event.currentTarget).val());
},
'change .js-select-lists'(event) {
Template.instance().selectedListId.set($(event.currentTarget).val());
},
'change .js-select-cards'(event) {
Template.instance().selectedCardId.set($(event.currentTarget).val());
},
'change input[name="position"]'(event) {
Template.instance().position.set($(event.currentTarget).val());
},
'click .js-done'() {
const instance = Template.instance();
const boardId = instance.selectedBoardId.get();
const swimlaneId = instance.selectedSwimlaneId.get();
const listId = instance.selectedListId.get();
const cardId = instance.selectedCardId.get();
const position = instance.position.get();
// Calculate sortIndex
let sortIndex = 0;
if (cardId) {
const targetCard = ReactiveCache.getCard(cardId);
if (targetCard) {
if (position === 'above') {
sortIndex = targetCard.sort - 0.5;
} else {
sortIndex = targetCard.sort + 0.5;
}
}
} else {
// If no card selected, move to end
const board = ReactiveCache.getBoard(boardId);
const cards = board.cards({ swimlaneId, listId }).sort('sort');
if (cards.length > 0) {
sortIndex = cards[cards.length - 1].sort + 1;
}
}
mutateSelectedCards('move', boardId, swimlaneId, listId, sortIndex);
EscapeActions.executeUpTo('multiselection');
},
});
Template.copySelectionPopup.onCreated(function() {
this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
this.selectedSwimlaneId = new ReactiveVar('');
this.selectedListId = new ReactiveVar('');
this.selectedCardId = new ReactiveVar('');
this.position = new ReactiveVar('above');
this.getBoardData = function(boardId) {
const self = this;
Meteor.subscribe('board', boardId, false, {
onReady() {
const sameBoardId = self.selectedBoardId.get() === boardId;
self.selectedBoardId.set(boardId);
if (!sameBoardId) {
self.setFirstSwimlaneId();
self.setFirstListId();
}
},
});
};
this.setFirstSwimlaneId = function() {
try {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
const swimlaneId = board.swimlanes()[0]._id;
this.selectedSwimlaneId.set(swimlaneId);
} catch (e) {}
};
this.setFirstListId = function() {
try {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
const listId = board.lists()[0]._id;
this.selectedListId.set(listId);
} catch (e) {}
};
this.getBoardData(Session.get('currentBoard'));
this.setFirstSwimlaneId();
this.setFirstListId();
});
Template.copySelectionPopup.helpers({
boards() {
return ReactiveCache.getBoards(
{
archived: false,
'members.userId': Meteor.userId(),
_id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() },
},
{
sort: { sort: 1 },
},
);
},
swimlanes() {
const board = ReactiveCache.getBoard(Template.instance().selectedBoardId.get());
return board ? board.swimlanes() : [];
},
lists() {
const board = ReactiveCache.getBoard(Template.instance().selectedBoardId.get());
return board ? board.lists() : [];
},
cards() {
const instance = Template.instance();
const list = ReactiveCache.getList(instance.selectedListId.get());
if (!list) return [];
return list.cards(instance.selectedSwimlaneId.get()).sort((a, b) => a.sort - b.sort);
},
isDialogOptionBoardId(boardId) {
return Template.instance().selectedBoardId.get() === boardId;
},
isDialogOptionSwimlaneId(swimlaneId) {
return Template.instance().selectedSwimlaneId.get() === swimlaneId;
},
isDialogOptionListId(listId) {
return Template.instance().selectedListId.get() === listId;
},
});
Template.copySelectionPopup.events({
'change .js-select-boards'(event) {
const boardId = $(event.currentTarget).val();
Template.instance().getBoardData(boardId);
},
'change .js-select-swimlanes'(event) {
Template.instance().selectedSwimlaneId.set($(event.currentTarget).val());
},
'change .js-select-lists'(event) {
Template.instance().selectedListId.set($(event.currentTarget).val());
},
'change .js-select-cards'(event) {
Template.instance().selectedCardId.set($(event.currentTarget).val());
},
'change input[name="position"]'(event) {
Template.instance().position.set($(event.currentTarget).val());
},
'click .js-done'() {
const instance = Template.instance();
const boardId = instance.selectedBoardId.get();
const swimlaneId = instance.selectedSwimlaneId.get();
const listId = instance.selectedListId.get();
const cardId = instance.selectedCardId.get();
const position = instance.position.get();
mutateSelectedCards((card) => {
const newCard = card.copy(boardId, swimlaneId, listId);
if (newCard) {
let sortIndex = 0;
if (cardId) {
const targetCard = ReactiveCache.getCard(cardId);
if (targetCard) {
if (position === 'above') {
sortIndex = targetCard.sort - 0.5;
} else {
sortIndex = targetCard.sort + 0.5;
}
}
} else {
// To end
const board = ReactiveCache.getBoard(boardId);
const cards = board.cards({ swimlaneId, listId }).sort('sort');
if (cards.length > 0) {
sortIndex = cards[cards.length - 1].sort + 1;
}
}
newCard.setSort(sortIndex);
}
});
'click .js-select-list'() {
// Move the minicard to the end of the target list
mutateSelectedCards('moveToEndOfList', { listId: this._id });
EscapeActions.executeUpTo('multiselection');
},
});

View file

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

View file

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

View file

@ -26,21 +26,25 @@ template(name="swimlaneFixedHeader")
if currentUser
unless currentUser.isCommentOnly
unless currentUser.isWorker
a.swimlane-collapse-indicator.js-collapse-swimlane.swimlane-header-collapse(title="{{_ 'collapse'}}")
if collapseSwimlane
| ▶
else
| 🔽
a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
|
a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
| ☰
//// TODO: Collapse Swimlane: make button working, etc.
//unless collapsed
// a.js-collapse-swimlane(title="{{_ 'collapse'}}")
// i.fa.fa-arrow-down.swimlane-header-collapse-down
// ⬆️.swimlane-header-collapse-up
//if collapsed
// a.js-collapse-swimlane(title="{{_ 'uncollapse'}}")
// ⬆️.swimlane-header-collapse-up
// i.fa.fa-arrow-down.swimlane-header-collapse-down
unless isTouchScreen
a.swimlane-header-handle.handle.js-swimlane-header-handle
| ↕️
if isTouchScreen
a.swimlane-header-miniscreen-handle.handle.js-swimlane-header-handle
| ↕️
a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
| ☰
template(name="editSwimlaneTitleForm")
.list-composer

View file

@ -20,14 +20,13 @@ BlazeComponent.extendComponent({
},
collapsed(check = undefined) {
const swimlane = Template.currentData();
const status = Utils.getSwimlaneCollapseState(swimlane);
const status = swimlane.isCollapsed();
if (check === undefined) {
// just check
return status;
} else {
const next = typeof check === 'boolean' ? check : !status;
Utils.setSwimlaneCollapseState(swimlane, next);
return next;
swimlane.collapse(!status);
return !status;
}
},
@ -50,10 +49,6 @@ Template.swimlaneFixedHeader.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
collapseSwimlane() {
const swimlane = Template.currentData();
return Utils.getSwimlaneCollapseState(swimlane);
},
isTitleDefault(title) {
// https://github.com/wekan/wekan/issues/4763
// https://github.com/wekan/wekan/issues/4742

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