Compare commits

..

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

353 changed files with 7024 additions and 46377 deletions

1
.github/FUNDING.yml vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
[main] [main]
host = https://www.transifex.com 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] [o:wekan:p:wekan:r:application]
file_filter = imports/i18n/data/<lang>.i18n.json file_filter = imports/i18n/data/<lang>.i18n.json

View file

@ -22,286 +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. 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. 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),
[Part 3](https://github.com/wekan/wekan/commit/9431b2d53014289bebb06567f5662fdcb6dd409c).
Thanks to juri_ at WeKan Libera.Chat IRC and xet7.
- [Added s390x firewall Docs](https://github.com/wekan/wekan/commit/ec7c0e6dc3641f43b1a110d285f6ef15c146584a).
Thanks to 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:
- [Feature: Workspaces, at All Boards page](https://github.com/wekan/wekan/commit/0afbdc95b49537e06b4f9cf98f51a669ef249384).
Thanks to xet7.
and fixes the following bugs:
- [Fix 8.16: Switching Board View fails with 403 error](https://github.com/wekan/wekan/commit/550d87ac6cb3ec946600616485afdbd242983ab4).
Thanks to xet7.
- [Moved migrations from opening board to right sidebar / Migrations](https://github.com/wekan/wekan/commit/1b25d1d5720d4f486a10d2acce37e315cf9b6057).
Thanks to xet7.
- [Fix 8.16 Lists with no items are deleted every time when board is opened. Moved migrations to right sidebar](https://github.com/wekan/wekan/commit/7713e613b431e44dc13cee72e7a1e5f031473fa6).
Thanks to xet7.
- [Remove old translations and code not in use anymore](https://github.com/wekan/wekan/commit/ba49d4d140bc0d4cfb5a96db9ab077bc85db58f1).
Thanks to xet7.
- [Fixed sidebar migrations to be per-board, not global. Clarified translations](https://github.com/wekan/wekan/commit/e4638d5fbcbe004ac393462331805cac3ba25097).
Thanks to xet7.
- [Fix star board](https://github.com/wekan/wekan/commit/8711b476be30496b96b845529b5717bb6e685c27).
Thanks to xet7.
- [Fix Card emoji issues](https://github.com/wekan/wekan/commit/e5e711c938edcca23c974c3eec97296898bcf24e).
Thanks to xet7.
- [Try to fix Edit Custom Fields button not working. Removed duplicate option from Boards Settings](https://github.com/wekan/wekan/commit/20af0a2ef55b11e7205845859ee92a929616ce91).
Thanks to xet7.
- [Fix Regression - calendar popup to set due date has gone](https://github.com/wekan/wekan/commit/581733d605b7e0494e72229c45947cff134f6dd6).
Thanks to xet7.
- [Remove not working Bookmark menu option](https://github.com/wekan/wekan/commit/c829c073cf822e48b7cd84bbfb79d42867412517).
Thanks to xet7.
- [Fix Workspaces at All Boards to have correct count of remaining etc, while starred also at Starred/Favorites](https://github.com/wekan/wekan/commit/6244657ca53a54646ec01e702851a51d89bd0d55).
Thanks to xet7.
- [Fix Worker Permissions does not allow for cards to be moved. - v8.15. Removed buttons Worker should not use](https://github.com/wekan/wekan/commit/18003900c2d497c129793d1653d4d9872a2f19da).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.16 2025-11-02 WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUES of [Spacebleed](https://wekan.fi/hall-of-fame/spacebleed/):
- [Fix SECURITY ISSUE 1: File Attachments enables stored XSS (High)](https://github.com/wekan/wekan/commit/e9a727301d7b4f1689a703503df668c0f4f4cab8).
Thanks to Siam Thanat Hack (STH) and xet7.
- [Fix SECURITY ISSUE 2: Access to boards of any Orgs/Teams, and avatar permissions](https://github.com/wekan/wekan/commit/f26d58201855e861bab1cd1fda4d62c664efdb81).
Thanks to Siam Thanat Hack (STH) and xet7.
- [Fix SECURITY ISSUE 3: Unauthenticated (or any) user can update board sort](https://github.com/wekan/wekan/commit/ea310d7508b344512e5de0dfbc9bdfd38145c5c5).
Thanks to Siam Thanat Hack (STH) and xet7.
- [Fix SECURITY ISSUE 4: Members can forge others votes (Low). Bonus: Similar fixes to planning poker too done by xet7](https://github.com/wekan/wekan/commit/0a1a075f3153e71d9a858576f1c68d2925230d9c).
Thanks to Siam Thanat Hack (STH) and xet7.
- [Fix SECURITY ISSUE 5: Attachment API uses bearer value as userId and DoS (Low)](https://github.com/wekan/wekan/commit/ccd90343394f433b287733ad0a33c08e0a71f53c).
Thanks to Siam Thanat Hack (STH) and xet7.
and adds the following new features:
- [List menu / More / Delete duplicate lists that do not have any cards](https://github.com/wekan/wekan/commit/91b846e2cdee9154b045d11b4b4c1a7ae1d79016).
Thanks to xet7.
- [Disabled migrations that happen when opening board. Defaulting to per-swimlane lists and drag drop list to same or different swimlane](https://github.com/wekan/wekan/commit/034dc08269520ca31c780cce64e0150969e9228e).
Thanks to xet7.
and fixes the following bugs:
- [Fix changing swimlane color to not reload webpage](https://github.com/wekan/wekan/commit/ecf2418347cae4329deb292b534f68eb099d3f90).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.15 2025-10-23 WeKan ® release
This release fixes the following bugs:
- Fix drag lists did not work
[Part 1](https://github.com/wekan/wekan/commit/8662c96d1c8d4fa76ce7b31eb06678ad59c3ebe1),
[Part 2](https://github.com/wekan/wekan/commit/0cebd8aa4dbe0bf2418b814716744ab806b671c2).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.14 2025-10-23 WeKan ® release
This release fixes the following bugs:
- [Fix board reloading page every second](https://github.com/wekan/wekan/commit/b4b598f542d0cefc5f2d5d6c7286f0a312cf6a55).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.12 2025-10-23 WeKan ® release
This release fixes the following bugs:
- [Fix Regression - unable to view cards by due date v8.11](https://github.com/wekan/wekan/commit/ae11e80bde79d9ad412d185f20e5a7f802685260).
Thanks to xet7.
- [Fix Regression - unable to rearrange tasks within a checklist - v8.11](https://github.com/wekan/wekan/commit/544b24ceb1687e5b568d8c7b74403a5a2e3f6bc6).
Thanks to xet7.
- [Fix unable to add members to board](https://github.com/wekan/wekan/commit/c6d46006837a29fb311e444f94fa65f236e23bc7).
Thanks to xet7.
- [Removed not needed | at left side of minicard badges](https://github.com/wekan/wekan/commit/a0c30c35ed57113df041ef1020d3e9e5449f35e4).
Thanks to xet7.
- [Fix opened card Date Format to be used at dates popups](https://github.com/wekan/wekan/commit/7ca81285b14d1ec60d6e7e9c191d1194950f18c8).
Thanks to xet7.
- [Fix UI issues of Right Sidebar / Subtasks Settings and Card Settings](https://github.com/wekan/wekan/commit/45537ede870eca59ad72cd7ad013a12f60032df4).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.11 2025-10-21 WeKan ® release # v8.11 2025-10-21 WeKan ® release
This release fixes the following bugs: This release fixes the following bugs:

View file

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

View file

@ -1,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) We thank you with a place at our hall of fame page, that is at https://wekan.fi/hall-of-fame
- Send info about security issue ONLY to security@wekan.fi (that is Protomail email address). NOT TO ANYWHERE ELSE. NO CC, NO BCC.
- Wait for new WeKan release that fixes security issue
- If you approve, we thank you by adding you to Hall of Fame: https://wekan.fi/hall-of-fame/
## Bonus Points
- If you include code for fixing security issue
## Losing Points
- If you ask about [bounty](CONTRIBUTING.md). There is no bounty. WeKan is NOT Big Tech. WeKan is FLOSS.
- If you forget to include vulnerability details.
- If you send info about security issue to somewhere else than security@wekan.fi
## How should reports be formatted? ## How should reports be formatted?
@ -34,7 +26,7 @@ CWSS (optional): %cwss
Anyone who reports a unique security issue in scope and does not disclose it to Anyone who reports a unique security issue in scope and does not disclose it to
a third party before we have patched and updated may be upon their approval a third party before we have patched and updated may be upon their approval
added to the WeKan Hall of Fame https://wekan.fi/hall-of-fame/ added to the Wekan Hall of Fame.
## Which domains are in scope? ## 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 - If you are thinking about TLS MITM, look at https://github.com/caddyserver/caddy/issues/2530
- Let's Encrypt TLS requires publicly accessible webserver, that Let's Encrypt TLS validation servers check. - Let's Encrypt TLS requires publicly accessible webserver, that Let's Encrypt TLS validation servers check.
- If firewall limits to only allowed IP addresses, you may need non-Let's Encrypt TLS cert. - If firewall limits to only allowed IP addresses, you may need non-Let's Encrypt TLS cert.
- For On Premise:
- https://caddyserver.com/docs/automatic-https#local-https
- https://github.com/wekan/wekan/wiki/Caddy-Webserver-Config
- https://github.com/wekan/wekan/wiki/Azure
- https://github.com/wekan/wekan/wiki/Traefik-and-self-signed-SSL-certs
## XSS ## XSS
@ -175,57 +172,6 @@ Meteor.startup(() => {
- https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312 - https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
- https://wekan.github.io/hall-of-fame/filebleed/ - https://wekan.github.io/hall-of-fame/filebleed/
### Attachments: Forced download to prevent stored XSS
- To prevent browser-side execution of uploaded content under the app origin, all attachment downloads are served with safe headers:
- `Content-Type: application/octet-stream`
- `Content-Disposition: attachment`
- `X-Content-Type-Options: nosniff`
- A restrictive `Content-Security-Policy` with `sandbox`
- This means attachments are downloaded instead of rendered inline by default. This mitigates HTML/JS/SVG based stored XSS vectors.
- Avatars and inline images remain supported but SVG uploads are blocked and never rendered inline.
## Users: Client update restrictions
- Client-side updates to user documents are limited to safe fields only:
- `username`
- `profile.*`
- Sensitive fields are blocked from any client updates and can only be modified by server methods with authorization:
- `orgs`, `teams`, `roles`, `isAdmin`, `createdThroughApi`, `loginDisabled`, `authenticationMethod`, `services.*`, `emails.*`, `sessionData.*`
- Attempts to update forbidden fields from the client are denied.
- Admin operations like managing org/team membership or toggling flags must use server methods that check permissions.
## Voting: integrity and authorization
- Client updates to card `vote` fields are blocked to prevent forged votes and inconsistent policy enforcement.
- Voting is performed via a server method that enforces:
- Authentication and board membership, or an explicit per-card flag allowing non-members to vote.
- Only the caller's own userId is added/removed from `vote.positive`/`vote.negative`.
- This prevents members from fabricating other users' votes and ensures non-members cannot vote unless explicitly allowed.
## Planning Poker: integrity and authorization
- Client updates to card `poker` fields are blocked. All poker actions go through server methods that enforce:
- Authentication and board membership for configuration and results.
- For casting a poker vote, either board membership or an explicit per-card flag allowing non-members to participate.
- Only the caller's own userId is added/removed from the selected estimation bucket (e.g., one, two, five, etc.).
- Methods cover setting/unsetting poker question/end, casting votes, replaying, and setting final estimation.
## Attachment API: authentication and DoS prevention
- The attachment API (`/api/attachment/*`) requires proper authentication using `X-User-Id` and `X-Auth-Token` headers.
- Authentication validates tokens by hashing with `Accounts._hashLoginToken` and matching against stored login tokens, preventing identity spoofing.
- Request handlers implement:
- 30-second timeout to prevent hanging connections.
- Request body size limits (50MB for uploads, 10MB for metadata operations).
- Proper error handling and guaranteed response completion.
- Request error event handlers to clean up failed connections.
- This prevents:
- DoS attacks via concurrent unauthenticated or malformed requests.
- Identity spoofing by using arbitrary bearer tokens or user IDs.
- Resource exhaustion from hanging connections or excessive payloads.
- Access control: all attachment operations verify board membership before allowing access.
## Brute force login protection ## Brute force login protection
- https://github.com/wekan/wekan/commit/23e5e1e3bd081699ce39ce5887db7e612616014d - https://github.com/wekan/wekan/commit/23e5e1e3bd081699ce39ce5887db7e612616014d
@ -272,4 +218,9 @@ Typical already known or "no impact" bugs such as:
- Email spoofing, SPF, DMARC & DKIM. Wekan does not include email server. - Email spoofing, SPF, DMARC & DKIM. Wekan does not include email server.
Wekan is Open Source with MIT license, and free to use also for commercial use. Wekan is Open Source with MIT license, and free to use also for commercial use.
We welcome all fixes to improve security by email to security@wekan.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 appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v8.19.0" appVersion: "v8.11.0"
files: files:
userUploads: userUploads:
- README.md - README.md

View file

@ -10,72 +10,8 @@ import '/client/lib/boardConverter';
import '/client/components/boardConversionProgress'; import '/client/components/boardConversionProgress';
// Import migration manager and progress UI // Import migration manager and progress UI
import '/client/lib/attachmentMigrationManager'; import '/client/lib/migrationManager';
import '/client/components/settings/migrationProgress'; import '/client/components/migrationProgress';
// Import cron settings // Import cron settings
import '/client/components/settings/cronSettings'; import '/client/components/settings/cronSettings';
// Mirror Meteor login token into a cookie for server-side file route auth
// This enables cookie-based auth for /cdn/storage/* without leaking ROOT_URL
// Token already lives in localStorage; cookie adds same-origin send-on-request semantics
Meteor.startup(() => {
const COOKIE_NAME = 'meteor_login_token';
const cookieAttrs = () => {
const attrs = ['Path=/', 'SameSite=Lax'];
try {
if (window.location && window.location.protocol === 'https:') {
attrs.push('Secure');
}
} catch (_) {}
return attrs.join('; ');
};
const setCookie = (name, value) => {
if (!value) return;
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; ${cookieAttrs()}`;
};
const clearCookie = (name) => {
document.cookie = `${encodeURIComponent(name)}=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; ${cookieAttrs()}`;
};
const syncCookie = () => {
try {
const token = Accounts && typeof Accounts._storedLoginToken === 'function' ? Accounts._storedLoginToken() : null;
if (token) setCookie(COOKIE_NAME, token); else clearCookie(COOKIE_NAME);
} catch (e) {
// ignore
}
};
// Initial sync on startup
syncCookie();
// Keep cookie in sync on login/logout
if (Accounts && typeof Accounts.onLogin === 'function') Accounts.onLogin(syncCookie);
if (Accounts && typeof Accounts.onLogout === 'function') Accounts.onLogout(syncCookie);
// Sync across tabs/windows when localStorage changes
window.addEventListener('storage', (ev) => {
if (ev && typeof ev.key === 'string' && ev.key.indexOf('Meteor.loginToken') !== -1) {
syncCookie();
}
});
});
// 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; text-decoration: none;
height: 24px; height: 24px;
} }
.comments .comment .comment-desc .reactions .open-comment-reaction-popup span { .comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-smile-o {
display: inline-block; font-size: 17px;
font-size: clamp(14px, 2vw, 18px);
font-weight: 500; font-weight: 500;
line-height: 1; margin-left: 2px;
margin-left: 4px; }
.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 { .comments .comment .comment-desc .reactions .reaction {
cursor: pointer; cursor: pointer;

View file

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

View file

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

View file

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

View file

@ -231,30 +231,6 @@
font-size: 1em !important; /* Keep original icon size */ 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 */ /* Ensure scrollbars are positioned correctly */
#content[style*="overflow-x: auto"]::-webkit-scrollbar:vertical { #content[style*="overflow-x: auto"]::-webkit-scrollbar:vertical {
width: 12px; width: 12px;
@ -287,106 +263,63 @@ body.mobile-mode.iphone-device .card-details .card-details-item-title {
animation: fadeIn 0.2s; animation: fadeIn 0.2s;
z-index: 16; 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 .open-minicard-composer,
.board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked { .board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked {
display: none; display: none;
} }
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */ /* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
.board-wrapper.mobile-view { .board-wrapper.mobile-view {
width: 100vw !important; width: 100% !important;
max-width: 100vw !important; min-width: 100% !important;
min-width: 100vw !important;
left: 0 !important; left: 0 !important;
right: 0 !important; right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
} }
.board-wrapper.mobile-view .board-canvas { .board-wrapper.mobile-view .board-canvas {
width: 100vw !important; width: 100% !important;
max-width: 100vw !important; min-width: 100% !important;
min-width: 100vw !important;
left: 0 !important; left: 0 !important;
right: 0 !important; right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
} }
.board-wrapper.mobile-view .board-canvas.mobile-view .swimlane { .board-wrapper.mobile-view .board-canvas.mobile-view .swimlane {
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important; display: flex;
flex-direction: column; flex-direction: column;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-x: hidden !important; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
width: 100vw !important; width: 100%;
max-width: 100vw !important; min-width: 100%;
min-width: 100vw !important;
} }
@media screen and (max-width: 800px), @media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) { screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
.board-wrapper { .board-wrapper {
width: 100vw !important; width: 100% !important;
max-width: 100vw !important; min-width: 100% !important;
min-width: 100vw !important;
left: 0 !important; left: 0 !important;
right: 0 !important; right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
} }
.board-wrapper .board-canvas { .board-wrapper .board-canvas {
width: 100vw !important; width: 100% !important;
max-width: 100vw !important; min-width: 100% !important;
min-width: 100vw !important;
left: 0 !important; left: 0 !important;
right: 0 !important; right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
} }
.board-wrapper .board-canvas .swimlane { .board-wrapper .board-canvas .swimlane {
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important; display: flex;
flex-direction: column; flex-direction: column;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-x: hidden !important; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
width: 100vw !important; width: 100%;
max-width: 100vw !important; min-width: 100%;
min-width: 100vw !important;
} }
} }
.calendar-event-green { .calendar-event-green {

View file

@ -1,6 +1,8 @@
template(name="board") template(name="board")
if isConverting.get if isMigrating.get
+migrationProgress
else if isConverting.get
+boardConversionProgress +boardConversionProgress
else if isBoardReady.get else if isBoardReady.get
if currentBoard if currentBoard
@ -22,7 +24,7 @@ template(name="boardBody")
// Debug information (remove in production) // Debug information (remove in production)
if debugBoardState 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;") .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-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
.board-canvas.js-swimlanes( .board-canvas.js-swimlanes(
class="{{#if hasSwimlanes}}dragscroll{{/if}}" class="{{#if hasSwimlanes}}dragscroll{{/if}}"
@ -47,8 +49,6 @@ template(name="boardBody")
+listsGroup(currentBoard) +listsGroup(currentBoard)
else if isViewCalendar else if isViewCalendar
+calendarView +calendarView
else if isViewGantt
+ganttView
else else
// Default view - show swimlanes if they exist, otherwise show lists // Default view - show swimlanes if they exist, otherwise show lists
if hasSwimlanes if hasSwimlanes
@ -56,10 +56,6 @@ template(name="boardBody")
+swimlane(this) +swimlane(this)
else else
+listsGroup(currentBoard) +listsGroup(currentBoard)
//- Render multiple open cards in desktop mode
unless isMiniScreen
each openCards
+cardDetails(this cardIndex=@index)
+sidebar +sidebar
template(name="calendarView") template(name="calendarView")
@ -67,4 +63,4 @@ template(name="calendarView")
.calendar-view.swimlane .calendar-view.swimlane
if currentCard if currentCard
+cardDetails(currentCard) +cardDetails(currentCard)
+fullcalendar(calendarOptions) +fullcalendar(calendarOptions)

View file

@ -1,9 +1,9 @@
import { ReactiveCache } from '/imports/reactiveCache'; import { ReactiveCache } from '/imports/reactiveCache';
import '../gantt/gantt.js';
import { TAPi18n } from '/imports/i18n'; import { TAPi18n } from '/imports/i18n';
import dragscroll from '@wekanteam/dragscroll'; import dragscroll from '@wekanteam/dragscroll';
import { boardConverter } from '/client/lib/boardConverter'; import { boardConverter } from '/client/lib/boardConverter';
import { formatDateByUserPreference } from '/imports/lib/dateUtils'; import { migrationManager } from '/client/lib/migrationManager';
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
import Swimlanes from '/models/swimlanes'; import Swimlanes from '/models/swimlanes';
import Lists from '/models/lists'; import Lists from '/models/lists';
@ -15,9 +15,8 @@ BlazeComponent.extendComponent({
onCreated() { onCreated() {
this.isBoardReady = new ReactiveVar(false); this.isBoardReady = new ReactiveVar(false);
this.isConverting = 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._swimlaneCreated = new Set(); // Track boards where we've created swimlanes
this._boardProcessed = false; // Track if board has been processed
this._lastProcessedBoardId = null; // Track last processed board ID
// The pattern we use to manually handle data loading is described here: // The pattern we use to manually handle data loading is described here:
// https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager // https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
@ -29,32 +28,21 @@ BlazeComponent.extendComponent({
const handle = subManager.subscribe('board', currentBoardId, false); const handle = subManager.subscribe('board', currentBoardId, false);
// Use a separate autorun for subscription ready state to avoid reactive loops Tracker.nonreactive(() => {
this.subscriptionReadyAutorun = Tracker.autorun(() => { Tracker.autorun(() => {
if (handle.ready()) { if (handle.ready()) {
if (!this._boardProcessed || this._lastProcessedBoardId !== currentBoardId) {
this._boardProcessed = true;
this._lastProcessedBoardId = currentBoardId;
// Ensure default swimlane exists (only once per board) // Ensure default swimlane exists (only once per board)
this.ensureDefaultSwimlane(currentBoardId); this.ensureDefaultSwimlane(currentBoardId);
// Check if board needs conversion // Check if board needs conversion
this.checkAndConvertBoard(currentBoardId); this.checkAndConvertBoard(currentBoardId);
} else {
this.isBoardReady.set(false);
} }
} else { });
this.isBoardReady.set(false);
}
}); });
}); });
}, },
onDestroyed() {
// Clean up the subscription ready autorun to prevent memory leaks
if (this.subscriptionReadyAutorun) {
this.subscriptionReadyAutorun.stop();
}
},
ensureDefaultSwimlane(boardId) { ensureDefaultSwimlane(boardId) {
// Only create swimlane once per board // Only create swimlane once per board
if (this._swimlaneCreated.has(boardId)) { if (this._swimlaneCreated.has(boardId)) {
@ -96,31 +84,334 @@ BlazeComponent.extendComponent({
return; return;
} }
this.isBoardReady.set(true); // Check if board needs migration based on migration version
const needsMigration = !board.migrationVersion || board.migrationVersion < 1;
if (needsMigration) {
// Start background migration for old boards
this.isMigrating.set(true);
await this.startBackgroundMigration(boardId);
this.isMigrating.set(false);
}
// Check if board needs conversion (for old structure)
if (boardConverter.isBoardConverted(boardId)) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been converted, skipping conversion`);
}
this.isBoardReady.set(true);
} else {
const needsConversion = boardConverter.needsConversion(boardId);
if (needsConversion) {
this.isConverting.set(true);
const success = await boardConverter.convertBoard(boardId);
this.isConverting.set(false);
if (success) {
this.isBoardReady.set(true);
} else {
console.error('Board conversion failed, setting ready to true anyway');
this.isBoardReady.set(true); // Still show board even if conversion failed
}
} else {
this.isBoardReady.set(true);
}
}
// Convert shared lists to per-swimlane lists if needed
await this.convertSharedListsToPerSwimlane(boardId);
// Fix missing lists migration (for cards with wrong listId references)
await this.fixMissingLists(boardId);
// Fix duplicate lists created by WeKan 8.10
await this.fixDuplicateLists(boardId);
// Start attachment migration in background if needed
this.startAttachmentMigrationIfNeeded(boardId);
} catch (error) { } catch (error) {
console.error('Error during board conversion check:', error); console.error('Error during board conversion check:', error);
this.isConverting.set(false); this.isConverting.set(false);
this.isMigrating.set(false);
this.isBoardReady.set(true); // Show board even if conversion check failed this.isBoardReady.set(true); // Show board even if conversion check failed
} }
}, },
async startBackgroundMigration(boardId) {
try {
// Start background migration using the cron system
Meteor.call('boardMigration.startBoardMigration', boardId, (error, result) => {
if (error) {
console.error('Failed to start background migration:', error);
} else {
if (process.env.DEBUG === 'true') {
console.log('Background migration started for board:', boardId);
}
}
});
} catch (error) {
console.error('Error starting background migration:', error);
}
},
async convertSharedListsToPerSwimlane(boardId) {
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) return;
// Check if board has already been processed for shared lists conversion
if (board.hasSharedListsConverted) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been processed for shared lists conversion`);
}
return;
}
// Get all lists for this board
const allLists = board.lists();
const swimlanes = board.swimlanes();
if (swimlanes.length === 0) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has no swimlanes, skipping shared lists conversion`);
}
return;
}
// Find shared lists (lists with empty swimlaneId or null swimlaneId)
const sharedLists = allLists.filter(list => !list.swimlaneId || list.swimlaneId === '');
if (sharedLists.length === 0) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has no shared lists to convert`);
}
// Mark as processed even if no shared lists
Meteor.call('boards.update', boardId, { $set: { hasSharedListsConverted: true } });
return;
}
if (process.env.DEBUG === 'true') {
console.log(`Converting ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`);
}
// Convert each shared list to per-swimlane lists
for (const sharedList of sharedLists) {
// Create a copy of the list for each swimlane
for (const swimlane of swimlanes) {
// Check if this list already exists in this swimlane
const existingList = Lists.findOne({
boardId: boardId,
swimlaneId: swimlane._id,
title: sharedList.title
});
if (!existingList) {
// 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
Meteor.call('boards.update', boardId, { $set: { hasSharedListsConverted: true } });
if (process.env.DEBUG === 'true') {
console.log(`Successfully converted ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`);
}
} catch (error) {
console.error('Error converting shared lists to per-swimlane:', error);
}
},
async fixMissingLists(boardId) {
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) return;
// Check if board has already been processed for missing lists fix
if (board.fixMissingListsCompleted) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been processed for missing lists fix`);
}
return;
}
// Check if migration is needed
const needsMigration = await new Promise((resolve, reject) => {
Meteor.call('fixMissingListsMigration.needsMigration', boardId, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
if (!needsMigration) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} does not need missing lists fix`);
}
return;
}
if (process.env.DEBUG === 'true') {
console.log(`Starting fix missing lists migration for board ${boardId}`);
}
// Execute the migration
const result = await new Promise((resolve, reject) => {
Meteor.call('fixMissingListsMigration.execute', boardId, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
if (result && result.success) {
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
Meteor.call('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
Meteor.call('boards.update', boardId, { $set: { fixDuplicateListsCompleted: true } });
} else {
// Still mark as processed to avoid repeated checks
Meteor.call('boards.update', boardId, { $set: { fixDuplicateListsCompleted: true } });
}
} catch (error) {
console.error('Error fixing duplicate lists:', error);
}
},
async startAttachmentMigrationIfNeeded(boardId) {
try {
// Check if board has already been migrated
if (attachmentMigrationManager.isBoardMigrated(boardId)) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been migrated, skipping`);
}
return;
}
// Check if there are unconverted attachments
const unconvertedAttachments = attachmentMigrationManager.getUnconvertedAttachments(boardId);
if (unconvertedAttachments.length > 0) {
if (process.env.DEBUG === 'true') {
console.log(`Starting attachment migration for ${unconvertedAttachments.length} attachments in board ${boardId}`);
}
await attachmentMigrationManager.startAttachmentMigration(boardId);
} else {
// No attachments to migrate, mark board as migrated
// This will be handled by the migration manager itself
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has no attachments to migrate`);
}
}
} catch (error) {
console.error('Error starting attachment migration:', error);
}
},
onlyShowCurrentCard() { onlyShowCurrentCard() {
const isMiniScreen = Utils.isMiniScreen(); const isMiniScreen = Utils.isMiniScreen();
const currentCardId = Utils.getCurrentCardId(true); const currentCardId = Utils.getCurrentCardId(true);
return isMiniScreen && currentCardId; 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() { goHome() {
FlowRouter.go('home'); FlowRouter.go('home');
}, },
@ -129,6 +420,10 @@ BlazeComponent.extendComponent({
return this.isConverting.get(); return this.isConverting.get();
}, },
isMigrating() {
return this.isMigrating.get();
},
isBoardReady() { isBoardReady() {
return this.isBoardReady.get(); return this.isBoardReady.get();
}, },
@ -146,50 +441,39 @@ BlazeComponent.extendComponent({
this._isDragging = false; this._isDragging = false;
// Used to set the overlay // Used to set the overlay
this.mouseHasEnterCardDetails = false; this.mouseHasEnterCardDetails = false;
this._sortFieldsFixed = new Set(); // Track which boards have had sort fields fixed
// fix swimlanes sort field if there are null values // fix swimlanes sort field if there are null values
const currentBoardData = Utils.getCurrentBoard(); const currentBoardData = Utils.getCurrentBoard();
if (currentBoardData && Swimlanes) { if (currentBoardData && Swimlanes) {
const boardId = currentBoardData._id; const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
// Only fix sort fields once per board to prevent reactive loops if (nullSortSwimlanes.length > 0) {
if (!this._sortFieldsFixed.has(`swimlanes-${boardId}`)) { const swimlanes = currentBoardData.swimlanes();
const nullSortSwimlanes = currentBoardData.nullSortSwimlanes(); let count = 0;
if (nullSortSwimlanes.length > 0) { swimlanes.forEach(s => {
const swimlanes = currentBoardData.swimlanes(); Swimlanes.update(s._id, {
let count = 0; $set: {
swimlanes.forEach(s => { sort: count,
Swimlanes.update(s._id, { },
$set: {
sort: count,
},
});
count += 1;
}); });
} count += 1;
this._sortFieldsFixed.add(`swimlanes-${boardId}`); });
} }
} }
// fix lists sort field if there are null values // fix lists sort field if there are null values
if (currentBoardData && Lists) { if (currentBoardData && Lists) {
const boardId = currentBoardData._id; const nullSortLists = currentBoardData.nullSortLists();
// Only fix sort fields once per board to prevent reactive loops if (nullSortLists.length > 0) {
if (!this._sortFieldsFixed.has(`lists-${boardId}`)) { const lists = currentBoardData.lists();
const nullSortLists = currentBoardData.nullSortLists(); let count = 0;
if (nullSortLists.length > 0) { lists.forEach(l => {
const lists = currentBoardData.lists(); Lists.update(l._id, {
let count = 0; $set: {
lists.forEach(l => { sort: count,
Lists.update(l._id, { },
$set: {
sort: count,
},
});
count += 1;
}); });
} count += 1;
this._sortFieldsFixed.add(`lists-${boardId}`); });
} }
} }
}, },
@ -580,19 +864,6 @@ BlazeComponent.extendComponent({
return boardView === 'board-view-cal'; 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() { hasSwimlanes() {
const currentBoard = Utils.getCurrentBoard(); const currentBoard = Utils.getCurrentBoard();
if (!currentBoard) { if (!currentBoard) {
@ -636,6 +907,7 @@ BlazeComponent.extendComponent({
const currentBoardId = Session.get('currentBoard'); const currentBoardId = Session.get('currentBoard');
const isBoardReady = this.isBoardReady.get(); const isBoardReady = this.isBoardReady.get();
const isConverting = this.isConverting.get(); const isConverting = this.isConverting.get();
const isMigrating = this.isMigrating.get();
const boardView = Utils.boardView(); const boardView = Utils.boardView();
if (process.env.DEBUG === 'true') { if (process.env.DEBUG === 'true') {
@ -644,6 +916,7 @@ BlazeComponent.extendComponent({
console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none'); console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none');
console.log('isBoardReady:', isBoardReady); console.log('isBoardReady:', isBoardReady);
console.log('isConverting:', isConverting); console.log('isConverting:', isConverting);
console.log('isMigrating:', isMigrating);
console.log('boardView:', boardView); console.log('boardView:', boardView);
console.log('========================'); console.log('========================');
} }
@ -654,6 +927,7 @@ BlazeComponent.extendComponent({
currentBoardTitle: currentBoard ? currentBoard.title : 'none', currentBoardTitle: currentBoard ? currentBoard.title : 'none',
isBoardReady, isBoardReady,
isConverting, isConverting,
isMigrating,
boardView boardView
}; };
}, },
@ -1020,8 +1294,3 @@ BlazeComponent.extendComponent({
} }
}, },
}).register('calendarView'); }).register('calendarView');
/**
* Gantt View Component
* Displays cards as a Gantt chart with start/due dates
*/

View file

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

View file

@ -16,6 +16,13 @@ template(name="boardHeaderBar")
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title) a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
| ✏️ | ✏️
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}" aria-label="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}")
| {{#if isStarred}}⭐{{else}}☆{{/if}}
if showStarCounter
span
= currentBoard.stars
a.board-header-btn( a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}" class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}") title="{{_ currentBoard.permission}}")
@ -31,13 +38,6 @@ template(name="boardHeaderBar")
if $eq watchLevel "muted" if $eq watchLevel "muted"
| 🔕 | 🔕
span {{_ watchLevel}} span {{_ watchLevel}}
a.board-header-btn.js-star-board(title="{{_ 'star-board'}}")
if isStarred
| ⭐
else
| ☆
if showStarCounter
span.board-star-counter {{currentBoard.stars}}
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}") a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
| {{sortCardsIcon}} | {{sortCardsIcon}}
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}} span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
@ -61,6 +61,10 @@ template(name="boardHeaderBar")
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title) a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
| ✏️ | ✏️
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
| {{#if isStarred}}⭐{{else}}☆{{/if}}
a.board-header-btn( a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}" class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}") title="{{_ currentBoard.permission}}")
@ -74,11 +78,6 @@ template(name="boardHeaderBar")
| 🔔 | 🔔
if $eq watchLevel "muted" if $eq watchLevel "muted"
| 🔕 | 🔕
a.board-header-btn.js-star-board(title="{{_ 'star-board'}}")
if isStarred
| ⭐
else
| ☆
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}") a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
| {{sortCardsIcon}} | {{sortCardsIcon}}
if isSortActive if isSortActive
@ -109,7 +108,7 @@ template(name="boardHeaderBar")
| ❌ | ❌
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}") a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
span.emoji-icon 🔍 | 🔍
unless currentBoard.isTemplatesBoard unless currentBoard.isTemplatesBoard
a.board-header-btn.js-toggle-board-view( a.board-header-btn.js-toggle-board-view(
@ -121,8 +120,6 @@ template(name="boardHeaderBar")
| 📋 | 📋
if $eq boardView 'board-view-cal' if $eq boardView 'board-view-cal'
| 📅 | 📅
if $eq boardView 'board-view-gantt'
| 📊
if canModifyBoard if canModifyBoard
a.board-header-btn.js-multiselection-activate( a.board-header-btn.js-multiselection-activate(
@ -210,13 +207,6 @@ template(name="boardChangeViewPopup")
| {{_ 'board-view-cal'}} | {{_ 'board-view-cal'}}
if $eq Utils.boardView "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") template(name="createBoard")
form form
@ -276,36 +266,6 @@ template(name="createBoardPopup")
| / | /
a.js-board-template {{_ 'template'}} a.js-board-template {{_ 'template'}}
// New popup for Template Container creation; shares the same form content
template(name="createTemplateContainerPopup")
form
label
| {{_ 'title'}}
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
if visibilityMenuIsOpen.get
+boardVisibilityList
else
p.quiet
if $eq visibility.get 'public'
span 🌐
= " "
| {{{_ 'board-public-info'}}}
else
span 🔒
= " "
| {{{_ 'board-private-info'}}}
a.js-change-visibility {{_ 'change'}}.
a.flex.js-toggle-add-template-container
.materialCheckBox#add-template-container
span {{_ 'add-template-container'}}
input.primary.wide(type="submit" value="{{_ 'create'}}")
span.quiet
| {{_ 'or'}}
a.js-import-board {{_ 'import'}}
span.quiet
| /
a.js-board-template {{_ 'template'}}
//template(name="listsortPopup") //template(name="listsortPopup")
// h2 // h2
// | {{_ 'list-sort-by'}} // | {{_ 'list-sort-by'}}

View file

@ -72,10 +72,7 @@ BlazeComponent.extendComponent({
{ {
'click .js-edit-board-title': Popup.open('boardChangeTitle'), 'click .js-edit-board-title': Popup.open('boardChangeTitle'),
'click .js-star-board'() { 'click .js-star-board'() {
const boardId = Session.get('currentBoard'); ReactiveCache.getCurrentUser().toggleBoardStar(Session.get('currentBoard'));
if (boardId) {
Meteor.call('toggleBoardStar', boardId);
}
}, },
'click .js-open-board-menu': Popup.open('boardMenu'), 'click .js-open-board-menu': Popup.open('boardMenu'),
'click .js-change-visibility': Popup.open('boardChangeVisibility'), 'click .js-change-visibility': Popup.open('boardChangeVisibility'),
@ -208,10 +205,6 @@ Template.boardChangeViewPopup.events({
Utils.setBoardView('board-view-cal'); Utils.setBoardView('board-view-cal');
Popup.back(); Popup.back();
}, },
'click .js-open-gantt-view'() {
Utils.setBoardView('board-view-gantt');
Popup.back();
},
}); });
const CreateBoard = BlazeComponent.extendComponent({ const CreateBoard = BlazeComponent.extendComponent({
@ -298,15 +291,6 @@ const CreateBoard = BlazeComponent.extendComponent({
}, },
); );
// Assign to space if one was selected
const spaceId = Session.get('createBoardInWorkspace');
if (spaceId) {
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
if (err) console.error('Error assigning board to space:', err);
});
Session.set('createBoardInWorkspace', null); // Clear after use
}
Utils.goBoardId(this.boardId.get()); Utils.goBoardId(this.boardId.get());
} else { } else {
@ -325,15 +309,6 @@ const CreateBoard = BlazeComponent.extendComponent({
boardId: this.boardId.get(), boardId: this.boardId.get(),
}); });
// Assign to space if one was selected
const spaceId = Session.get('createBoardInWorkspace');
if (spaceId) {
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
if (err) console.error('Error assigning board to space:', err);
});
Session.set('createBoardInWorkspace', null); // Clear after use
}
Utils.goBoardId(this.boardId.get()); Utils.goBoardId(this.boardId.get());
} }
}, },
@ -355,13 +330,6 @@ const CreateBoard = BlazeComponent.extendComponent({
}, },
}).register('createBoardPopup'); }).register('createBoardPopup');
(class CreateTemplateContainerPopup extends CreateBoard {
onRendered() {
// Always pre-check the template container checkbox for this popup
$('#add-template-container').addClass('is-checked');
}
}).register('createTemplateContainerPopup');
(class HeaderBarCreateBoard extends CreateBoard { (class HeaderBarCreateBoard extends CreateBoard {
onSubmit(event) { onSubmit(event) {
super.onSubmit(event); super.onSubmit(event);

View file

@ -8,273 +8,6 @@
padding: 1vh 0; padding: 1vh 0;
} }
/* Two-column layout for All Boards */
.boards-layout {
display: grid;
grid-template-columns: 260px 1fr;
gap: 16px;
}
.boards-left-menu {
border-right: 1px solid #e0e0e0;
padding-right: 12px;
}
.boards-left-menu ul.menu {
list-style: none;
padding: 0;
margin: 0 0 12px 0;
}
.boards-left-menu .menu-item {
margin: 4px 0;
}
.boards-left-menu .menu-item a {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
border-radius: 4px;
cursor: pointer;
}
.boards-left-menu .menu-item .menu-label {
flex: 1;
}
.boards-left-menu .menu-item .menu-count {
background: #ddd;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
margin-left: 8px;
}
.boards-left-menu .menu-item.active a,
.boards-left-menu .menu-item a:hover {
background: #f0f0f0;
}
.boards-left-menu .menu-item.active .menu-count {
background: #bbb;
}
/* Drag-over state for menu items (for dropping boards on Remaining) */
.boards-left-menu .menu-item a.drag-over {
background: #d0e8ff;
border: 2px dashed #2196F3;
}
.workspaces-header {
display: flex;
align-items: center;
justify-content: space-between;
font-weight: bold;
margin-top: 12px;
}
.workspaces-header .js-add-space {
text-decoration: none;
font-weight: bold;
border: 1px solid #ccc;
padding: 2px 8px;
border-radius: 4px;
}
.workspace-tree {
list-style: none;
padding-left: 10px;
}
.workspace-node {
margin: 2px 0;
position: relative;
}
.workspace-node-content {
display: flex;
align-items: center;
gap: 4px;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s;
}
.workspace-node.dragging > .workspace-node-content {
opacity: 0.5;
background: #e0e0e0;
}
.workspace-node.drag-over > .workspace-node-content {
background: #d0e8ff;
border: 2px dashed #2196F3;
}
.workspace-drag-handle {
cursor: grab;
color: #999;
font-size: 14px;
padding: 0 4px;
user-select: none;
}
.workspace-drag-handle:active {
cursor: grabbing;
}
.workspace-node .js-select-space {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
flex: 1;
text-decoration: none;
}
.workspace-node .workspace-icon {
font-size: 16px;
line-height: 1;
}
.workspace-node .workspace-name {
flex: 1;
}
.workspace-node .workspace-count {
background: #ddd;
padding: 2px 6px;
border-radius: 10px;
font-size: 11px;
font-weight: bold;
min-width: 20px;
text-align: center;
}
.workspace-node .js-edit-space,
.workspace-node .js-add-subspace {
padding: 2px 6px;
border-radius: 3px;
cursor: pointer;
text-decoration: none;
font-size: 14px;
opacity: 0.6;
transition: opacity 0.2s;
}
.workspace-node .js-edit-space:hover,
.workspace-node .js-add-subspace:hover {
opacity: 1;
background: #e0e0e0;
}
.workspace-node.active > .workspace-node-content .js-select-space,
.workspace-node > .workspace-node-content:hover .js-select-space {
background: #f0f0f0;
}
.workspace-node.active .workspace-count {
background: #bbb;
}
.boards-right-grid {
min-height: 200px;
}
.boards-path-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 12px 16px;
margin-bottom: 16px;
background: #f5f5f5;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
}
.boards-path-header .path-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.boards-path-header .multiselection-hint {
background: #FFF3CD;
color: #856404;
padding: 4px 12px;
border-radius: 4px;
font-size: 13px;
font-weight: normal;
border: 1px solid #FFE69C;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.boards-path-header .path-right {
display: flex;
align-items: center;
gap: 8px;
}
.boards-path-header .path-icon {
font-size: 18px;
}
.boards-path-header .path-text {
color: #333;
}
.boards-path-header .board-header-btn {
padding: 6px 12px;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
transition: all 0.2s;
}
.boards-path-header .board-header-btn:hover {
background: #f0f0f0;
border-color: #bbb;
}
.boards-path-header .board-header-btn.emphasis {
background: #2196F3;
color: #fff;
border-color: #2196F3;
font-weight: bold;
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.5);
transform: scale(1.05);
}
.boards-path-header .board-header-btn.emphasis:hover {
background: #1976D2;
box-shadow: 0 3px 12px rgba(33, 150, 243, 0.7);
}
.boards-path-header .board-header-btn-close {
padding: 4px 10px;
background: #f44336;
color: #000;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-left: 10px; /* Extra space between MultiSelection toggle and Remove Filter */
}
.boards-path-header .board-header-btn-close:hover {
background: #d32f2f;
}
.zoom-controls { .zoom-controls {
display: flex; display: flex;
align-items: center; align-items: center;
@ -373,35 +106,23 @@
.board-list li.starred .is-star-active, .board-list li.starred .is-star-active,
.board-list li.starred .is-not-star-active { .board-list li.starred .is-not-star-active {
opacity: 1; opacity: 1;
color: #ffd700;
}
/* Show star icon on hover even for non-starred boards */
.board-list li:hover .is-star-active,
.board-list li:hover .is-not-star-active {
opacity: 1;
} }
.board-list .board-list-item { .board-list .board-list-item {
overflow: hidden; overflow: hidden;
background-color: inherit; /* Inherit board color from parent li.js-board */ background-color: #999;
color: #f6f6f6; color: #f6f6f6;
min-height: 100px; min-height: 100px;
font-size: 16px; font-size: 16px;
line-height: 22px; line-height: 22px;
border-radius: 0; /* No border-radius - parent .js-board has it */ border-radius: 3px;
display: block; display: block;
font-weight: 700; font-weight: 700;
padding: 36px 8px 32px 8px; /* Top padding for drag handle, bottom for checkbox */ padding: 8px;
margin: 0; /* No margin - moved to parent .js-board */ margin: 8px;
position: relative; position: relative;
text-decoration: none; text-decoration: none;
word-wrap: break-word; word-wrap: break-word;
} }
.board-list .board-list-item > .js-open-board {
text-decoration: none;
color: inherit;
display: block;
}
.board-list .board-list-item.template-container { .board-list .board-list-item.template-container {
border: 4px solid #fff; border: 4px solid #fff;
} }
@ -429,20 +150,13 @@
.board-list .js-add-board .label { .board-list .js-add-board .label {
font-weight: normal; font-weight: normal;
line-height: 56px; line-height: 56px;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
background-color: #999; /* Darker background for better text contrast */
border-radius: 3px;
padding: 36px 8px 32px 8px;
} }
.board-list .js-add-board .label:hover { .board-list .js-add-board :hover {
background-color: #808080; /* Even darker on hover */ background-color: #939393;
} }
.board-list .is-star-active, .board-list .is-star-active,
.board-list .is-not-star-active { .board-list .is-not-star-active {
top: 0; bottom: 0;
font-size: 14px; font-size: 14px;
height: 18px; height: 18px;
line-height: 18px; line-height: 18px;
@ -450,6 +164,7 @@
padding: 9px 9px; padding: 9px 9px;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0;
transition-duration: 0.15s; transition-duration: 0.15s;
transition-property: color, font-size, background; transition-property: color, font-size, background;
} }
@ -523,107 +238,6 @@
.board-list li:hover a .is-not-star-active { .board-list li:hover a .is-not-star-active {
opacity: 1; opacity: 1;
} }
/* Board drag handle - always visible and positioned at top */
.board-list .board-handle {
position: absolute;
padding: 4px 6px;
top: 4px;
left: 50%;
transform: translateX(-50%);
font-size: 14px;
color: #fff;
background: rgba(0,0,0,0.4);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
transition: background-color 0.2s ease;
cursor: grab;
opacity: 1;
user-select: none;
}
.board-list .board-handle:active {
cursor: grabbing;
}
.board-list .board-handle:hover {
background: rgba(255, 255, 0, 0.8) !important;
color: #000;
}
/* Multiselection checkbox on board items */
.board-list .board-list-item .multi-selection-checkbox {
position: absolute !important;
bottom: 4px !important;
left: 4px !important;
top: auto !important;
width: 24px;
height: 24px;
border: 3px solid #fff;
background: rgba(0,0,0,0.5);
border-radius: 4px;
cursor: pointer;
z-index: 11;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
transform: none !important;
margin: 0 !important;
}
.board-list .board-list-item .multi-selection-checkbox:hover {
background: rgba(0,0,0,0.7);
transform: scale(1.15) !important;
box-shadow: 0 3px 6px rgba(0,0,0,0.5);
}
.board-list .board-list-item .multi-selection-checkbox.is-checked {
background: #3cb500;
border-color: #3cb500;
box-shadow: 0 2px 8px rgba(60, 181, 0, 0.6);
width: 24px !important;
height: 24px !important;
top: auto !important;
left: 4px !important;
transform: none !important;
border-radius: 4px !important;
}
.board-list .board-list-item .multi-selection-checkbox.is-checked::after {
content: '✓';
color: #fff;
font-size: 16px;
font-weight: bold;
}
/* 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-offset: -4px;
box-shadow: 0 4px 12px rgba(60, 181, 0, 0.4);
}
/* Visual hint when multiselection is active */
.board-list.is-multiselection-active .board-list-item {
border: 2px dashed rgba(33, 150, 243, 0.3);
}
.board-backgrounds-list .board-background-select { .board-backgrounds-list .board-background-select {
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
@ -657,19 +271,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 { .board-backgrounds-list .board-background-select .background-box i.fa-check {
font-size: 25px; 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.) */ /* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
.board-list.mobile-view { .board-list.mobile-view {
height: calc(100vh - 120px); height: calc(100vh - 120px);
@ -1136,62 +739,9 @@ body.grey-icons-enabled .checkmark-no-grey {
#resetBtn { #resetBtn {
display: inline; display: inline;
} }
#resetBtn.filter-reset-btn {
background: #f44336;
color: #000;
border: none;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 6px;
transition: background 0.2s;
}
#resetBtn.filter-reset-btn:hover {
background: #d32f2f;
}
#resetBtn.filter-reset-btn .reset-icon {
font-size: 14px;
}
.js-board { .js-board {
display: block; display: block;
background-color: #999; /* Default gray background if no color class is applied */
border-radius: 3px; /* Rounded corners for board items */
overflow: hidden; /* Ensure children respect rounded corners */
margin: 8px; /* Space between board items */
} }
/* Reset background for add-board button */
.js-add-board {
background-color: transparent !important;
margin: 8px !important; /* Keep margin for add-board */
}
/* Apply board colors to li.js-board parent instead of just the link */
.board-list .board-color-nephritis { background-color: #27ae60; }
.board-list .board-color-pomegranate { background-color: #c0392b; }
.board-list .board-color-belize { background-color: #2980b9; }
.board-list .board-color-wisteria { background-color: #8e44ad; }
.board-list .board-color-midnight { background-color: #2c3e50; }
.board-list .board-color-pumpkin { background-color: #e67e22; }
.board-list .board-color-moderatepink { background-color: #cd5a91; }
.board-list .board-color-strongcyan { background-color: #00aecc; }
.board-list .board-color-limegreen { background-color: #4bbf6b; }
.board-list .board-color-dark { background-color: #2c3e51; }
.board-list .board-color-relax { background-color: #27ae61; }
.board-list .board-color-corteza { background-color: #568ba2; }
.board-list .board-color-clearblue { background-color: #3498db; }
.board-list .board-color-natural { background-color: #596557; }
.board-list .board-color-modern { background-color: #2a80b8; }
.board-list .board-color-moderndark { background-color: #2a2a2a; }
.board-list .board-color-exodark { background-color: #222; }
.minicard-members { .minicard-members {
padding: 6px 0 6px 8px; padding: 6px 0 6px 8px;
width: 100%; width: 100%;

View file

@ -2,180 +2,151 @@ template(name="boardList")
.wrapper .wrapper
.board-list-header .board-list-header
.boards-layout ul.AllBoardTeamsOrgs
// Left menu li.AllBoardTeams
.boards-left-menu if userHasTeams
ul.menu select.js-AllBoardTeams#jsAllBoardTeams("multiple")
li(class="menu-item {{#if isSelectedMenu 'starred'}}active{{/if}}") option(value="-1") {{_ 'teams'}} :
a.js-select-menu(data-type="starred") each teamsDatas
span.menu-label option(value="{{teamId}}") {{_ teamDisplayName}}
span.emoji-icon ⭐
| {{_ '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-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-count {{menuItemCount 'remaining'}}
.workspaces-header
span
span.emoji-icon 🗂️
| {{_ 'allboards.workspaces'}}
a.js-add-workspace(title="{{_ 'allboards.add-workspace'}}") +
// Workspaces tree
+workspaceTree(nodes=workspacesTree selectedWorkspaceId=selectedWorkspaceId)
// Existing filter by orgs/teams (kept) li.AllBoardOrgs
ul.AllBoardTeamsOrgs if userHasOrgs
li.AllBoardTeams select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
if userHasTeams option(value="-1") {{_ 'organizations'}} :
select.js-AllBoardTeams#jsAllBoardTeams("multiple") each orgsDatas
option(value="-1") {{_ 'teams'}} : option(value="{{orgId}}") {{orgDisplayName}}
each teamsDatas
option(value="{{teamId}}") {{_ teamDisplayName}}
li.AllBoardOrgs //li.AllBoardTemplates
if userHasOrgs // if userHasTemplates
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple") // select.js-AllBoardTemplates#jsAllBoardTemplates("multiple")
option(value="-1") {{_ 'organizations'}} : // option(value="-1") {{_ 'templates'}} :
each orgsDatas // each templatesDatas
option(value="{{orgId}}") {{orgDisplayName}} // option(value="{{templateId}}") {{_ templateDisplayName}}
li.AllBoardBtns li.AllBoardBtns
div.AllBoardButtonsContainer div.AllBoardButtonsContainer
if userHasOrgsOrTeams if userHasOrgsOrTeams
span.emoji-icon 🔍 i.fa.fa-filter
input#filterBtn(type="button" value="{{_ 'filter'}}") input#filterBtn(type="button" value="{{_ 'filter'}}")
button#resetBtn.filter-reset-btn input#resetBtn(type="button" value="{{_ 'filter-clear'}}")
span.reset-icon
span.emoji-icon ❌
span {{_ 'filter-clear'}}
// Right boards grid ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}}")
.boards-right-grid li.js-add-board
.boards-path-header a.board-list-item.label(title="{{_ 'add-board'}}")
.path-left | {{_ 'add-board'}}
span.path-icon.emoji-icon {{currentMenuPath.icon}} each boards
span.path-text {{currentMenuPath.text}} li(class="{{_id}}" class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
if BoardMultiSelection.isActive if isInvited
span.multiselection-hint .board-list-item
span.emoji-icon 📌 span.details
| {{_ 'multi-selection-active'}} span.board-list-item-name= title
.path-right i.fa.js-star-board(
if canModifyBoards class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
if hasBoardsSelected title="{{_ 'star-board-title'}}")
button.js-archive-selected-boards.board-header-btn p.board-list-item-desc {{_ 'just-invited'}}
span.emoji-icon 📦 button.js-accept-invite.primary {{_ 'accept'}}
span {{_ 'archive-board'}} button.js-decline-invite {{_ 'decline'}}
button.js-duplicate-selected-boards.board-header-btn else
span.emoji-icon 📋 if $eq type "template-container"
span {{_ 'duplicate-board'}} a.js-open-board.template-container.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
a.board-header-btn.js-multiselection-activate( span.details
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}" span.board-list-item-name(title="{{_ 'template-container'}}")
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}") +viewer
span.emoji-icon ☑️ = title
if BoardMultiSelection.isActive i.fa.js-star-board(
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}") class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
span.emoji-icon ✖ title="{{_ 'star-board-title'}}")
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}") p.board-list-item-desc
li.js-add-board +viewer
if isSelectedMenu 'templates' = description
a.board-list-item.label(title="{{_ 'add-template-container'}}") if hasSpentTimeCards
span.emoji-icon i.fa.js-has-spenttime-cards(
| {{_ 'add-template-container'}} class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
i.fa.board-handle(
class="fa-arrows"
title="{{_ 'drag-board'}}")
if isSandstorm
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
else if isAdministrable
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
else if currentUser.isAdmin
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
else else
a.board-list-item.label(title="{{_ 'add-board'}}") a.js-open-board.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
span.emoji-icon span.details
| {{_ 'add-board'}} span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
each boards +viewer
li.js-board(class="{{_id}} {{#if isStarred}}starred{{/if}} {{colorClass}} {{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}", draggable="true") = title
if isInvited unless currentSetting.hideBoardMemberList
.board-list-item if allowsBoardMemberList
if BoardMultiSelection.isActive .minicard-members
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection( each member in boardMembers _id
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}") a.name
span.details +userAvatar(userId=member noRemove=true)
span.board-list-item-name= title unless currentSetting.hideCardCounterList
span.js-star-board( if allowsCardCounterList
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}" .minicard-lists.flex.flex-wrap
title="{{_ 'star-board-title'}}") each list in boardLists _id
span.emoji-icon .item
| {{#if isStarred}}⭐{{else}}☆{{/if}} | {{ list }}
p.board-list-item-desc {{_ 'just-invited'}} a.js-star-board(
button.js-accept-invite.primary {{_ 'accept'}} class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
button.js-decline-invite {{_ 'decline'}} title="{{_ 'star-board-title'}}")
else | {{#if isStarred}}⭐{{else}}☆{{/if}}
if $eq type "template-container" p.board-list-item-desc
.template-container.board-list-item +viewer
if BoardMultiSelection.isActive = description
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection( if hasSpentTimeCards
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}") i.fa.js-has-spenttime-cards(
span.board-handle(title="{{_ 'drag-board'}}") class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
span.emoji-icon ↕️ title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
i.fa.board-handle(
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}") class="fa-arrows"
span.details title="{{_ 'drag-board'}}")
span.board-list-item-name(title="{{_ 'template-container'}}") if isSandstorm
+viewer a.js-clone-board(
= title class="fa-clone"
p.board-list-item-desc title="{{_ 'duplicate-board'}}")
+viewer | 📋
= description a.js-archive-board(
if hasSpentTimeCards class="fa-archive"
span.js-has-spenttime-cards( title="{{_ 'archive-board'}}")
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}" | 📦
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}") else if isAdministrable
span.emoji-icon ⏱️ a.js-clone-board(
span.js-star-board( class="fa-clone"
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}" title="{{_ 'duplicate-board'}}")
title="{{_ 'star-board-title'}}") | 📋
span.emoji-icon a.js-archive-board(
| {{#if isStarred}}⭐{{else}}☆{{/if}} class="fa-archive"
else title="{{_ 'archive-board'}}")
.board-list-item | 📦
if BoardMultiSelection.isActive else if currentUser.isAdmin
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection( a.js-clone-board(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}") class="fa-clone"
span.board-handle(title="{{_ 'drag-board'}}") title="{{_ 'duplicate-board'}}")
span.emoji-icon ↕️ | 📋
a.js-archive-board(
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}") class="fa-archive"
span.details title="{{_ 'archive-board'}}")
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}") | 📦
+viewer
= title
unless currentSetting.hideBoardMemberList
if allowsBoardMemberList
.minicard-members
each member in boardMembers _id
a.name
+userAvatar(userId=member noRemove=true)
unless currentSetting.hideCardCounterList
if allowsCardCounterList
.minicard-lists.flex.flex-wrap
each list in boardLists _id
.item
| {{ list }}
p.board-list-item-desc
+viewer
= description
if hasSpentTimeCards
span.js-has-spenttime-cards(
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
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}}
template(name="boardListHeaderBar") template(name="boardListHeaderBar")
h1 {{_ title }} h1 {{_ title }}
@ -186,28 +157,3 @@ template(name="boardListHeaderBar")
// a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}") // a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
// i.fa.fa-clone // i.fa.fa-clone
// span {{_ 'templates'}} // span {{_ 'templates'}}
// Recursive template for workspaces tree
template(name="workspaceTree")
if nodes
ul.workspace-tree.js-workspace-tree
each nodes
li.workspace-node(class="{{#if $eq id selectedWorkspaceId}}active{{/if}}" data-workspace-id="{{id}}" draggable="true")
.workspace-node-content
span.workspace-drag-handle
span.emoji-icon ↕️
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 ✏️
span.workspace-count {{workspaceCount id}}
a.js-add-subworkspace(data-id="{{id}}" title="{{_ 'allboards.add-subworkspace'}}") +
if children
+workspaceTree(nodes=children selectedWorkspaceId=selectedWorkspaceId)

View file

@ -14,9 +14,6 @@ Template.boardList.helpers({
return Utils.isMiniScreen() && Session.get('currentBoard'); */ return Utils.isMiniScreen() && Session.get('currentBoard'); */
return true; return true;
}, },
BoardMultiSelection() {
return BoardMultiSelection;
},
}) })
Template.boardListHeaderBar.events({ Template.boardListHeaderBar.events({
@ -48,9 +45,6 @@ BlazeComponent.extendComponent({
onCreated() { onCreated() {
Meteor.subscribe('setting'); Meteor.subscribe('setting');
Meteor.subscribe('tableVisibilityModeSettings'); Meteor.subscribe('tableVisibilityModeSettings');
this.selectedMenu = new ReactiveVar('starred');
this.selectedWorkspaceIdVar = new ReactiveVar(null);
this.workspacesTreeVar = new ReactiveVar([]);
let currUser = ReactiveCache.getCurrentUser(); let currUser = ReactiveCache.getCurrentUser();
let userLanguage; let userLanguage;
if (currUser && currUser.profile) { if (currUser && currUser.profile) {
@ -59,72 +53,9 @@ BlazeComponent.extendComponent({
if (userLanguage) { if (userLanguage) {
TAPi18n.setLanguage(userLanguage); TAPi18n.setLanguage(userLanguage);
} }
// Load workspaces tree reactively
this.autorun(() => {
const u = ReactiveCache.getCurrentUser();
const tree = (u && u.profile && u.profile.boardWorkspacesTree) || [];
this.workspacesTreeVar.set(tree);
});
},
reorderWorkspaces(draggedSpaceId, targetSpaceId) {
const tree = this.workspacesTreeVar.get();
// Helper to remove a space from tree
const removeSpace = (nodes, id) => {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].id === id) {
const removed = nodes.splice(i, 1)[0];
return { tree: nodes, removed };
}
if (nodes[i].children) {
const result = removeSpace(nodes[i].children, id);
if (result.removed) {
return { tree: nodes, removed: result.removed };
}
}
}
return { tree: nodes, removed: null };
};
// Helper to insert a space after target
const insertAfter = (nodes, targetId, spaceToInsert) => {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].id === targetId) {
nodes.splice(i + 1, 0, spaceToInsert);
return true;
}
if (nodes[i].children) {
if (insertAfter(nodes[i].children, targetId, spaceToInsert)) {
return true;
}
}
}
return false;
};
// Clone the tree
const newTree = EJSON.clone(tree);
// Remove the dragged space
const { tree: treeAfterRemoval, removed } = removeSpace(newTree, draggedSpaceId);
if (removed) {
// Insert after target
insertAfter(treeAfterRemoval, targetSpaceId, removed);
// Save the new tree
Meteor.call('setWorkspacesTree', treeAfterRemoval, (err) => {
if (err) console.error(err);
});
}
}, },
onRendered() { onRendered() {
// jQuery sortable is disabled in favor of HTML5 drag-and-drop for space management
// The old sortable code has been removed to prevent conflicts
/* OLD SORTABLE CODE - DISABLED
const itemsSelector = '.js-board:not(.placeholder)'; const itemsSelector = '.js-board:not(.placeholder)';
const $boards = this.$('.js-boards'); const $boards = this.$('.js-boards');
@ -142,20 +73,27 @@ BlazeComponent.extendComponent({
EscapeActions.executeUpTo('popup-close'); EscapeActions.executeUpTo('popup-close');
}, },
stop(evt, ui) { stop(evt, ui) {
// To attribute the new index number, we need to get the DOM element
// of the previous and the following card -- if any.
const prevBoardDom = ui.item.prev('.js-board').get(0); const prevBoardDom = ui.item.prev('.js-board').get(0);
const nextBoardDom = ui.item.next('.js-board').get(0); const nextBoardBom = ui.item.next('.js-board').get(0);
const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardDom, 1); const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardBom, 1);
const boardDomElement = ui.item.get(0); const boardDomElement = ui.item.get(0);
const board = Blaze.getData(boardDomElement); const board = Blaze.getData(boardDomElement);
// Normally the jquery-ui sortable library moves the dragged DOM element
// to its new position, which disrupts Blaze reactive updates mechanism
// (especially when we move the last card of a list, or when multiple
// users move some cards at the same time). To prevent these UX glitches
// we ask sortable to gracefully cancel the move, and to put back the
// DOM in its initial state. The card move is then handled reactively by
// Blaze with the below query.
$boards.sortable('cancel'); $boards.sortable('cancel');
const currentUser = ReactiveCache.getCurrentUser(); board.move(sortIndex.base);
if (currentUser && typeof currentUser.setBoardSortIndex === 'function') {
currentUser.setBoardSortIndex(board._id, sortIndex.base);
}
}, },
}); });
// Disable drag-dropping if the current user is not a board member or is comment only
this.autorun(() => { this.autorun(() => {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) { if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$boards.sortable({ $boards.sortable({
@ -163,7 +101,6 @@ BlazeComponent.extendComponent({
}); });
} }
}); });
*/
}, },
userHasTeams() { userHasTeams() {
if (ReactiveCache.getCurrentUser()?.teams?.length > 0) if (ReactiveCache.getCurrentUser()?.teams?.length > 0)
@ -195,55 +132,22 @@ BlazeComponent.extendComponent({
const ret = this.userHasOrgs() || this.userHasTeams(); const ret = this.userHasOrgs() || this.userHasTeams();
return ret; return ret;
}, },
currentMenuPath() {
const sel = this.selectedMenu.get();
const currentUser = ReactiveCache.getCurrentUser();
// Helper to find space by id in tree
const findSpaceById = (nodes, targetId, path = []) => {
for (const node of nodes) {
if (node.id === targetId) {
return [...path, node];
}
if (node.children && node.children.length > 0) {
const result = findSpaceById(node.children, targetId, [...path, node]);
if (result) return result;
}
}
return null;
};
if (sel === 'starred') {
return { icon: '⭐', text: TAPi18n.__('allboards.starred') };
} else if (sel === 'templates') {
return { icon: '📋', text: TAPi18n.__('allboards.templates') };
} else if (sel === 'remaining') {
return { icon: '📂', text: TAPi18n.__('allboards.remaining') };
} else {
// sel is a workspaceId, build path
const tree = this.workspacesTreeVar.get();
const spacePath = findSpaceById(tree, sel);
if (spacePath && spacePath.length > 0) {
const pathText = spacePath.map(s => s.name).join(' / ');
return { icon: '🗂️', text: `${TAPi18n.__('allboards.workspaces')} / ${pathText}` };
}
return { icon: '🗂️', text: TAPi18n.__('allboards.workspaces') };
}
},
boards() { boards() {
let query = { let query = {
// { type: 'board' },
// { type: { $in: ['board','template-container'] } },
$and: [ $and: [
{ archived: false }, { archived: false },
{ type: { $in: ['board', 'template-container'] } }, { type: { $in: ['board', 'template-container'] } },
{ $or: [] },
{ title: { $not: { $regex: /^\^.*\^$/ } } } { title: { $not: { $regex: /^\^.*\^$/ } } }
] ]
}; };
const membershipOrs = [];
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly'); let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
if (FlowRouter.getRouteName() === 'home') { if (FlowRouter.getRouteName() === 'home') {
membershipOrs.push({ 'members.userId': Meteor.userId() }); query.$and[2].$or.push({ 'members.userId': Meteor.userId() });
if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue) { if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue) {
query.$and.push({ 'permission': 'private' }); query.$and.push({ 'permission': 'private' });
@ -258,7 +162,7 @@ BlazeComponent.extendComponent({
// } // }
//query.$and[2].$or.push({'orgs': {$elemMatch : {orgId: orgsIds[0]}}}); //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() || ''; let teamIdsUserBelongs = currUser?.teamIdsUserBelongs() || '';
@ -268,11 +172,8 @@ BlazeComponent.extendComponent({
// query.$or[2].$or.push({'teams.teamId': teamsIds[i]}); // query.$or[2].$or.push({'teams.teamId': teamsIds[i]});
// } // }
//query.$and[2].$or.push({'teams': { $elemMatch : {teamId: teamsIds[0]}}}); //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) { else if (allowPrivateVisibilityOnly !== undefined && !allowPrivateVisibilityOnly.booleanValue) {
query = { query = {
@ -283,33 +184,10 @@ BlazeComponent.extendComponent({
}; };
} }
const boards = ReactiveCache.getBoards(query, {}); const ret = ReactiveCache.getBoards(query, {
const currentUser = ReactiveCache.getCurrentUser(); sort: { sort: 1 /* boards default sorting */ },
let list = boards; });
// Apply left menu filtering return ret;
const sel = this.selectedMenu.get();
const assignments = (currentUser && currentUser.profile && currentUser.profile.boardWorkspaceAssignments) || {};
if (sel === 'starred') {
list = list.filter(b => currentUser && currentUser.hasStarred(b._id));
} else if (sel === 'templates') {
list = list.filter(b => b.type === 'template-container');
} else if (sel === 'remaining') {
// Show boards not in any workspace AND not templates
// Keep starred boards visible in Remaining too
list = list.filter(b =>
!assignments[b._id] &&
b.type !== 'template-container'
);
} else {
// assume sel is a workspaceId
// Keep starred boards visible in their workspace too
list = list.filter(b => assignments[b._id] === sel);
}
if (currentUser && typeof currentUser.sortBoardsForUser === 'function') {
return currentUser.sortBoardsForUser(list);
}
return list.slice().sort((a, b) => (a.title || '').localeCompare(b.title || ''));
}, },
boardLists(boardId) { boardLists(boardId) {
/* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214 /* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
@ -357,65 +235,11 @@ BlazeComponent.extendComponent({
events() { events() {
return [ return [
{ {
'click .js-select-menu'(evt) { 'click .js-add-board': Popup.open('createBoard'),
const type = evt.currentTarget.getAttribute('data-type');
this.selectedWorkspaceIdVar.set(null);
this.selectedMenu.set(type);
},
'click .js-select-workspace'(evt) {
const id = evt.currentTarget.getAttribute('data-id');
this.selectedWorkspaceIdVar.set(id);
this.selectedMenu.set(id);
},
'click .js-add-workspace'(evt) {
evt.preventDefault();
const name = prompt(TAPi18n.__('allboards.add-workspace-prompt') || 'New Space name');
if (name && name.trim()) {
Meteor.call('createWorkspace', { parentId: null, name: name.trim() }, (err, res) => {
if (err) console.error(err);
});
}
},
'click .js-add-board'(evt) {
// Store the currently selected workspace/menu for board creation
const selectedWorkspaceId = this.selectedWorkspaceIdVar.get();
const selectedMenu = this.selectedMenu.get();
if (selectedWorkspaceId) {
Session.set('createBoardInWorkspace', selectedWorkspaceId);
} else {
Session.set('createBoardInWorkspace', null);
}
// Open different popup based on context
if (selectedMenu === 'templates') {
Popup.open('createTemplateContainer')(evt);
} else {
Popup.open('createBoard')(evt);
}
},
'click .js-star-board'(evt) { 'click .js-star-board'(evt) {
const boardId = this.currentData()._id;
ReactiveCache.getCurrentUser().toggleBoardStar(boardId);
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation();
const boardId = this.currentData()._id;
if (boardId) {
Meteor.call('toggleBoardStar', boardId);
}
},
// HTML5 DnD from boards to spaces
'dragstart .js-board'(evt) {
const boardId = this.currentData()._id;
// Support multi-drag
if (BoardMultiSelection.isActive() && BoardMultiSelection.isSelected(boardId)) {
const selectedIds = BoardMultiSelection.getSelectedBoardIds();
try {
evt.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(selectedIds));
evt.originalEvent.dataTransfer.setData('application/x-board-multi', 'true');
} catch (e) {}
} else {
try { evt.originalEvent.dataTransfer.setData('text/plain', boardId); } catch (e) {}
}
}, },
'click .js-clone-board'(evt) { 'click .js-clone-board'(evt) {
if (confirm(TAPi18n.__('duplicate-board-confirm'))) { if (confirm(TAPi18n.__('duplicate-board-confirm'))) {
@ -466,58 +290,6 @@ BlazeComponent.extendComponent({
} }
}); });
}, },
'click .js-multiselection-activate'(evt) {
evt.preventDefault();
if (BoardMultiSelection.isActive()) {
BoardMultiSelection.disable();
} else {
BoardMultiSelection.activate();
}
},
'click .js-multiselection-reset'(evt) {
evt.preventDefault();
BoardMultiSelection.disable();
},
'click .js-toggle-board-multi-selection'(evt) {
evt.preventDefault();
evt.stopPropagation();
const boardId = this.currentData()._id;
BoardMultiSelection.toogle(boardId);
},
'click .js-archive-selected-boards'(evt) {
evt.preventDefault();
const selectedBoards = BoardMultiSelection.getSelectedBoardIds();
if (selectedBoards.length > 0 && confirm(TAPi18n.__('archive-board-confirm'))) {
selectedBoards.forEach(boardId => {
Meteor.call('archiveBoard', boardId);
});
BoardMultiSelection.reset();
}
},
'click .js-duplicate-selected-boards'(evt) {
evt.preventDefault();
const selectedBoards = BoardMultiSelection.getSelectedBoardIds();
if (selectedBoards.length > 0 && confirm(TAPi18n.__('duplicate-board-confirm'))) {
selectedBoards.forEach(boardId => {
const board = ReactiveCache.getBoard(boardId);
if (board) {
Meteor.call(
'copyBoard',
boardId,
{
sort: ReactiveCache.getBoards({ archived: false }).length,
type: 'board',
title: board.title,
},
(err, res) => {
if (err) console.error(err);
}
);
}
});
BoardMultiSelection.reset();
}
},
'click #resetBtn'(event) { 'click #resetBtn'(event) {
let allBoards = document.getElementsByClassName("js-board"); let allBoards = document.getElementsByClassName("js-board");
let currBoard; let currBoard;
@ -546,18 +318,15 @@ BlazeComponent.extendComponent({
const query = { const query = {
$and: [ $and: [
{ archived: false }, { archived: false },
{ type: 'board' } { type: 'board' },
{ $or: [] }
] ]
}; };
const ors = [];
if (selectedTeamsValues.length > 0) { if (selectedTeamsValues.length > 0) {
ors.push({ 'teams.teamId': { $in: selectedTeamsValues } }); query.$and[2].$or.push({ 'teams.teamId': { $in: selectedTeamsValues } });
} }
if (selectedOrgsValues.length > 0) { if (selectedOrgsValues.length > 0) {
ors.push({ 'orgs.orgId': { $in: selectedOrgsValues } }); query.$and[2].$or.push({ 'orgs.orgId': { $in: selectedOrgsValues } });
}
if (ors.length) {
query.$and.push({ $or: ors });
} }
let filteredBoards = ReactiveCache.getBoards(query, {}); let filteredBoards = ReactiveCache.getBoards(query, {});
@ -587,260 +356,7 @@ BlazeComponent.extendComponent({
} }
} }
}, },
'click .js-edit-workspace'(evt) {
evt.preventDefault();
evt.stopPropagation();
const workspaceId = evt.currentTarget.getAttribute('data-id');
// Find the space in the tree
const findSpace = (nodes, id) => {
for (const node of nodes) {
if (node.id === id) return node;
if (node.children) {
const found = findSpace(node.children, id);
if (found) return found;
}
}
return null;
};
const tree = this.workspacesTreeVar.get();
const space = findSpace(tree, workspaceId);
if (space) {
const newName = prompt(TAPi18n.__('allboards.edit-workspace-name') || 'Space name:', space.name);
const newIcon = prompt(TAPi18n.__('allboards.edit-workspace-icon') || 'Space icon (markdown):', space.icon || '📁');
if (newName !== null && newName.trim()) {
// Update space in tree
const updateSpaceInTree = (nodes, id, updates) => {
return nodes.map(node => {
if (node.id === id) {
return { ...node, ...updates };
}
if (node.children) {
return { ...node, children: updateSpaceInTree(node.children, id, updates) };
}
return node;
});
};
const updatedTree = updateSpaceInTree(tree, workspaceId, {
name: newName.trim(),
icon: newIcon || '📁'
});
Meteor.call('setWorkspacesTree', updatedTree, (err) => {
if (err) console.error(err);
});
}
}
},
'click .js-add-subworkspace'(evt) {
evt.preventDefault();
evt.stopPropagation();
const parentId = evt.currentTarget.getAttribute('data-id');
const name = prompt(TAPi18n.__('allboards.add-subworkspace-prompt') || 'Subspace name:');
if (name && name.trim()) {
Meteor.call('createWorkspace', { parentId, name: name.trim() }, (err) => {
if (err) console.error(err);
});
}
},
'dragstart .workspace-node'(evt) {
const workspaceId = evt.currentTarget.getAttribute('data-workspace-id');
evt.originalEvent.dataTransfer.effectAllowed = 'move';
evt.originalEvent.dataTransfer.setData('application/x-workspace-id', workspaceId);
// Create a better drag image
const dragImage = evt.currentTarget.cloneNode(true);
dragImage.style.position = 'absolute';
dragImage.style.top = '-9999px';
dragImage.style.opacity = '0.8';
document.body.appendChild(dragImage);
evt.originalEvent.dataTransfer.setDragImage(dragImage, 0, 0);
setTimeout(() => document.body.removeChild(dragImage), 0);
evt.currentTarget.classList.add('dragging');
},
'dragend .workspace-node'(evt) {
evt.currentTarget.classList.remove('dragging');
document.querySelectorAll('.workspace-node').forEach(el => {
el.classList.remove('drag-over');
});
},
'dragover .workspace-node'(evt) {
evt.preventDefault();
evt.stopPropagation();
const draggingEl = document.querySelector('.workspace-node.dragging');
const targetEl = evt.currentTarget;
// Allow dropping boards on any space
// Or allow dropping spaces on other spaces (but not on itself or descendants)
if (!draggingEl || (targetEl !== draggingEl && !draggingEl.contains(targetEl))) {
evt.originalEvent.dataTransfer.dropEffect = 'move';
targetEl.classList.add('drag-over');
}
},
'dragleave .workspace-node'(evt) {
evt.currentTarget.classList.remove('drag-over');
},
'drop .workspace-node'(evt) {
evt.preventDefault();
evt.stopPropagation();
const targetEl = evt.currentTarget;
targetEl.classList.remove('drag-over');
// Check what's being dropped - board or workspace
const draggedWorkspaceId = evt.originalEvent.dataTransfer.getData('application/x-workspace-id');
const isMultiBoard = evt.originalEvent.dataTransfer.getData('application/x-board-multi');
const boardData = evt.originalEvent.dataTransfer.getData('text/plain');
if (draggedWorkspaceId && !boardData) {
// This is a workspace reorder operation
const targetWorkspaceId = targetEl.getAttribute('data-workspace-id');
if (draggedWorkspaceId !== targetWorkspaceId) {
this.reorderWorkspaces(draggedWorkspaceId, targetWorkspaceId);
}
} else if (boardData) {
// This is a board assignment operation
// Get the workspace ID directly from the dropped workspace-node's data-workspace-id attribute
const workspaceId = targetEl.getAttribute('data-workspace-id');
if (workspaceId) {
if (isMultiBoard) {
// Multi-board drag
try {
const boardIds = JSON.parse(boardData);
boardIds.forEach(boardId => {
Meteor.call('assignBoardToWorkspace', boardId, workspaceId);
});
} catch (e) {
// Error parsing multi-board data
}
} else {
// Single board drag
Meteor.call('assignBoardToWorkspace', boardData, workspaceId);
}
}
}
},
'dragover .js-select-menu'(evt) {
evt.preventDefault();
evt.stopPropagation();
const menuType = evt.currentTarget.getAttribute('data-type');
// Only allow drop on "remaining" menu to unassign boards from spaces
if (menuType === 'remaining') {
evt.originalEvent.dataTransfer.dropEffect = 'move';
evt.currentTarget.classList.add('drag-over');
}
},
'dragleave .js-select-menu'(evt) {
evt.currentTarget.classList.remove('drag-over');
},
'drop .js-select-menu'(evt) {
evt.preventDefault();
evt.stopPropagation();
const menuType = evt.currentTarget.getAttribute('data-type');
evt.currentTarget.classList.remove('drag-over');
// Only handle drops on "remaining" menu
if (menuType !== 'remaining') return;
const isMultiBoard = evt.originalEvent.dataTransfer.getData('application/x-board-multi');
const boardData = evt.originalEvent.dataTransfer.getData('text/plain');
if (boardData) {
if (isMultiBoard) {
// Multi-board drag - unassign all from workspaces
try {
const boardIds = JSON.parse(boardData);
boardIds.forEach(boardId => {
Meteor.call('unassignBoardFromWorkspace', boardId);
});
} catch (e) {
// Error parsing multi-board data
}
} else {
// Single board drag - unassign from workspace
Meteor.call('unassignBoardFromWorkspace', boardData);
}
}
},
}, },
]; ];
}, },
// Helpers for templates
workspacesTree() {
return this.workspacesTreeVar.get();
},
selectedWorkspaceId() {
return this.selectedWorkspaceIdVar.get();
},
isSelectedMenu(type) {
return this.selectedMenu.get() === type;
},
isSpaceSelected(id) {
return this.selectedWorkspaceIdVar.get() === id;
},
menuItemCount(type) {
const currentUser = ReactiveCache.getCurrentUser();
const assignments = (currentUser && currentUser.profile && currentUser.profile.boardWorkspaceAssignments) || {};
// Get all boards for counting
let query = {
$and: [
{ archived: false },
{ type: { $in: ['board', 'template-container'] } },
{ $or: [{ 'members.userId': Meteor.userId() }] },
{ title: { $not: { $regex: /^\^.*\^$/ } } }
]
};
const allBoards = ReactiveCache.getBoards(query, {});
if (type === 'starred') {
return allBoards.filter(b => currentUser && currentUser.hasStarred(b._id)).length;
} else if (type === 'templates') {
return allBoards.filter(b => b.type === 'template-container').length;
} else if (type === 'remaining') {
// Count boards not in any workspace AND not templates
// Include starred boards (they appear in both Starred and Remaining)
return allBoards.filter(b =>
!assignments[b._id] &&
b.type !== 'template-container'
).length;
}
return 0;
},
workspaceCount(workspaceId) {
const currentUser = ReactiveCache.getCurrentUser();
const assignments = (currentUser && currentUser.profile && currentUser.profile.boardWorkspaceAssignments) || {};
// Get all boards for counting
let query = {
$and: [
{ archived: false },
{ type: { $in: ['board', 'template-container'] } },
{ $or: [{ 'members.userId': Meteor.userId() }] },
{ title: { $not: { $regex: /^\^.*\^$/ } } }
]
};
const allBoards = ReactiveCache.getBoards(query, {});
// Count boards directly assigned to this space (not including children)
return allBoards.filter(b => assignments[b._id] === workspaceId).length;
},
canModifyBoards() {
const currentUser = ReactiveCache.getCurrentUser();
return currentUser && !currentUser.isCommentOnly();
},
hasBoardsSelected() {
return BoardMultiSelection.count() > 0;
},
}).register('boardList'); }).register('boardList');

View file

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

View file

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

View file

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

View file

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

View file

@ -97,12 +97,6 @@ template(name="minicardCustomFieldDate")
template(name="editCardReceivedDatePopup") template(name="editCardReceivedDatePopup")
form.edit-card-received-date form.edit-card-received-date
.datepicker .datepicker
// Date input field (existing)
// Insert calendar selector right after date input
.calendar-selector
label(for="calendar-received") 🗓️
input#calendar-received.js-calendar-date(type="date")
// Time input field (if present)
.clear-date .clear-date
a.js-clear-date {{_ 'clear'}} a.js-clear-date {{_ 'clear'}}
.datepicker-actions .datepicker-actions
@ -112,11 +106,6 @@ template(name="editCardReceivedDatePopup")
template(name="editCardStartDatePopup") template(name="editCardStartDatePopup")
form.edit-card-start-date form.edit-card-start-date
.datepicker .datepicker
// Date input field (existing)
.calendar-selector
label(for="calendar-start") 🗓️
input#calendar-start.js-calendar-date(type="date")
// Time input field (if present)
.clear-date .clear-date
a.js-clear-date {{_ 'clear'}} a.js-clear-date {{_ 'clear'}}
.datepicker-actions .datepicker-actions
@ -126,11 +115,6 @@ template(name="editCardStartDatePopup")
template(name="editCardDueDatePopup") template(name="editCardDueDatePopup")
form.edit-card-due-date form.edit-card-due-date
.datepicker .datepicker
// Date input field (existing)
.calendar-selector
label(for="calendar-due") 🗓️
input#calendar-due.js-calendar-date(type="date")
// Time input field (if present)
.clear-date .clear-date
a.js-clear-date {{_ 'clear'}} a.js-clear-date {{_ 'clear'}}
.datepicker-actions .datepicker-actions
@ -140,11 +124,6 @@ template(name="editCardDueDatePopup")
template(name="editCardEndDatePopup") template(name="editCardEndDatePopup")
form.edit-card-end-date form.edit-card-end-date
.datepicker .datepicker
// Date input field (existing)
.calendar-selector
label(for="calendar-end") 🗓️
input#calendar-end.js-calendar-date(type="date")
// Time input field (if present)
.clear-date .clear-date
a.js-clear-date {{_ 'clear'}} a.js-clear-date {{_ 'clear'}}
.datepicker-actions .datepicker-actions

View file

@ -50,17 +50,6 @@ import {
onRendered() { onRendered() {
super.onRendered(); super.onRendered();
// DatePicker base class handles initialization with native HTML inputs // DatePicker base class handles initialization with native HTML inputs
const self = this;
this.$('.js-calendar-date').on('change', function(evt) {
const currentUser = ReactiveCache.getCurrentUser && ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const value = evt.target.value;
if (value) {
// Format date according to user preference
const formatted = formatDateByUserPreference(new Date(value), dateFormat, true);
self._storeDate(new Date(value));
}
});
} }
_storeDate(date) { _storeDate(date) {

View file

@ -31,8 +31,8 @@
display: block; display: block;
position: relative; position: relative;
float: left; float: left;
height: clamp(24px, 3.5vw, 36px); height: 30px;
width: clamp(24px, 3.5vw, 36px); width: 30px;
margin: .3vh; margin: .3vh;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
@ -118,65 +118,6 @@
transition: flex-basis 0.1s; transition: flex-basis 0.1s;
box-sizing: border-box; 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 { .card-details .mCustomScrollBox {
padding-left: 0; padding-left: 0;
} }
@ -198,30 +139,6 @@ body.desktop-mode .card-details.card-details-collapsed {
display: inline-block; display: inline-block;
margin-right: 5px; 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 .close-card-details,
.card-details .card-details-header .maximize-card-details, .card-details .card-details-header .maximize-card-details,
.card-details .card-details-header .minimize-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; font-size: 24px;
padding: 5px 10px 5px 10px; padding: 5px 10px 5px 10px;
margin-right: -8px; margin-right: -8px;
cursor: pointer;
user-select: none;
} }
.card-details .card-details-header .close-card-details-mobile-web, .card-details .card-details-header .close-card-details-mobile-web {
.card-details .card-details-header .card-mobile-desktop-toggle {
font-size: 24px; font-size: 24px;
padding: 5px; padding: 5px;
margin-right: 5px; margin-right: 40px;
cursor: pointer;
user-select: none;
} }
.card-details .card-details-header .card-copy-button { .card-details .card-details-header .card-copy-button {
font-size: 17px; font-size: 17px;
@ -269,36 +181,6 @@ body.desktop-mode .card-details.card-details-collapsed {
padding: 10px; padding: 10px;
margin-right: 30px; 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 { .card-details .card-details-header .card-details-watch {
font-size: 17px; font-size: 17px;
padding-left: 7px; padding-left: 7px;
@ -402,19 +284,6 @@ body.desktop-mode .card-details.card-details-collapsed {
position: fixed; position: fixed;
resize: both; 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 { .card-details-maximized {
padding: 0; padding: 0;
flex-shrink: 0; flex-shrink: 0;
@ -466,53 +335,19 @@ input[type="submit"].attachment-add-link-submit {
} }
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
.card-details { .card-details {
width: 100% !important; width: calc(100% - 1px);
padding: 0px 0px 0px 0px !important; padding: 0px 20px 0px 20px;
margin: 0px !important; margin: 0px;
transition: none; transition: none;
overflow-y: auto; overflow-y: revert;
overflow-x: hidden; overflow-x: revert;
/* 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;
} }
.card-details .card-details-canvas { .card-details .card-details-canvas {
width: 100%; width: 100%;
padding-left: 0px; padding-left: 0px;
padding: 0 15px;
} }
.card-details .card-details-header .close-card-details { .card-details .card-details-header .close-card-details {
margin-right: 0px; 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 { .card-details .card-details-header .card-details-menu {
margin-right: 40px; 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 { .pop-over > .content-wrapper > .popup-container-depth-0 .card-details-header {
margin: 0; 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 { .card-details-white {
background: #fff !important; background: #fff !important;

View file

@ -5,18 +5,13 @@ template(name="cardDetails")
+attachmentViewer +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}}') .card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
+inlinedForm(classNames="js-card-details-title") +inlinedForm(classNames="js-card-details-title")
+editCardTitleForm +editCardTitleForm
else else
unless isMiniScreen unless isMiniScreen
unless isPopup 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'}}") a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
| ❌ | ❌
if canModifyCard if canModifyCard
@ -31,40 +26,24 @@ template(name="cardDetails")
| ☰ | ☰
a.card-copy-button.js-copy-link( a.card-copy-button.js-copy-link(
id="cardURL_copy" id="cardURL_copy"
class="fa-link"
title="{{_ 'copy-card-link-to-clipboard'}}" title="{{_ 'copy-card-link-to-clipboard'}}"
href="{{ originRelativeUrl }}" href="{{ originRelativeUrl }}"
) )
span.emoji-icon 🔗
span.card-drag-handle.js-card-drag-handle(title="Drag card")
| ↕️
span.copied-tooltip {{_ 'copied'}} span.copied-tooltip {{_ 'copied'}}
else else
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}") unless isPopup
| ❌ 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
| 📱
if canModifyCard 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-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
| ☰ | ☰
a.card-copy-mobile-button.js-copy-link( a.card-copy-mobile-button.js-copy-link(
id="cardURL_copy" id="cardURL_copy"
class="fa-link"
title="{{_ 'copy-card-link-to-clipboard'}}" title="{{_ 'copy-card-link-to-clipboard'}}"
href="{{ originRelativeUrl }}" href="{{ originRelativeUrl }}"
) )
span.emoji-icon 🔗
span.copied-tooltip {{_ 'copied'}} span.copied-tooltip {{_ 'copied'}}
h2.card-details-title.js-card-title( h2.card-details-title.js-card-title(
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}") class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
@ -212,7 +191,7 @@ template(name="cardDetails")
if currentBoard.allowsMembers if currentBoard.allowsMembers
.card-details-item.card-details-item-members .card-details-item.card-details-item-members
h3.card-details-item-title h3.card-details-item-title
| &#x1F465; | 👤s
| {{_ 'members'}} | {{_ 'members'}}
each userId in getMembers each userId in getMembers
+userAvatar(userId=userId cardId=_id) +userAvatar(userId=userId cardId=_id)
@ -263,7 +242,7 @@ template(name="cardDetails")
if currentBoard.allowsAssignedBy if currentBoard.allowsAssignedBy
.card-details-item.card-details-item-name .card-details-item.card-details-item-name
h3.card-details-item-title h3.card-details-item-title
| ✍️ | 👤-plus
| {{_ 'assigned-by'}} | {{_ 'assigned-by'}}
if canModifyCard if canModifyCard
unless currentUser.isWorker unless currentUser.isWorker
@ -325,7 +304,7 @@ template(name="cardDetails")
hr hr
.card-details-item.card-details-item-customfield .card-details-item.card-details-item-customfield
h3.card-details-item-title h3.card-details-item-title
| 📋 | 📋-alt
= definition.name = definition.name
+cardCustomField +cardCustomField
@ -699,7 +678,7 @@ template(name="cardDetailsActionsPopup")
| 👁️ | 👁️
| {{_ 'unwatch'}} | {{_ 'unwatch'}}
else else
| 👁️ | 👁️-slash
| {{_ 'watch'}} | {{_ 'watch'}}
hr hr
if canModifyCard if canModifyCard
@ -719,7 +698,7 @@ template(name="cardDetailsActionsPopup")
if currentUser.isBoardAdmin if currentUser.isBoardAdmin
li li
a.js-custom-fields a.js-custom-fields
| 📋 | 📋-alt
| {{_ 'card-edit-custom-fields'}} | {{_ 'card-edit-custom-fields'}}
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}} //li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}} //li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
@ -739,7 +718,7 @@ template(name="cardDetailsActionsPopup")
| 👁️ | 👁️
| {{_ 'hide-list-on-minicard'}} | {{_ 'hide-list-on-minicard'}}
else else
| 👁️ | 👁️-slash
| {{_ 'show-list-on-minicard'}} | {{_ 'show-list-on-minicard'}}
hr hr
ul.pop-over-list ul.pop-over-list
@ -788,7 +767,7 @@ template(name="cardDetailsActionsPopup")
ul.pop-over-list ul.pop-over-list
li li
a.js-more a.js-more
span.emoji-icon 🔗 | 🔗
| {{_ 'cardMorePopup-title'}} | {{_ 'cardMorePopup-title'}}
template(name="exportCardPopup") template(name="exportCardPopup")
@ -824,29 +803,17 @@ template(name="copyAndMoveCard")
label {{_ 'boards'}}: label {{_ 'boards'}}:
select.js-select-boards(autofocus) select.js-select-boards(autofocus)
each boards 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'}}: label {{_ 'swimlanes'}}:
select.js-select-swimlanes select.js-select-swimlanes
each 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'}}: label {{_ 'lists'}}:
select.js-select-lists select.js-select-lists
each 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 {{_ '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'}}
.edit-controls.clearfix .edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}} button.primary.confirm.js-done {{_ 'done'}}

View file

@ -31,7 +31,6 @@ import CardComments from '/models/cardComments';
import { ALLOWED_COLORS } from '/config/const'; import { ALLOWED_COLORS } from '/config/const';
import { UserAvatar } from '../users/userAvatar'; import { UserAvatar } from '../users/userAvatar';
import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList'; import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList';
import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard';
import { handleFileUpload } from './attachments'; import { handleFileUpload } from './attachments';
import uploadProgressManager from '../../lib/uploadProgressManager'; import uploadProgressManager from '../../lib/uploadProgressManager';
@ -64,11 +63,7 @@ BlazeComponent.extendComponent({
const boardBody = this.parentComponent().parentComponent(); const boardBody = this.parentComponent().parentComponent();
//in Miniview parent is Board, not BoardBody. //in Miniview parent is Board, not BoardBody.
if (boardBody !== null) { if (boardBody !== null) {
// Only show overlay in mobile mode, not in desktop mode boardBody.showOverlay.set(true);
const isMobile = Utils.getMobileMode();
if (isMobile) {
boardBody.showOverlay.set(true);
}
boardBody.mouseHasEnterCardDetails = false; boardBody.mouseHasEnterCardDetails = false;
} }
} }
@ -86,7 +81,6 @@ BlazeComponent.extendComponent({
isWatching() { isWatching() {
const card = this.currentData(); const card = this.currentData();
if (!card || typeof card.findWatcher !== 'function') return false;
return card.findWatcher(Meteor.userId()); return card.findWatcher(Meteor.userId());
}, },
@ -99,18 +93,6 @@ BlazeComponent.extendComponent({
return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized(); 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() { presentParentTask() {
let result = this.currentBoard.presentParentTask; let result = this.currentBoard.presentParentTask;
if (result === null || result === undefined) { if (result === null || result === undefined) {
@ -163,9 +145,8 @@ BlazeComponent.extendComponent({
* @return is the list id the current list id ? * @return is the list id the current list id ?
*/ */
isCurrentListId(listId) { isCurrentListId(listId) {
const data = this.data(); const ret = this.data().listId == listId;
if (!data || typeof data.listId === 'undefined') return false; return ret;
return data.listId == listId;
}, },
onRendered() { onRendered() {
@ -315,74 +296,8 @@ BlazeComponent.extendComponent({
return [ return [
{ {
...events, ...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'() { 'click .js-close-card-details'() {
// Get board ID from either the card data or current board in session Utils.goBoardId(this.data().boardId);
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,
});
}
}
}, },
'click .js-copy-link'(event) { 'click .js-copy-link'(event) {
event.preventDefault(); event.preventDefault();
@ -396,34 +311,6 @@ BlazeComponent.extendComponent({
Meteor.call('changeDateFormat', dateFormat); Meteor.call('changeDateFormat', dateFormat);
}, },
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'), '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) { 'submit .js-card-description'(event) {
event.preventDefault(); event.preventDefault();
const description = this.currentComponent().getValue(); const description = this.currentComponent().getValue();
@ -543,57 +430,56 @@ BlazeComponent.extendComponent({
) { ) {
newState = forIt; newState = forIt;
} }
// Use secure server method; direct client updates to vote are blocked this.data().setVote(Meteor.userId(), newState);
Meteor.call('cards.vote', this.data()._id, newState);
}, },
'click .js-poker'(e) { 'click .js-poker'(e) {
let newState = null; let newState = null;
if ($(e.target).hasClass('js-poker-vote-one')) { if ($(e.target).hasClass('js-poker-vote-one')) {
newState = 'one'; newState = 'one';
Meteor.call('cards.pokerVote', this.data()._id, newState); this.data().setPoker(Meteor.userId(), newState);
} }
if ($(e.target).hasClass('js-poker-vote-two')) { if ($(e.target).hasClass('js-poker-vote-two')) {
newState = 'two'; newState = 'two';
Meteor.call('cards.pokerVote', this.data()._id, newState); this.data().setPoker(Meteor.userId(), newState);
} }
if ($(e.target).hasClass('js-poker-vote-three')) { if ($(e.target).hasClass('js-poker-vote-three')) {
newState = 'three'; newState = 'three';
Meteor.call('cards.pokerVote', this.data()._id, newState); this.data().setPoker(Meteor.userId(), newState);
} }
if ($(e.target).hasClass('js-poker-vote-five')) { if ($(e.target).hasClass('js-poker-vote-five')) {
newState = 'five'; newState = 'five';
Meteor.call('cards.pokerVote', this.data()._id, newState); this.data().setPoker(Meteor.userId(), newState);
} }
if ($(e.target).hasClass('js-poker-vote-eight')) { if ($(e.target).hasClass('js-poker-vote-eight')) {
newState = 'eight'; newState = 'eight';
Meteor.call('cards.pokerVote', this.data()._id, newState); this.data().setPoker(Meteor.userId(), newState);
} }
if ($(e.target).hasClass('js-poker-vote-thirteen')) { if ($(e.target).hasClass('js-poker-vote-thirteen')) {
newState = 'thirteen'; newState = 'thirteen';
Meteor.call('cards.pokerVote', this.data()._id, newState); this.data().setPoker(Meteor.userId(), newState);
} }
if ($(e.target).hasClass('js-poker-vote-twenty')) { if ($(e.target).hasClass('js-poker-vote-twenty')) {
newState = 'twenty'; newState = 'twenty';
Meteor.call('cards.pokerVote', this.data()._id, newState); this.data().setPoker(Meteor.userId(), newState);
} }
if ($(e.target).hasClass('js-poker-vote-forty')) { if ($(e.target).hasClass('js-poker-vote-forty')) {
newState = 'forty'; newState = 'forty';
Meteor.call('cards.pokerVote', this.data()._id, newState); this.data().setPoker(Meteor.userId(), newState);
} }
if ($(e.target).hasClass('js-poker-vote-one-hundred')) { if ($(e.target).hasClass('js-poker-vote-one-hundred')) {
newState = 'oneHundred'; newState = 'oneHundred';
Meteor.call('cards.pokerVote', this.data()._id, newState); this.data().setPoker(Meteor.userId(), newState);
} }
if ($(e.target).hasClass('js-poker-vote-unsure')) { if ($(e.target).hasClass('js-poker-vote-unsure')) {
newState = 'unsure'; newState = 'unsure';
Meteor.call('cards.pokerVote', this.data()._id, newState); this.data().setPoker(Meteor.userId(), newState);
} }
}, },
'click .js-poker-finish'(e) { 'click .js-poker-finish'(e) {
if ($(e.target).hasClass('js-poker-finish')) { if ($(e.target).hasClass('js-poker-finish')) {
e.preventDefault(); e.preventDefault();
const now = new Date(); const now = formatDateTime(new Date());
Meteor.call('cards.setPokerEnd', this.data()._id, now); this.data().setPokerEnd(now);
} }
}, },
@ -601,9 +487,9 @@ BlazeComponent.extendComponent({
if ($(e.target).hasClass('js-poker-replay')) { if ($(e.target).hasClass('js-poker-replay')) {
e.preventDefault(); e.preventDefault();
this.currentCard = this.currentData(); this.currentCard = this.currentData();
Meteor.call('cards.replayPoker', this.currentCard._id); this.currentCard.replayPoker();
Meteor.call('cards.unsetPokerEnd', this.currentCard._id); this.data().unsetPokerEnd();
Meteor.call('cards.unsetPokerEstimation', this.currentCard._id); this.data().unsetPokerEstimation();
} }
}, },
'click .js-poker-estimation'(event) { 'click .js-poker-estimation'(event) {
@ -614,9 +500,9 @@ BlazeComponent.extendComponent({
this.find('#pokerEstimation').value = ''; this.find('#pokerEstimation').value = '';
if (ruleTitle) { if (ruleTitle) {
Meteor.call('cards.setPokerEstimation', this.data()._id, parseInt(ruleTitle, 10)); this.data().setPokerEstimation(parseInt(ruleTitle, 10));
} else { } else {
Meteor.call('cards.unsetPokerEstimation', this.data()._id); this.data().setPokerEstimation('');
} }
} }
}, },
@ -796,7 +682,6 @@ Template.editCardSortOrderForm.onRendered(function () {
Template.cardDetailsActionsPopup.helpers({ Template.cardDetailsActionsPopup.helpers({
isWatching() { isWatching() {
if (!this || typeof this.findWatcher !== 'function') return false;
return this.findWatcher(Meteor.userId()); return this.findWatcher(Meteor.userId());
}, },
@ -950,42 +835,26 @@ Template.editCardAssignerForm.events({
}); });
/** Move Card Dialog */ /** Move Card Dialog */
(class extends DialogWithBoardSwimlaneListCard { (class extends DialogWithBoardSwimlaneList {
getDialogOptions() { getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions(); const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret; return ret;
} }
setDone(cardId, options) { setDone(boardId, swimlaneId, listId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data(); const card = this.data();
let sortIndex = 0; const minOrder = card.getMinSort(listId, swimlaneId);
card.move(boardId, swimlaneId, listId, minOrder - 1);
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);
} }
}).register('moveCardPopup'); }).register('moveCardPopup');
/** Copy Card Dialog */ /** Copy Card Dialog */
(class extends DialogWithBoardSwimlaneListCard { (class extends DialogWithBoardSwimlaneList {
getDialogOptions() { getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions(); const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret; return ret;
} }
setDone(cardId, options) { setDone(boardId, swimlaneId, listId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data(); const card = this.data();
@ -994,30 +863,8 @@ Template.editCardAssignerForm.events({
const title = textarea.val().trim(); const title = textarea.val().trim();
if (title) { if (title) {
const newCardId = Meteor.call('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, true, {title: title}); // insert new card to the top of new list
const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, 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);
}
// In case the filter is active we need to add the newly inserted card in // 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 // the list of exceptions -- cards that are not filtered. Otherwise the
@ -1029,12 +876,12 @@ Template.editCardAssignerForm.events({
}).register('copyCardPopup'); }).register('copyCardPopup');
/** Convert Checklist-Item to card dialog */ /** Convert Checklist-Item to card dialog */
(class extends DialogWithBoardSwimlaneListCard { (class extends DialogWithBoardSwimlaneList {
getDialogOptions() { getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions(); const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret; return ret;
} }
setDone(cardId, options) { setDone(boardId, swimlaneId, listId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data(); const card = this.data();
@ -1044,29 +891,14 @@ Template.editCardAssignerForm.events({
if (title) { if (title) {
const _id = Cards.insert({ const _id = Cards.insert({
title: title, title: title,
listId: options.listId, listId: listId,
boardId: options.boardId, boardId: boardId,
swimlaneId: options.swimlaneId, swimlaneId: swimlaneId,
sort: 0, sort: 0,
}); });
const newCard = ReactiveCache.getCard(_id); const card = ReactiveCache.getCard(_id);
const minOrder = card.getMinSort();
let sortIndex = 0; card.move(card.boardId, card.swimlaneId, card.listId, minOrder - 1);
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);
Filter.addException(_id); Filter.addException(_id);
} }
@ -1074,12 +906,12 @@ Template.editCardAssignerForm.events({
}).register('convertChecklistItemToCardPopup'); }).register('convertChecklistItemToCardPopup');
/** Copy many cards dialog */ /** Copy many cards dialog */
(class extends DialogWithBoardSwimlaneListCard { (class extends DialogWithBoardSwimlaneList {
getDialogOptions() { getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions(); const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret; return ret;
} }
setDone(cardId, options) { setDone(boardId, swimlaneId, listId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data(); const card = this.data();
@ -1089,29 +921,7 @@ Template.editCardAssignerForm.events({
if (title) { if (title) {
const titleList = JSON.parse(title); const titleList = JSON.parse(title);
for (const obj of titleList) { 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}); const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, 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);
}
// In case the filter is active we need to add the newly inserted card in // 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 // the list of exceptions -- cards that are not filtered. Otherwise the
@ -1161,51 +971,6 @@ BlazeComponent.extendComponent({
}, },
}).register('setCardColorPopup'); }).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({ BlazeComponent.extendComponent({
onCreated() { onCreated() {
this.currentCard = this.currentData(); this.currentCard = this.currentData();
@ -1340,15 +1105,20 @@ BlazeComponent.extendComponent({
'is-checked', 'is-checked',
); );
const endString = this.currentCard.getVoteEnd(); const endString = this.currentCard.getVoteEnd();
Meteor.call('cards.setVoteQuestion', this.currentCard._id, voteQuestion, publicVote, allowNonBoardMembers);
this.currentCard.setVoteQuestion(
voteQuestion,
publicVote,
allowNonBoardMembers,
);
if (endString) { if (endString) {
Meteor.call('cards.setVoteEnd', this.currentCard._id, endString); this.currentCard.setVoteEnd(endString);
} }
Popup.back(); Popup.back();
}, },
'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => { 'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => {
event.preventDefault(); event.preventDefault();
Meteor.call('cards.unsetVote', this.currentCard._id); this.currentCard.unsetVote();
Popup.back(); Popup.back();
}), }),
'click a.js-toggle-vote-public'(event) { 'click a.js-toggle-vote-public'(event) {
@ -1547,10 +1317,10 @@ BlazeComponent.extendComponent({
]; ];
} }
_storeDate(newDate) { _storeDate(newDate) {
Meteor.call('cards.setVoteEnd', this.card._id, newDate); this.card.setVoteEnd(newDate);
} }
_deleteDate() { _deleteDate() {
Meteor.call('cards.unsetVoteEnd', this.card._id); this.card.unsetVoteEnd();
} }
}.register('editVoteEndDatePopup')); }.register('editVoteEndDatePopup'));
@ -1572,14 +1342,17 @@ BlazeComponent.extendComponent({
); );
const endString = this.currentCard.getPokerEnd(); const endString = this.currentCard.getPokerEnd();
Meteor.call('cards.setPokerQuestion', this.currentCard._id, pokerQuestion, allowNonBoardMembers); this.currentCard.setPokerQuestion(
pokerQuestion,
allowNonBoardMembers,
);
if (endString) { if (endString) {
Meteor.call('cards.setPokerEnd', this.currentCard._id, new Date(endString)); this.currentCard.setPokerEnd(endString);
} }
Popup.back(); Popup.back();
}, },
'click .js-remove-poker': Popup.afterConfirm('deletePoker', (event) => { 'click .js-remove-poker': Popup.afterConfirm('deletePoker', (event) => {
Meteor.call('cards.unsetPoker', this.currentCard._id); this.currentCard.unsetPoker();
Popup.back(); Popup.back();
}), }),
'click a.js-toggle-poker-allow-non-members'(event) { 'click a.js-toggle-poker-allow-non-members'(event) {
@ -1800,10 +1573,10 @@ BlazeComponent.extendComponent({
]; ];
} }
_storeDate(newDate) { _storeDate(newDate) {
Meteor.call('cards.setPokerEnd', this.card._id, newDate); this.card.setPokerEnd(newDate);
} }
_deleteDate() { _deleteDate() {
Meteor.call('cards.unsetPokerEnd', this.card._id); this.card.unsetPokerEnd();
} }
}.register('editPokerEndDatePopup')); }.register('editPokerEndDatePopup'));

View file

@ -37,23 +37,14 @@ textarea.js-edit-checklist-item {
.checklist-progress-bar-container .checklist-progress-bar { .checklist-progress-bar-container .checklist-progress-bar {
width: 80%; width: 80%;
height: 10px; height: 10px;
background-color: #d6ebff !important;
border-radius: 16px;
} }
.checklist-progress-bar-container .checklist-progress-bar .checklist-progress { .checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
color: #fff !important; color: #fff !important;
background-color: #3cb500 !important; background-color: #2196f3 !important;
padding: 0.01em 16px; padding: 0.01em 16px;
border-radius: 16px; border-radius: 16px;
height: 100%; 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 { .checklist-title {
padding: 10px; padding: 10px;
} }
@ -76,14 +67,10 @@ body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-ba
.checklist-title .checklist-stat.is-finished { .checklist-title .checklist-stat.is-finished {
color: #3cb500; color: #3cb500;
} }
.checklist-title span.checklist-handle { .checklist-title span.fa.checklist-handle {
padding-right: 20px; padding-right: 20px;
padding-top: 3px; padding-top: 3px;
float: left; float: left;
display: inline-block;
width: 1.2em;
text-align: center;
color: #999;
} }
#card-details-overlay { #card-details-overlay {
top: 0; top: 0;
@ -114,25 +101,6 @@ body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-ba
height: auto; height: auto;
overflow: hidden; 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 { .checklist-item.is-checked.invisible {
opacity: 0; opacity: 0;
height: 0; height: 0;
@ -162,27 +130,6 @@ body.mobile-mode.iphone-device .checklist-item span.checklistitem-handle {
border-bottom: 2px solid #3cb500; border-bottom: 2px solid #3cb500;
border-right: 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 { .checklist-item .item-title {
flex: 1; flex: 1;
} }
@ -197,14 +144,9 @@ body.grey-icons-enabled .cardCustomField-checkbox .check-box-unicode {
word-wrap: break-word; word-wrap: break-word;
max-width: 420px; max-width: 420px;
} }
.checklist-item span.checklistitem-handle { .checklist-item span.fa.checklistitem-handle {
padding-top: 2px; padding-top: 2px;
padding-right: 10px; padding-right: 10px;
display: inline-block;
width: 1.2em;
text-align: center;
color: #999;
cursor: pointer;
} }
.js-delete-checklist-item, .js-delete-checklist-item,
.js-convert-checklist-item-to-card { .js-convert-checklist-item-to-card {

View file

@ -9,10 +9,19 @@ template(name="checklists")
else else
a.add-checklist-top.js-open-inlined-form(title="{{_ 'add-checklist'}}") 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 .card-checklist-items
each checklist in checklists each checklist in checklists
+checklistDetail(checklist = checklist card = card) if checklist.showChecklist card.hideFinishedChecklistIfItemsAreHidden
+checklistDetail(checklist = checklist card = card)
if canModifyCard if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId) +inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId)
@ -29,12 +38,12 @@ template(name="checklistDetail")
.checklist-title .checklist-title
span span
if canModifyCard 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 if canModifyCard
h4.title.js-open-inlined-form.is-editable h4.title.js-open-inlined-form.is-editable
if isTouchScreenOrShowDesktopDragHandles if isTouchScreenOrShowDesktopDragHandles
span.checklist-handle(title="{{_ 'dragChecklist'}}") ↕️ span.fa.checklist-handle(class="fa-arrows" title="{{_ 'dragChecklist'}}")
+viewer +viewer
= checklist.title = checklist.title
else else
@ -53,10 +62,6 @@ template(name="checklistDeletePopup")
p {{_ 'confirm-checklist-delete-popup'}} p {{_ 'confirm-checklist-delete-popup'}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}} 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") template(name="addChecklistItemForm")
a(title="{{_ 'copy-text-to-clipboard'}}") a(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}} span.copied-tooltip {{_ 'copied'}}
@ -64,7 +69,6 @@ template(name="addChecklistItemForm")
.edit-controls.clearfix .edit-controls.clearfix
button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}} button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
a.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}") a.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
| ❌
if showNewlineBecomesNewChecklistItem if showNewlineBecomesNewChecklistItem
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItem'}}") .material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItem'}}")
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItem") input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItem")
@ -87,16 +91,12 @@ template(name="editChecklistItemForm")
.edit-controls.clearfix .edit-controls.clearfix
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}} button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
a.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}") a.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
| ❌
span(title=createdAt) {{ moment createdAt }} span(title=createdAt) {{ moment createdAt }}
if canModifyCard if canModifyCard
if $eq type 'item' a.js-delete-checklist-item {{_ "delete"}}...
a.js-delete-checklist-item {{_ "delete"}}... a.js-convert-checklist-item-to-card
a.js-convert-checklist-item-to-card | 📋
| 📋 | {{_ 'convertChecklistItemToCardPopup-title'}}
| {{_ 'convertChecklistItemToCardPopup-title'}}
else
a.js-delete-checklist {{_ "delete"}}...
template(name="checklistItems") template(name="checklistItems")
if checklist.items.length if checklist.items.length
@ -123,13 +123,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}}" .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") role="checkbox" aria-checked="{{#if item.isFinished }}true{{else}}false{{/if}}" tabindex="0")
if canModifyCard if canModifyCard
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}} .check-box-container
span.checklistitem-handle(title="{{_ 'dragChecklistItem'}}") ↕️ .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}}") .item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer +viewer
= item.title = item.title
else 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}}") .item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer +viewer
= item.title = item.title
@ -164,62 +165,34 @@ template(name="checklistActionsPopup")
else else
input.toggle-switch(type="checkbox" id="toggleHideAllChecklistItems_{{checklist._id}}") input.toggle-switch(type="checkbox" id="toggleHideAllChecklistItems_{{checklist._id}}")
label.toggle-label(for="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") template(name="copyChecklistPopup")
unless currentUser.isWorker +copyAndMoveChecklist
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'}}
template(name="moveChecklistPopup") template(name="moveChecklistPopup")
+copyAndMoveChecklist
template(name="copyAndMoveChecklist")
unless currentUser.isWorker unless currentUser.isWorker
label {{_ 'boards'}}: label {{_ 'boards'}}:
select.js-select-boards(autofocus) select.js-select-boards(autofocus)
each boards 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'}}: label {{_ 'swimlanes'}}:
select.js-select-swimlanes select.js-select-swimlanes
each 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'}}: label {{_ 'lists'}}:
select.js-select-lists select.js-select-lists
each 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 select.js-select-cards
each 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 .edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}} button.primary.confirm.js-done {{_ 'done'}}

View file

@ -65,7 +65,7 @@ BlazeComponent.extendComponent({
$(self.itemsDom).sortable('option', 'disabled', !userIsMember()); $(self.itemsDom).sortable('option', 'disabled', !userIsMember());
if (Utils.isTouchScreenOrShowDesktopDragHandles()) { if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$(self.itemsDom).sortable({ $(self.itemsDom).sortable({
handle: 'span.checklistitem-handle', handle: 'span.fa.checklistitem-handle',
}); });
} }
} }
@ -157,6 +157,14 @@ BlazeComponent.extendComponent({
textarea.focus(); textarea.focus();
}, },
deleteItem() {
const checklist = this.currentData().checklist;
const item = this.currentData().item;
if (checklist && item && item._id) {
ChecklistItems.remove(item._id);
}
},
editChecklist(event) { editChecklist(event) {
event.preventDefault(); event.preventDefault();
const textarea = this.find('textarea.js-edit-checklist-item'); const textarea = this.find('textarea.js-edit-checklist-item');
@ -208,28 +216,14 @@ BlazeComponent.extendComponent({
'submit .js-add-checklist-item': this.addChecklistItem, 'submit .js-add-checklist-item': this.addChecklistItem,
'submit .js-edit-checklist-item': this.editChecklistItem, 'submit .js-edit-checklist-item': this.editChecklistItem,
'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'), 'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
'click .js-delete-checklist-item'(event) { 'click .js-delete-checklist-item': this.deleteItem,
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);
},
'focus .js-add-checklist-item': this.focusChecklistItem, 'focus .js-add-checklist-item': this.focusChecklistItem,
// add and delete checklist / checklist-item // add and delete checklist / checklist-item
'click .js-open-inlined-form': this.closeAllInlinedForms, 'click .js-open-inlined-form': this.closeAllInlinedForms,
'click #toggleHideFinishedChecklist'(event) {
event.preventDefault();
this.data().card.toggleHideFinishedChecklist();
},
keydown: this.pressKey, keydown: this.pressKey,
}, },
]; ];
@ -281,8 +275,8 @@ BlazeComponent.extendComponent({
Template.checklists.helpers({ Template.checklists.helpers({
checklists() { checklists() {
const card = ReactiveCache.getCard(this.cardId); const card = ReactiveCache.getCard(this.cardId);
if (!card || typeof card.checklists !== 'function') return []; const ret = card.checklists();
return card.checklists(); return ret;
}, },
}); });
@ -309,16 +303,13 @@ BlazeComponent.extendComponent({
events() { events() {
return [ return [
{ {
'click .js-delete-checklist'(event) { 'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () {
const checklist = this.data().checklist; Popup.back(2);
const confirmFunc = Popup.afterConfirm('checklistDelete', function () { const checklist = this.checklist;
Popup.back(2); if (checklist && checklist._id) {
if (checklist && checklist._id) { Checklists.remove(checklist._id);
Checklists.remove(checklist._id); }
} }),
});
confirmFunc.call(this, event);
},
'click .js-move-checklist': Popup.open('moveChecklist'), 'click .js-move-checklist': Popup.open('moveChecklist'),
'click .js-copy-checklist': Popup.open('copyChecklist'), 'click .js-copy-checklist': Popup.open('copyChecklist'),
'click .js-hide-checked-checklist-items'(event) { 'click .js-hide-checked-checklist-items'(event) {
@ -331,12 +322,6 @@ BlazeComponent.extendComponent({
this.data().checklist.toggleHideAllChecklistItems(); this.data().checklist.toggleHideAllChecklistItems();
Popup.back(); 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() { events() {
return [ return [
{ {
'click .js-checklist-item .check-box-unicode': this.toggleItem,
'click .js-checklist-item .check-box-container': this.toggleItem, 'click .js-checklist-item .check-box-container': this.toggleItem,
}, },
]; ];
@ -390,12 +374,7 @@ BlazeComponent.extendComponent({
} }
setDone(cardId, options) { setDone(cardId, options) {
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options); ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
const checklist = this.data().checklist; this.data().checklist.move(cardId);
Meteor.call('moveChecklist', checklist._id, cardId, (error) => {
if (error) {
console.error('Error moving checklist:', error);
}
});
} }
}).register('moveChecklistPopup'); }).register('moveChecklistPopup');

View file

@ -223,13 +223,9 @@
.card-label-edit-button:hover { .card-label-edit-button:hover {
background: #dbdbdb; background: #dbdbdb;
} }
ul.edit-labels-pop-over span.label-handle { ul.edit-labels-pop-over span.fa.label-handle {
padding-right: 10px; 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; max-width: 180px;
} }

View file

@ -31,7 +31,7 @@ template(name="cardLabelsPopup")
a.card-label-edit-button.js-edit-label a.card-label-edit-button.js-edit-label
| ✏️ | ✏️
if isTouchScreenOrShowDesktopDragHandles 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}}" span.card-label.card-label-selectable.js-select-label.card-label-wrapper(class="card-label-{{color}}"
class="{{# if isLabelSelected ../_id }}active{{/if}}") class="{{# if isLabelSelected ../_id }}active{{/if}}")
+viewer +viewer

View file

@ -142,12 +142,9 @@
display: block; display: block;
} }
} }
.minicard .handle .drag-handle { .minicard .handle .fa-arrows {
font-size: clamp(16px, 3vw, 20px); font-size: clamp(16px, 3vw, 20px);
color: #ccc; color: #ccc;
display: inline-block;
width: 1.4em;
text-align: center;
} }
.minicard .minicard-title .card-number { .minicard .minicard-title .card-number {
color: #b3b3b3; color: #b3b3b3;
@ -300,6 +297,19 @@
background-color: #1976d2 !important; background-color: #1976d2 !important;
} }
/* Font Awesome icons in minicard dates */
.minicard .card-date i.fa {
margin-right: 0.3vw;
font-size: 0.9em;
vertical-align: middle;
}
/* Font Awesome icons in minicard spent time */
.minicard .card-time i.fa {
margin-right: 0.3vw;
font-size: 0.9em;
vertical-align: middle;
}
.minicard .badges { .minicard .badges {
float: left; float: left;
margin-top: 1vh; margin-top: 1vh;
@ -731,80 +741,7 @@
gap: 0.3vw; gap: 0.3vw;
} }
/* Checklist display on minicard */ .minicard-list-name i.fa {
.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 {
font-size: 0.8em; font-size: 0.8em;
font-weight: bold; opacity: 0.7;
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;
} }

View file

@ -5,7 +5,6 @@ template(name="minicard")
class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}") class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}")
if canModifyCard if canModifyCard
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") ☰ a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") ☰
if canMoveCard
.handle .handle
| ↕️ | ↕️
.dates .dates
@ -142,7 +141,7 @@ template(name="minicard")
if canModifyCard if canModifyCard
if comments.length if comments.length
.badge(title="{{_ 'card-comments-title' comments.length }}") .badge(title="{{_ 'card-comments-title' comments.length }}")
span.badge-icon.badge-comment.badge-text 💬 span.badge-icon.badge-comment.badge-text | 💬
= ' ' = ' '
= comments.length = comments.length
//span.badge-comment.badge-text //span.badge-comment.badge-text
@ -150,36 +149,37 @@ template(name="minicard")
if getDescription if getDescription
unless currentBoard.allowsDescriptionTextOnMinicard unless currentBoard.allowsDescriptionTextOnMinicard
.badge.badge-state-image-only(title=getDescription) .badge.badge-state-image-only(title=getDescription)
span.badge-icon 📝 span.badge-icon | 📝
if getVoteQuestion if getVoteQuestion
.badge.badge-state-image-only(title=getVoteQuestion) .badge.badge-state-image-only(title=getVoteQuestion)
span.badge-icon(class="{{#if voteState}}text-green{{/if}}") 👍 span.badge-icon(class="{{#if voteState}}text-green{{/if}}") | 👍
span.badge-text {{ voteCountPositive }} span.badge-text {{ voteCountPositive }}
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}") 👎 span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}") | 👎
span.badge-text {{ voteCountNegative }} span.badge-text {{ voteCountNegative }}
if getPokerQuestion if getPokerQuestion
.badge.badge-state-image-only(title=getPokerQuestion) .badge.badge-state-image-only(title=getPokerQuestion)
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}") ✅ span.badge-icon(class="{{#if pokerState}}text-green{{/if}}") |
if expiredPoker if expiredPoker
span.badge-text {{ getPokerEstimation }} span.badge-text {{ getPokerEstimation }}
if attachments.length if attachments.length
if currentBoard.allowsBadgeAttachmentOnMinicard if currentBoard.allowsBadgeAttachmentOnMinicard
.badge .badge
span.badge-icon 📎 span.badge-icon | 📎
span.badge-text= attachments.length 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 if allSubtasks.count
.badge .badge
span.badge-icon 🌐 span.badge-icon | 🌐
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}} span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down //{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
if currentBoard.allowsCardSortingByNumber if currentBoard.allowsCardSortingByNumber
if currentBoard.allowsCardSortingByNumberOnMinicard if currentBoard.allowsCardSortingByNumberOnMinicard
.badge .badge
span.badge-icon 🔢 span.badge-icon | 🔢
span.badge-text.check-list-sort {{ sort }} span.badge-text.check-list-sort {{ sort }}
if shouldShowChecklistAtMinicard
each shouldShowChecklistAtMinicard
+minicardChecklist(checklist=. card=..)
if currentBoard.allowsDescriptionTextOnMinicard if currentBoard.allowsDescriptionTextOnMinicard
if getDescription if getDescription
.minicard-description .minicard-description
@ -201,12 +201,55 @@ template(name="editCardSortOrderPopup")
.edit-controls.clearfix .edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-sort-popup(type="submit") {{_ 'save'}} button.primary.confirm.js-submit-edit-card-sort-popup(type="submit") {{_ 'save'}}
template(name="minicardChecklist") template(name="minicardDetailsActionsPopup")
.minicard-checklist ul.pop-over-list
.checklist-header if canModifyCard
.checklist-title= checklist.title li
if canModifyCard a.js-move-card
a.checklist-menu.js-open-checklist-menu(title="{{_ 'checklistActionsPopup-title'}}") ☰ | ➡️
each visibleItems | {{_ 'moveCardPopup-title'}}
+checklistItemDetail(item = . checklist = checklist card = card) 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() { events() {
return [ 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 span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
'click .minicard-labels' : this.cardLabelsPopup, '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 // Drag and drop file upload handlers
'dragover .minicard'(event) { 'dragover .minicard'(event) {
// Only prevent default for file drags to avoid interfering with sortable // Only prevent default for file drags to avoid interfering with sortable
@ -177,43 +170,6 @@ BlazeComponent.extendComponent({
}, },
}).register('minicard'); }).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({ Template.minicard.helpers({
hiddenMinicardLabelText() { hiddenMinicardLabelText() {
const currentUser = ReactiveCache.getCurrentUser(); const currentUser = ReactiveCache.getCurrentUser();
@ -253,29 +209,9 @@ Template.minicard.helpers({
// Show list name if either: // Show list name if either:
// 1. Board-wide setting is enabled, OR // 1. Board-wide setting is enabled, OR
// 2. This specific card has the setting enabled // 2. This specific card has the setting enabled
const currentBoard = this.board(); const currentBoard = this.currentBoard;
if (!currentBoard) return false; if (!currentBoard) return false;
return currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard; 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'); }).register('editCardSortOrderPopup');
Template.cardDetailsActionsPopup.events({ Template.minicardDetailsActionsPopup.events({
'click .js-due-date': Popup.open('editCardDueDate'), 'click .js-due-date': Popup.open('editCardDueDate'),
'click .js-move-card': Popup.open('moveCard'), 'click .js-move-card': Popup.open('moveCard'),
'click .js-copy-card': Popup.open('copyCard'), 'click .js-copy-card': Popup.open('copyCard'),

View file

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

View file

@ -87,15 +87,6 @@ textarea.js-edit-subtask-item {
top: 0; top: 0;
bottom: -600px; bottom: -600px;
right: 0; 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 { .subtasks {
background: #f7f7f7; background: #f7f7f7;
@ -136,25 +127,6 @@ textarea.js-edit-subtask-item {
border-bottom: 2px solid #3cb500; border-bottom: 2px solid #3cb500;
border-right: 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 { .subtasks-item .item-title {
flex: 1; flex: 1;
padding-left: 10px; padding-left: 10px;

View file

@ -74,12 +74,12 @@ template(name="subtasksItems")
template(name='subtaskItemDetail') template(name='subtaskItemDetail')
.js-subtasks-item.subtasks-item .js-subtasks-item.subtasks-item
if canModifyCard 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}}") .item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer +viewer
= item.title = item.title
else 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}}") .item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer +viewer
= item.title = item.title

View file

@ -104,19 +104,7 @@ BlazeComponent.extendComponent({
}).register('subtasks'); }).register('subtasks');
BlazeComponent.extendComponent({ 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'); }).register('subtaskItemDetail');
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({

View file

@ -2,17 +2,19 @@
<div class="original-position-info"> <div class="original-position-info">
{{#if isLoading}} {{#if isLoading}}
<div class="original-position-loading"> <div class="original-position-loading">
Loading original position... <i class="fa fa-spinner fa-spin"></i> Loading original position...
</div> </div>
{{else if showOriginalPosition}} {{else if showOriginalPosition}}
<div class="original-position-details"> <div class="original-position-details">
{{#if hasMovedFromOriginal}} {{#if hasMovedFromOriginal}}
<div class="original-position-moved"> <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> </div>
{{else}} {{else}}
<div class="original-position-unchanged"> <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> </div>
{{/if}} {{/if}}

View file

@ -315,18 +315,11 @@ textarea::-moz-placeholder {
margin-right: 6px; margin-right: 6px;
border-top: 2px solid transparent; border-top: 2px solid transparent;
border-left: 2px solid transparent; border-left: 2px solid transparent;
border-bottom: 2px solid #3cb500;
border-right: 2px solid #3cb500;
transform: rotate(40deg); transform: rotate(40deg);
-webkit-backface-visibility: hidden; -webkit-backface-visibility: hidden;
backface-visibility: hidden; backface-visibility: hidden;
transform-origin: 100% 100%; 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 { .button-link {
background: #fff; background: #fff;
background: linear-gradient(#fff, #f5f5f5); 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") template(name="importHeaderBar")
h1 h1
a.back-btn(href="{{pathFor 'home'}}") a.back-btn(href="{{pathFor 'home'}}")
| ⬅️ i.fa.fa-chevron-left
| {{_ title}} | {{_ title}}
template(name="import") template(name="import")
@ -36,7 +36,7 @@ template(name="importMapMembers")
+userAvatar(userId=wekanId) +userAvatar(userId=wekanId)
else else
a.member.add-member a.member.add-member
| i.fa.fa-plus
//- //-
Due to the way the flewbox layout is working, we need to set some 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. invisible items so that the last row items have a consistent width.
@ -56,17 +56,17 @@ template(name="importMapMembersAddPopup")
p p
| {{_ 'import-user-select'}} | {{_ 'import-user-select'}}
.js-map-member .js-map-member
input.js-search-member-input(type="text" placeholder="{{_ 'search-users'}}") +EasySearch.Input(index=searchIndex)
ul.pop-over-list ul.pop-over-list
each searchResults +EasySearch.Each(index=searchIndex)
li.item.js-member-item li.item.js-member-item
a.name.js-select-import(title="{{profile.fullname}} ({{username}})" data-id="{{_id}}") a.name.js-select-import(title="{{profile.fullname}} ({{username}})" data-id="{{__originalId}}")
+userAvatar(userId=_id) +userAvatar(userId=__originalId)
span.full-name span.full-name
= profile.fullname = profile.fullname
| (<span class="username">{{username}}</span>) | (<span class="username">{{username}}</span>)
if searching.get +EasySearch.IfSearching(index=searchIndex)
+spinner +spinner
if noResults.get +EasySearch.IfNoResults(index=searchIndex)
.manage-member-section .manage-member-section
p.quiet {{_ 'no-results'}} p.quiet {{_ 'no-results'}}

View file

@ -311,73 +311,6 @@ BlazeComponent.extendComponent({
}, },
}).register('importMapMembersAddPopup'); }).register('importMapMembersAddPopup');
// Global reactive variables for import member popup
const importMemberPopupState = {
searching: new ReactiveVar(false),
searchResults: new ReactiveVar([]),
noResults: new ReactiveVar(false),
searchTimeout: null
};
BlazeComponent.extendComponent({
onCreated() {
// Use global state
this.searching = importMemberPopupState.searching;
this.searchResults = importMemberPopupState.searchResults;
this.noResults = importMemberPopupState.noResults;
this.searchTimeout = importMemberPopupState.searchTimeout;
},
onRendered() {
this.find('.js-search-member-input').focus();
},
performSearch(query) {
if (!query || query.length < 2) {
this.searchResults.set([]);
this.noResults.set(false);
return;
}
this.searching.set(true);
this.noResults.set(false);
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
this.searchResults.set(results);
this.searching.set(false);
if (results.length === 0) {
this.noResults.set(true);
}
},
events() {
return [
{
'keyup .js-search-member-input'(event) {
const query = event.target.value.trim();
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
this.searchTimeout = setTimeout(() => {
this.performSearch(query);
}, 300);
},
},
];
},
}).register('importMapMembersAddPopupSearch');
Template.importMapMembersAddPopup.helpers({ Template.importMapMembersAddPopup.helpers({
searchResults() { searchIndex: () => UserSearchIndex,
return importMemberPopupState.searchResults.get();
},
searching() {
return importMemberPopupState.searching;
},
noResults() {
return importMemberPopupState.noResults;
}
}) })

View file

@ -282,7 +282,7 @@ body.list-resizing-active * {
margin: 0 auto; margin: 0 auto;
} }
.list.list-collapsed .list-header .js-collapse { .list.list-collapsed .list-header .js-collapse {
margin: 0 auto 0 auto; margin: 0 auto 20px auto;
z-index: 10; z-index: 10;
padding: 8px 12px; padding: 8px 12px;
font-size: 12px; font-size: 12px;
@ -290,12 +290,6 @@ body.list-resizing-active * {
display: block; display: block;
width: fit-content; 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 { .list.list-collapsed .list-header .list-rotated {
width: auto !important; width: auto !important;
height: auto !important; height: auto !important;
@ -303,6 +297,7 @@ body.list-resizing-active * {
position: relative !important; position: relative !important;
overflow: visible !important; overflow: visible !important;
} }
.list.list-collapsed .list-header .list-rotated h2.list-header-name { .list.list-collapsed .list-header .list-rotated h2.list-header-name {
text-align: left; text-align: left;
overflow: visible; overflow: visible;
@ -313,15 +308,15 @@ body.list-resizing-active * {
color: #333; color: #333;
background-color: rgba(255, 255, 255, 0.95); background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #ddd; border: 1px solid #ddd;
padding: 0; padding: 8px 4px;
border-radius: 4px; border-radius: 4px;
margin: 0; margin: 0 auto;
width: 100vh; width: 25vh;
height: 30px; height: 60vh;
position: absolute; position: absolute;
left: 40px; left: 50%;
top: 50%; top: 50%;
transform: translateY(calc(-50% + 20px)) rotate(0deg); transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
z-index: 10; z-index: 10;
visibility: visible !important; visibility: visible !important;
opacity: 1 !important; opacity: 1 !important;
@ -373,18 +368,6 @@ body.list-resizing-active * {
text-overflow: ellipsis; text-overflow: ellipsis;
word-wrap: break-word; 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 { .list-rotated {
width: 1.3vw; width: 1.3vw;
height: 35vh; height: 35vh;
@ -395,6 +378,9 @@ body.list-resizing-active * {
position: relative; position: relative;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
}
.list-header .list-rotated {
} }
.list-header .list-header-watch-icon { .list-header .list-header-watch-icon {
padding-left: 10px; padding-left: 10px;
@ -420,42 +406,22 @@ body.list-resizing-active * {
color: #a6a6a6; color: #a6a6a6;
margin-right: 15px; 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 { .list-header .js-collapse {
color: #a6a6a6; color: #a6a6a6;
margin-right: 15px;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
padding: 5px 8px; padding: 5px 8px;
border: none; border: 1px solid #ccc;
border-radius: 0; border-radius: 4px;
background-color: transparent; background-color: #f5f5f5;
cursor: pointer; cursor: pointer;
font-size: 18px; font-size: 14px;
line-height: 1;
min-width: 30px;
text-align: center;
flex-shrink: 0;
text-decoration: none;
margin: 0;
} }
.list-header .js-collapse:hover { .list-header .js-collapse:hover {
background-color: transparent; background-color: #e0e0e0;
color: #333; color: #333;
} }
.list-header .list-header-collapse-container > div {
flex: 1;
min-width: 0;
}
.list.list-collapsed .list-header .js-collapse { .list.list-collapsed .list-header .js-collapse {
display: inline-block !important; display: inline-block !important;
visibility: visible !important; visibility: visible !important;
@ -484,18 +450,17 @@ body.list-resizing-active * {
position: relative !important; position: relative !important;
} }
.list.list-collapsed .list-header .list-rotated h2.list-header-name { .list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: 100vh; width: 15vh;
font-size: 12px; font-size: 12px;
height: 30px; height: 30px;
line-height: 1.2; line-height: 1.2;
padding: 0; padding: 8px 4px;
margin: 0; margin: 0 auto;
overflow: visible;
position: absolute; position: absolute;
left: 40px; left: 50%;
top: 50%; top: 50%;
transform: translateY(calc(-50% + 120px)) rotate(0deg); transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
text-align: center; text-align: left;
visibility: visible !important; visibility: visible !important;
opacity: 1 !important; opacity: 1 !important;
display: block !important; display: block !important;
@ -525,18 +490,17 @@ body.list-resizing-active * {
position: relative !important; position: relative !important;
} }
.list.list-collapsed .list-header .list-rotated h2.list-header-name { .list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: 100vh; width: 15vh;
font-size: 12px; font-size: 12px;
height: 30px; height: 30px;
line-height: 1.2; line-height: 1.2;
padding: 0; padding: 8px 4px;
margin: 0; margin: 0 auto;
overflow: visible;
position: absolute; position: absolute;
left: 40px; left: 50%;
top: 50%; top: 50%;
transform: translateY(calc(-50% + 120px)) rotate(0deg); transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
text-align: center; text-align: left;
visibility: visible !important; visibility: visible !important;
opacity: 1 !important; opacity: 1 !important;
display: block !important; display: block !important;
@ -566,17 +530,16 @@ body.list-resizing-active * {
position: relative !important; position: relative !important;
} }
.list.list-collapsed .list-header .list-rotated h2.list-header-name { .list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: 100vh; width: 15vh;
font-size: 12px; font-size: 12px;
height: 30px; height: 30px;
line-height: 1.2; line-height: 1.2;
padding: 0; padding: 8px 4px;
margin: 0; margin: 0 auto;
overflow: visible;
position: absolute; position: absolute;
left: 40px; left: 50%;
top: 50%; top: 50%;
transform: translateY(calc(-50% + 40px)) rotate(0deg); transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
text-align: left; text-align: left;
visibility: visible !important; visibility: visible !important;
opacity: 1 !important; opacity: 1 !important;
@ -681,22 +644,17 @@ body.list-resizing-active * {
.mini-list.mobile-view { .mini-list.mobile-view {
flex: 0 0 60px; flex: 0 0 60px;
height: auto; height: auto;
width: 100vw; width: 100%;
max-width: 100vw; min-width: 100%;
min-width: 100vw;
border-left: 0px !important; border-left: 0px !important;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important;
} }
.list.mobile-view { .list.mobile-view {
display: block !important; display: contents;
flex-basis: auto; flex-basis: auto;
width: 100vw; width: 100%;
max-width: 100vw; min-width: 100%;
min-width: 100vw;
border-left: 0px !important; border-left: 0px !important;
margin: 0 !important;
padding: 0 !important;
} }
.list.mobile-view:first-child { .list.mobile-view:first-child {
margin-left: 0px; margin-left: 0px;
@ -704,11 +662,9 @@ body.list-resizing-active * {
.list.mobile-view.ui-sortable-helper { .list.mobile-view.ui-sortable-helper {
flex: 0 0 60px; flex: 0 0 60px;
height: 60px; height: 60px;
width: 100vw; width: 100%;
max-width: 100vw;
border-left: 0px !important; border-left: 0px !important;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important;
} }
.list.mobile-view.ui-sortable-helper .list-header.ui-sortable-handle { .list.mobile-view.ui-sortable-helper .list-header.ui-sortable-handle {
cursor: grabbing; cursor: grabbing;
@ -716,17 +672,14 @@ body.list-resizing-active * {
.list.mobile-view.placeholder { .list.mobile-view.placeholder {
flex: 0 0 60px; flex: 0 0 60px;
height: 60px; height: 60px;
width: 100vw; width: 100%;
max-width: 100vw;
border-left: 0px !important; border-left: 0px !important;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important;
} }
.list.mobile-view .list-body { .list.mobile-view .list-body {
padding: 15px 19px; padding: 15px 19px;
width: 100vw; width: 100%;
max-width: 100vw; min-width: 100%;
min-width: 100vw;
} }
.list.mobile-view .list-header { .list.mobile-view .list-header {
/*Updated padding values for mobile devices, this should fix text grouping issue*/ /*Updated padding values for mobile devices, this should fix text grouping issue*/
@ -735,9 +688,8 @@ body.list-resizing-active * {
min-height: 30px; min-height: 30px;
margin-top: 10px; margin-top: 10px;
align-items: center; align-items: center;
width: 100vw; width: 100%;
max-width: 100vw; min-width: 100%;
min-width: 100vw;
/* Force grid layout for iPhone */ /* Force grid layout for iPhone */
display: grid !important; display: grid !important;
grid-template-columns: 30px 1fr auto auto !important; grid-template-columns: 30px 1fr auto auto !important;
@ -790,9 +742,6 @@ body.list-resizing-active * {
grid-row: 2; grid-row: 2;
grid-column: 2; grid-column: 2;
align-self: start; align-self: start;
text-align: left;
padding-left: 0;
margin-left: 0;
font-size: 16px !important; font-size: 16px !important;
line-height: 1.2; line-height: 1.2;
} }
@ -821,22 +770,17 @@ body.list-resizing-active * {
.mini-list { .mini-list {
flex: 0 0 60px; flex: 0 0 60px;
height: auto; height: auto;
width: 100vw; width: 100%;
max-width: 100vw; min-width: 100%;
min-width: 100vw;
border-left: 0px !important; border-left: 0px !important;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important;
} }
.list { .list {
display: block !important; display: contents;
flex-basis: auto; flex-basis: auto;
width: 100vw; width: 100%;
max-width: 100vw; min-width: 100%;
min-width: 100vw;
border-left: 0px !important; border-left: 0px !important;
margin: 0 !important;
padding: 0 !important;
} }
.list:first-child { .list:first-child {
margin-left: 0px; margin-left: 0px;
@ -844,11 +788,9 @@ body.list-resizing-active * {
.list.ui-sortable-helper { .list.ui-sortable-helper {
flex: 0 0 60px; flex: 0 0 60px;
height: 60px; height: 60px;
width: 100vw; width: 100%;
max-width: 100vw;
border-left: 0px !important; border-left: 0px !important;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important;
} }
.list.ui-sortable-helper .list-header.ui-sortable-handle { .list.ui-sortable-helper .list-header.ui-sortable-handle {
cursor: grabbing; cursor: grabbing;
@ -856,17 +798,14 @@ body.list-resizing-active * {
.list.placeholder { .list.placeholder {
flex: 0 0 60px; flex: 0 0 60px;
height: 60px; height: 60px;
width: 100vw; width: 100%;
max-width: 100vw;
border-left: 0px !important; border-left: 0px !important;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important;
} }
.list-body { .list-body {
padding: 15px 19px; padding: 15px 19px;
width: 100vw; width: 100%;
max-width: 100vw; min-width: 100%;
min-width: 100vw;
} }
.list-header { .list-header {
/*Updated padding values for mobile devices, this should fix text grouping issue*/ /*Updated padding values for mobile devices, this should fix text grouping issue*/
@ -875,9 +814,8 @@ body.list-resizing-active * {
min-height: 30px; min-height: 30px;
margin-top: 10px; margin-top: 10px;
align-items: center; align-items: center;
width: 100vw; width: 100%;
max-width: 100vw; min-width: 100%;
min-width: 100vw;
} }
.list-header .list-header-left-icon { .list-header .list-header-left-icon {
padding: 7px; padding: 7px;
@ -1007,9 +945,6 @@ body.list-resizing-active * {
grid-row: 2 !important; grid-row: 2 !important;
grid-column: 2 !important; grid-column: 2 !important;
align-self: start !important; align-self: start !important;
text-align: left !important;
padding-left: 0 !important;
margin-left: 0 !important;
font-size: 16px !important; font-size: 16px !important;
line-height: 1.2 !important; line-height: 1.2 !important;
} }
@ -1081,23 +1016,6 @@ body.list-resizing-active * {
grid-row: 1/3 !important; grid-row: 1/3 !important;
grid-column: 1 !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 { .link-board-wrapper {
display: flex; display: flex;
align-items: baseline; align-items: baseline;

View file

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

View file

@ -279,8 +279,7 @@ BlazeComponent.extendComponent({
// Only enable resize for non-collapsed, non-auto-width lists // Only enable resize for non-collapsed, non-auto-width lists
const isAutoWidth = this.autoWidth(); const isAutoWidth = this.autoWidth();
const isCollapsed = Utils.getListCollapseState(list); if (list.collapsed || isAutoWidth) {
if (isCollapsed || isAutoWidth) {
$resizeHandle.hide(); $resizeHandle.hide();
return; 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(() => { component.autorun(() => {
const collapsed = Utils.getListCollapseState(list); if (component.autoWidth()) {
if (component.autoWidth() || collapsed) {
$resizeHandle.hide(); $resizeHandle.hide();
} else { } else {
$resizeHandle.show(); $resizeHandle.show();
@ -454,12 +452,6 @@ BlazeComponent.extendComponent({
}, },
}).register('list'); }).register('list');
Template.list.helpers({
collapsed() {
return Utils.getListCollapseState(this);
},
});
Template.miniList.events({ Template.miniList.events({
'click .js-select-list'() { 'click .js-select-list'() {
const listId = this._id; const listId = this._id;

View file

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

View file

@ -16,50 +16,11 @@ BlazeComponent.extendComponent({
}, },
customFieldsSum() { customFieldsSum() {
const list = Template.currentData(); const ret = ReactiveCache.getCustomFields({
if (!list) return []; boardIds: { $in: [Session.get('currentBoard')] },
const boardId = Session.get('currentBoard');
const fields = ReactiveCache.getCustomFields({
boardIds: { $in: [boardId] },
showSumAtTopOfList: true, showSumAtTopOfList: true,
}); });
return ret;
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;
}, },
openForm(options) { openForm(options) {
@ -293,22 +254,6 @@ BlazeComponent.extendComponent({
}, },
}).register('listBody'); }).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) { function toggleValueInReactiveArray(reactiveValue, value) {
const array = reactiveValue.get(); const array = reactiveValue.get();
const valueIndex = array.indexOf(value); const valueIndex = array.indexOf(value);
@ -542,6 +487,8 @@ BlazeComponent.extendComponent({
{ {
sort: { sort: 1 }, sort: { sort: 1 },
}); });
if (swimlanes.length)
this.selectedSwimlaneId.set(swimlanes[0]._id);
return swimlanes; return swimlanes;
}, },
@ -556,6 +503,7 @@ BlazeComponent.extendComponent({
{ {
sort: { sort: 1 }, sort: { sort: 1 },
}); });
if (lists.length) this.selectedListId.set(lists[0]._id);
return lists; return lists;
}, },
@ -564,17 +512,19 @@ BlazeComponent.extendComponent({
return []; return [];
} }
const ownCardsIds = this.board.cards().map(card => card.getRealId()); 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, archived: false,
linkedId: { $nin: ownCardsIds }, linkedId: { $nin: ownCardsIds },
_id: { $nin: ownCardsIds }, _id: { $nin: ownCardsIds },
type: { $nin: ['template-card'] }, type: { $nin: ['template-card'] },
}; },
if (this.selectedBoardId.get()) selector.boardId = this.selectedBoardId.get(); {
if (this.selectedSwimlaneId.get()) selector.swimlaneId = this.selectedSwimlaneId.get(); sort: { sort: 1 },
if (this.selectedListId.get()) selector.listId = this.selectedListId.get(); });
const ret = ReactiveCache.getCards(selector, { sort: { sort: 1 } });
return ret; return ret;
}, },
@ -595,12 +545,8 @@ BlazeComponent.extendComponent({
return [ return [
{ {
'change .js-select-boards'(evt) { 'change .js-select-boards'(evt) {
const val = $(evt.currentTarget).val(); subManager.subscribe('board', $(evt.currentTarget).val(), false);
subManager.subscribe('board', val, false); this.selectedBoardId.set($(evt.currentTarget).val());
// Clear selections to allow linking only board or re-choose swimlane/list
this.selectedSwimlaneId.set('');
this.selectedListId.set('');
this.selectedBoardId.set(val);
}, },
'change .js-select-swimlanes'(evt) { 'change .js-select-swimlanes'(evt) {
this.selectedSwimlaneId.set($(evt.currentTarget).val()); this.selectedSwimlaneId.set($(evt.currentTarget).val());

View file

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

View file

@ -34,14 +34,13 @@ BlazeComponent.extendComponent({
}, },
collapsed(check = undefined) { collapsed(check = undefined) {
const list = Template.currentData(); const list = Template.currentData();
const status = Utils.getListCollapseState(list); const status = list.isCollapsed();
if (check === undefined) { if (check === undefined) {
// just check // just check
return status; return status;
} else { } else {
const next = typeof check === 'boolean' ? check : !status; list.collapse(!status);
Utils.setListCollapseState(list, next); return !status;
return next;
} }
}, },
editTitle(event) { editTitle(event) {
@ -143,48 +142,7 @@ BlazeComponent.extendComponent({
Template.listHeader.helpers({ Template.listHeader.helpers({
isBoardAdmin() { isBoardAdmin() {
return ReactiveCache.getCurrentUser().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({ Template.listActionPopup.helpers({

View file

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

View file

@ -32,16 +32,8 @@ template(name="dueCards")
span.global-search-error-messages span.global-search-error-messages
= msg = msg
else else
.due-cards-results-header
h1
= resultsText
each card in dueCardsList each card in dueCardsList
+resultCard(card) +resultCard(card)
else
.global-search-results-list-wrapper
.no-results
h3 {{_ 'dueCards-noResults-title'}}
p {{_ 'dueCards-noResults-description'}}
template(name="dueCardsViewChangePopup") template(name="dueCardsViewChangePopup")
if currentUser if currentUser

View file

@ -1,6 +1,5 @@
import { ReactiveCache } from '/imports/reactiveCache'; import { ReactiveCache } from '/imports/reactiveCache';
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components'; import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
import { TAPi18n } from '/imports/i18n';
// const subManager = new SubsManager(); // const subManager = new SubsManager();
@ -25,25 +24,22 @@ Template.dueCards.helpers({
return Meteor.userId(); return Meteor.userId();
}, },
dueCardsList() { dueCardsList() {
const component = BlazeComponent.getComponentForElement(this.firstNode); const component = BlazeComponent.getComponentForElement(this);
if (component && component.dueCardsList) { if (component && component.dueCardsList) {
return component.dueCardsList(); return component.dueCardsList();
} }
return []; return [];
}, },
hasResults() { hasResults() {
const component = BlazeComponent.getComponentForElement(this.firstNode); const component = BlazeComponent.getComponentForElement(this);
if (component && component.hasResults) { if (component && component.dueCardsList) {
return component.hasResults.get(); const cards = component.dueCardsList();
return cards && cards.length > 0;
} }
return false; return false;
}, },
searching() { searching() {
const component = BlazeComponent.getComponentForElement(this.firstNode); return false; // No longer using search, so always false
if (component && component.isLoading) {
return component.isLoading.get();
}
return true; // Show loading by default
}, },
hasQueryErrors() { hasQueryErrors() {
return false; // No longer using search, so always false return false; // No longer using search, so always false
@ -51,20 +47,6 @@ Template.dueCards.helpers({
errorMessages() { errorMessages() {
return []; // No longer using search, so always empty return []; // No longer using search, so always empty
}, },
cardsCount() {
const component = BlazeComponent.getComponentForElement(this.firstNode);
if (component && component.cardsCount) {
return component.cardsCount();
}
return 0;
},
resultsText() {
const component = BlazeComponent.getComponentForElement(this.firstNode);
if (component && component.resultsText) {
return component.resultsText();
}
return '';
},
}); });
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
@ -96,9 +78,6 @@ class DueCardsComponent extends BlazeComponent {
this._cachedCards = null; this._cachedCards = null;
this._cachedTimestamp = null; this._cachedTimestamp = null;
this.subscriptionHandle = null; this.subscriptionHandle = null;
this.isLoading = new ReactiveVar(true);
this.hasResults = new ReactiveVar(false);
this.searching = new ReactiveVar(false);
// Subscribe to the optimized due cards publication // Subscribe to the optimized due cards publication
this.autorun(() => { this.autorun(() => {
@ -107,24 +86,6 @@ class DueCardsComponent extends BlazeComponent {
this.subscriptionHandle.stop(); this.subscriptionHandle.stop();
} }
this.subscriptionHandle = Meteor.subscribe('dueCards', allUsers); this.subscriptionHandle = Meteor.subscribe('dueCards', allUsers);
// Update loading state based on subscription
this.autorun(() => {
if (this.subscriptionHandle && this.subscriptionHandle.ready()) {
if (process.env.DEBUG === 'true') {
console.log('dueCards: subscription ready, loading data...');
}
this.isLoading.set(false);
const cards = this.dueCardsList();
this.hasResults.set(cards && cards.length > 0);
} else {
if (process.env.DEBUG === 'true') {
console.log('dueCards: subscription not ready, showing loading...');
}
this.isLoading.set(true);
this.hasResults.set(false);
}
});
}); });
} }
@ -145,45 +106,9 @@ class DueCardsComponent extends BlazeComponent {
return this.dueCardsView() === 'board'; return this.dueCardsView() === 'board';
} }
hasResults() {
return this.hasResults.get();
}
cardsCount() {
const cards = this.dueCardsList();
return cards ? cards.length : 0;
}
resultsText() {
const count = this.cardsCount();
if (count === 1) {
return TAPi18n.__('one-card-found');
} else {
// Get the translated text and manually replace %s with the count
const baseText = TAPi18n.__('n-cards-found');
const result = baseText.replace('%s', count);
if (process.env.DEBUG === 'true') {
console.log('dueCards: base text:', baseText, 'count:', count, 'result:', result);
}
return result;
}
}
dueCardsList() { dueCardsList() {
// Check if subscription is ready
if (!this.subscriptionHandle || !this.subscriptionHandle.ready()) {
if (process.env.DEBUG === 'true') {
console.log('dueCards client: subscription not ready');
}
return [];
}
// Use cached results if available to avoid expensive re-sorting // Use cached results if available to avoid expensive re-sorting
if (this._cachedCards && this._cachedTimestamp && (Date.now() - this._cachedTimestamp < 5000)) { if (this._cachedCards && this._cachedTimestamp && (Date.now() - this._cachedTimestamp < 5000)) {
if (process.env.DEBUG === 'true') {
console.log('dueCards client: using cached results,', this._cachedCards.length, 'cards');
}
return this._cachedCards; return this._cachedCards;
} }
@ -194,74 +119,23 @@ class DueCardsComponent extends BlazeComponent {
dueAt: { $exists: true, $nin: [null, ''] } dueAt: { $exists: true, $nin: [null, ''] }
}); });
if (process.env.DEBUG === 'true') {
console.log('dueCards client: found', cards.length, 'cards with due dates');
console.log('dueCards client: cards details:', cards.map(c => ({
id: c._id,
title: c.title,
dueAt: c.dueAt,
boardId: c.boardId,
members: c.members,
assignees: c.assignees,
userId: c.userId
})));
}
// Filter cards based on user view preference // Filter cards based on user view preference
const allUsers = this.dueCardsView() === 'all'; const allUsers = this.dueCardsView() === 'all';
const currentUser = ReactiveCache.getCurrentUser(); const currentUser = ReactiveCache.getCurrentUser();
let filteredCards = cards; let filteredCards = cards;
if (process.env.DEBUG === 'true') {
console.log('dueCards client: current user:', currentUser ? currentUser._id : 'none');
console.log('dueCards client: showing all users:', allUsers);
}
if (!allUsers && currentUser) { if (!allUsers && currentUser) {
filteredCards = cards.filter(card => { filteredCards = cards.filter(card => {
const isMember = card.members && card.members.includes(currentUser._id); return card.members && card.members.includes(currentUser._id) ||
const isAssignee = card.assignees && card.assignees.includes(currentUser._id); card.assignees && card.assignees.includes(currentUser._id) ||
const isAuthor = card.userId === currentUser._id; card.userId === currentUser._id;
const matches = isMember || isAssignee || isAuthor;
if (process.env.DEBUG === 'true' && matches) {
console.log('dueCards client: card matches user:', card.title, { isMember, isAssignee, isAuthor });
}
return matches;
}); });
} }
// 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');
}
// Cache the results for 5 seconds to avoid re-filtering on every render // Cache the results for 5 seconds to avoid re-filtering on every render
this._cachedCards = filteredCards; this._cachedCards = filteredCards;
this._cachedTimestamp = Date.now(); this._cachedTimestamp = Date.now();
// Update reactive variables
this.hasResults.set(filteredCards && filteredCards.length > 0);
this.isLoading.set(false);
return filteredCards; return filteredCards;
} }
} }

View file

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

View file

@ -83,6 +83,10 @@ template(name="header")
i.mobile-icon(class="{{#if mobileMode}}active{{/if}}") 📱 i.mobile-icon(class="{{#if mobileMode}}active{{/if}}") 📱
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}") 🖥️ i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}") 🖥️
// Bookmarks button - desktop opens popup, mobile routes to page
a.board-header-btn.js-open-bookmarks(title="{{_ 'bookmarks'}}")
| 🔖
// Notifications // Notifications
+notifications +notifications
@ -111,9 +115,7 @@ template(name="header")
| 📢 | 📢
+viewer +viewer
| #{announcement} | #{announcement}
a | ❌
.js-close-announcement
| ❌
template(name="offlineWarning") template(name="offlineWarning")
.offline-warning .offline-warning

View file

@ -81,27 +81,6 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; 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 { #content {
position: relative; position: relative;
@ -548,7 +527,7 @@ a:not(.disabled).is-active i.fa {
/* Board canvas */ /* Board canvas */
.board-canvas { .board-canvas {
padding: 0 8px 8px 0; padding: 8px;
overflow-x: auto; overflow-x: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
@ -696,7 +675,7 @@ a:not(.disabled).is-active i.fa {
} }
.board-canvas { .board-canvas {
padding: 0 12px 12px 0; padding: 12px;
} }
#header { #header {
@ -777,7 +756,7 @@ a:not(.disabled).is-active i.fa {
.inline-input { .inline-input {
height: 37px; height: 37px;
margin: 8px 10px 0 0; margin: 8px 10px 0 0;
width: 100px; width: 50px;
} }
.select-authentication { .select-authentication {
width: 100%; width: 100%;
@ -920,40 +899,6 @@ a:not(.disabled).is-active i.fa {
height: 100%; 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 { @-moz-keyframes lds-roller {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);

View file

@ -2,10 +2,8 @@ template(name="main")
html(lang="{{TAPi18n.getLanguage}}") html(lang="{{TAPi18n.getLanguage}}")
head head
title title
meta(name="viewport" content="width=device-width, initial-scale=1, 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(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 //- XXX We should use pathFor in the following `href` to support the case
where the application is deployed with a path prefix, but it seems to be where the application is deployed with a path prefix, but it seems to be
difficult to do that cleanly with Blaze -- at least without adding extra difficult to do that cleanly with Blaze -- at least without adding extra
@ -79,6 +77,7 @@ template(name="defaultLayout")
| {{{afterBodyStart}}} | {{{afterBodyStart}}}
+Template.dynamic(template=content) +Template.dynamic(template=content)
| {{{beforeBodyEnd}}} | {{{beforeBodyEnd}}}
+migrationProgress
+boardConversionProgress +boardConversionProgress
if (Modal.isOpen) if (Modal.isOpen)
#modal #modal

View file

@ -94,24 +94,24 @@
} }
/* Admin edit popups: use full height */ /* Admin edit popups: use full height */
.pop-over[data-popup="editUserPopup"], .pop-over[data-popup="editUser"],
.pop-over[data-popup="editOrgPopup"], .pop-over[data-popup="editOrg"],
.pop-over[data-popup="editTeamPopup"] { .pop-over[data-popup="editTeam"] {
height: calc(100vh - 20px) !important; height: calc(100vh - 20px) !important;
max-height: calc(100vh - 20px) !important; max-height: calc(100vh - 20px) !important;
} }
.pop-over[data-popup="editUserPopup"] .content-wrapper, .pop-over[data-popup="editUser"] .content-wrapper,
.pop-over[data-popup="editOrgPopup"] .content-wrapper, .pop-over[data-popup="editOrg"] .content-wrapper,
.pop-over[data-popup="editTeamPopup"] .content-wrapper { .pop-over[data-popup="editTeam"] .content-wrapper {
max-height: calc(100vh - 80px) !important; /* Subtract header height */ max-height: calc(100vh - 80px) !important; /* Subtract header height */
height: calc(100vh - 80px) !important; height: calc(100vh - 80px) !important;
overflow-y: auto !important; overflow-y: auto !important;
} }
.pop-over[data-popup="editUserPopup"] .content-container, .pop-over[data-popup="editUser"] .content-container,
.pop-over[data-popup="editOrgPopup"] .content-container, .pop-over[data-popup="editOrg"] .content-container,
.pop-over[data-popup="editTeamPopup"] .content-container { .pop-over[data-popup="editTeam"] .content-container {
max-height: calc(100vh - 80px) !important; /* Subtract header height */ max-height: calc(100vh - 80px) !important; /* Subtract header height */
height: calc(100vh - 80px) !important; height: calc(100vh - 80px) !important;
} }
@ -123,7 +123,7 @@
} }
/* Specific styling for language popup list */ /* 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; max-height: none;
overflow: visible; overflow: visible;
height: auto; height: auto;
@ -131,69 +131,46 @@
} }
/* Ensure content div in language popup contains all items */ /* Ensure content div in language popup contains all items */
.pop-over[data-popup="changeLanguagePopup"] .content { .pop-over[data-popup="changeLanguage"] .content {
height: auto; height: auto;
/* Remove forced min-height to avoid top gap */ min-height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
/* Ensure hidden stack pages truly take no space */ /* Allow dynamic height for Change Language popup */
.pop-over[data-popup="changeLanguagePopup"] .content.no-height { .pop-over[data-popup="changeLanguage"] .content-wrapper {
min-height: 0 !important; max-height: inherit; /* Use dynamic height from JavaScript */
height: 0 !important; }
padding: 0 !important;
margin: 0 !important; .pop-over[data-popup="changeLanguage"] .content-container {
visibility: hidden !important; max-height: inherit; /* Use dynamic height from JavaScript */
} }
/* Make language popup extend to bottom of browser window */ /* Make language popup extend to bottom of browser window */
.pop-over[data-popup="changeLanguagePopup"] { .pop-over[data-popup="changeLanguage"] {
position: fixed !important; height: calc(100vh - 30px);
bottom: 0 !important; min-height: 300px;
top: auto !important; /* Adjust positioning to move popup 30px higher */
left: auto !important; transform: translateY(-30px);
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;
} }
/* Allow dynamic height for Change Language popup */ .pop-over[data-popup="changeLanguage"] .content-wrapper {
.pop-over[data-popup="changeLanguagePopup"] .header { height: calc(100% - 50px); /* Subtract header height more precisely */
flex-shrink: 0 !important; min-height: 250px;
height: auto !important; overflow-y: auto;
max-height: none; /* Remove any max-height constraints */
display: flex;
flex-direction: column;
} }
.pop-over[data-popup="changeLanguagePopup"] .content-wrapper { .pop-over[data-popup="changeLanguage"] .content-container {
flex: 1 !important; height: auto; /* Let content determine height */
overflow-y: auto !important; min-height: 250px;
overflow-x: hidden !important; max-height: none; /* Remove any max-height constraints */
min-height: 0 !important; flex: 1;
max-height: none !important; display: flex;
height: auto !important; flex-direction: column;
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;
} }
/* Date popup sizing for native HTML inputs */ /* Date popup sizing for native HTML inputs */
@ -316,8 +293,6 @@
overflow-y: auto !important; overflow-y: auto !important;
} }
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date button, .pop-over[data-popup="editCardReceivedDatePopup"] .edit-date button,
.pop-over[data-popup="editCardStartDatePopup"] .edit-date button, .pop-over[data-popup="editCardStartDatePopup"] .edit-date button,
.pop-over[data-popup="editCardDueDatePopup"] .edit-date button, .pop-over[data-popup="editCardDueDatePopup"] .edit-date button,
@ -412,6 +387,9 @@
margin: 0; margin: 0;
visibility: hidden; visibility: hidden;
} }
.pop-over .quiet {
/* padding: 6px 6px 4px;*/
}
.pop-over.search-over { .pop-over.search-over {
background: #f0f0f0; background: #f0f0f0;
min-height: 14vh; min-height: 14vh;
@ -538,7 +516,6 @@
position: absolute; position: absolute;
top: 6px; top: 6px;
right: 12px; right: 12px;
color: #3cb500;
} }
.pop-over-list .pop-over-list.checkable li.active a { .pop-over-list .pop-over-list.checkable li.active a {
padding-right: 28px; padding-right: 28px;
@ -546,10 +523,6 @@
.pop-over-list .pop-over-list.checkable li.active a .fa-check { .pop-over-list .pop-over-list.checkable li.active a .fa-check {
display: block; 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 { .pop-over.miniprofile .header {
border-bottom-color: transparent; border-bottom-color: transparent;
height: 30px; height: 30px;
@ -595,10 +568,6 @@ body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
overflow: hidden; overflow: hidden;
margin-top: 0px; margin-top: 0px;
border: 0px solid #dbdbdb; 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 { .pop-over .header {
color: #fff; color: #fff;
@ -683,23 +652,3 @@ body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
transform: none !important; 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="{{#unless title}}miniprofile{{/unless}}"
class=currentBoard.colorClass class=currentBoard.colorClass
class="{{#unless title}}no-title{{/unless}}" 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}}") style="left:{{offset.left}}px; top:{{offset.top}}px;{{#if offset.maxHeight}} max-height:{{offset.maxHeight}}px;{{/if}}")
.header .header
a.back-btn.js-back-view(class="{{#unless hasPopupParent}}is-hidden{{/unless}}") a.back-btn.js-back-view(class="{{#unless hasPopupParent}}is-hidden{{/unless}}")

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

@ -0,0 +1,372 @@
/* Migration Progress Styles */
.migration-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
z-index: 10000;
display: none;
align-items: center;
justify-content: center;
overflow-y: auto;
}
.migration-overlay.active {
display: flex;
}
.migration-modal {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
max-width: 800px;
width: 95%;
max-height: 90vh;
overflow: hidden;
animation: slideInScale 0.4s ease-out;
margin: 20px;
}
@keyframes slideInScale {
from {
opacity: 0;
transform: translateY(-30px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.migration-header {
padding: 24px 32px 20px;
border-bottom: 2px solid #e0e0e0;
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.migration-header h3 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.migration-header h3 i {
margin-right: 12px;
color: #FFD700;
}
.migration-header p {
margin: 0;
font-size: 16px;
opacity: 0.9;
}
.migration-content {
padding: 24px 32px;
max-height: 60vh;
overflow-y: auto;
}
.migration-overview {
margin-bottom: 32px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.overall-progress {
margin-bottom: 20px;
}
.progress-bar {
width: 100%;
height: 12px;
background-color: #e0e0e0;
border-radius: 6px;
overflow: hidden;
margin-bottom: 8px;
position: relative;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 6px;
transition: width 0.3s ease;
position: relative;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.4),
transparent
);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.progress-text {
text-align: center;
font-weight: 700;
color: #667eea;
font-size: 18px;
}
.progress-label {
text-align: center;
color: #666;
font-size: 14px;
margin-top: 4px;
}
.current-step {
text-align: center;
color: #333;
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
}
.current-step i {
margin-right: 8px;
color: #667eea;
}
.estimated-time {
text-align: center;
color: #666;
font-size: 14px;
background-color: #fff3cd;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #ffeaa7;
}
.estimated-time i {
margin-right: 6px;
color: #f39c12;
}
.migration-steps {
margin-bottom: 24px;
}
.migration-steps h4 {
margin: 0 0 16px 0;
color: #333;
font-size: 18px;
font-weight: 600;
}
.steps-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.migration-step {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
transition: all 0.3s ease;
}
.migration-step:last-child {
border-bottom: none;
}
.migration-step.completed {
background-color: #d4edda;
border-left: 4px solid #28a745;
}
.migration-step.current {
background-color: #cce7ff;
border-left: 4px solid #667eea;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(102, 126, 234, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
}
}
.step-header {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.step-icon {
margin-right: 12px;
font-size: 18px;
width: 24px;
text-align: center;
}
.step-icon i.fa-check-circle {
color: #28a745;
}
.step-icon i.fa-cog.fa-spin {
color: #667eea;
}
.step-icon i.fa-circle-o {
color: #ccc;
}
.step-info {
flex: 1;
}
.step-name {
font-weight: 600;
color: #333;
font-size: 14px;
margin-bottom: 2px;
}
.step-description {
color: #666;
font-size: 12px;
line-height: 1.3;
}
.step-progress {
text-align: right;
min-width: 40px;
}
.step-progress .progress-text {
font-size: 12px;
font-weight: 600;
}
.step-progress-bar {
width: 100%;
height: 4px;
background-color: #e0e0e0;
border-radius: 2px;
overflow: hidden;
margin-top: 8px;
}
.step-progress-bar .progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 2px;
transition: width 0.3s ease;
}
.migration-status {
text-align: center;
color: #333;
font-size: 16px;
background-color: #e3f2fd;
padding: 12px 16px;
border-radius: 6px;
border: 1px solid #bbdefb;
margin-bottom: 16px;
}
.migration-status i {
margin-right: 8px;
color: #2196f3;
}
.migration-footer {
padding: 16px 32px 24px;
border-top: 1px solid #e0e0e0;
background-color: #f8f9fa;
}
.migration-info {
text-align: center;
color: #666;
font-size: 13px;
line-height: 1.4;
margin-bottom: 8px;
}
.migration-info i {
margin-right: 6px;
color: #667eea;
}
.migration-warning {
text-align: center;
color: #856404;
font-size: 12px;
line-height: 1.3;
background-color: #fff3cd;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #ffeaa7;
}
.migration-warning i {
margin-right: 6px;
color: #f39c12;
}
/* Responsive design */
@media (max-width: 768px) {
.migration-modal {
width: 98%;
margin: 10px;
}
.migration-header,
.migration-content,
.migration-footer {
padding-left: 16px;
padding-right: 16px;
}
.migration-header h3 {
font-size: 20px;
}
.step-header {
flex-direction: column;
align-items: flex-start;
}
.step-progress {
text-align: left;
margin-top: 8px;
}
.steps-list {
max-height: 200px;
}
}

View file

@ -0,0 +1,63 @@
template(name="migrationProgress")
.migration-overlay(class="{{#if isMigrating}}active{{/if}}")
.migration-modal
.migration-header
h3
| 🗄️
| {{_ 'database-migration'}}
p {{_ 'database-migration-description'}}
.migration-content
.migration-overview
.overall-progress
.progress-bar
.progress-fill(style="width: {{migrationProgress}}%")
.progress-text {{migrationProgress}}%
.progress-label {{_ 'overall-progress'}}
.current-step
| ⚙️
| {{migrationCurrentStep}}
.estimated-time(style="{{#unless migrationEstimatedTime}}display: none;{{/unless}}")
| ⏰
| {{_ 'estimated-time-remaining'}}: {{migrationEstimatedTime}}
.migration-steps
h4 {{_ 'migration-steps'}}
.steps-list
each migrationSteps
.migration-step(class="{{#if completed}}completed{{/if}}" class="{{#if isCurrentStep}}current{{/if}}")
.step-header
.step-icon
if completed
| ✅
else if isCurrentStep
| ⚙️
else
| ⭕
.step-info
.step-name {{name}}
.step-description {{description}}
.step-progress
if completed
.progress-text 100%
else if isCurrentStep
.progress-text {{progress}}%
else
.progress-text 0%
if isCurrentStep
.step-progress-bar
.progress-fill(style="width: {{progress}}%")
.migration-status
|
| {{migrationStatus}}
.migration-footer
.migration-info
| 💡
| {{_ 'migration-info-text'}}
.migration-warning
| ⚠️
| {{_ 'migration-warning-text'}}

View file

@ -0,0 +1,54 @@
import { Template } from 'meteor/templating';
import {
migrationManager,
isMigrating,
migrationProgress,
migrationStatus,
migrationCurrentStep,
migrationEstimatedTime,
migrationSteps
} from '/client/lib/migrationManager';
Template.migrationProgress.helpers({
isMigrating() {
return isMigrating.get();
},
migrationProgress() {
return migrationProgress.get();
},
migrationStatus() {
return migrationStatus.get();
},
migrationCurrentStep() {
return migrationCurrentStep.get();
},
migrationEstimatedTime() {
return migrationEstimatedTime.get();
},
migrationSteps() {
const steps = migrationSteps.get();
const currentStep = migrationCurrentStep.get();
return steps.map(step => ({
...step,
isCurrentStep: step.name === currentStep
}));
}
});
Template.migrationProgress.onCreated(function() {
// Subscribe to migration state changes
this.autorun(() => {
isMigrating.get();
migrationProgress.get();
migrationStatus.get();
migrationCurrentStep.get();
migrationEstimatedTime.get();
migrationSteps.get();
});
});

View file

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

View file

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

View file

@ -1,8 +1,8 @@
template(name='notificationIcon') template(name='notificationIcon')
if($in activityType 'deleteAttachment' 'addAttachment') if($in activityType 'deleteAttachment' 'addAttachment')
span.activity-type(title="attachment") 📎 i.fa.fa-paperclip.activity-type(title="attachment")
else if($in activityType 'createBoard' 'importBoard') 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') else if($in activityType 'createCard' 'importCard' 'moveCard')
+cardNotificationIcon +cardNotificationIcon
@ -19,17 +19,17 @@ template(name='notificationIcon')
//- DRY and consistant //- DRY and consistant
else if($in activityType 'checkedItem' 'uncheckedItem' 'addChecklistItem' 'removedChecklistItem') 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') 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') 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') 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') 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') 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') else if($in activityType 'createList' 'removeList' 'archivedList')
+listNotificationIcon +listNotificationIcon
@ -41,17 +41,17 @@ template(name='notificationIcon')
//- elswhere in the app we use fa-trello to indicate lists... //- elswhere in the app we use fa-trello to indicate lists...
//- i personally like fa-columns a bit better //- i personally like fa-columns a bit better
else if($in activityType 'unjoinMember' 'addBoardMember' 'joinMember' 'removeBoardMember') 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') else if($in activityType 'createSwimlane' 'archivedSwimlane')
span.activity-type(title="swimlane") 🧭 i.fa.fa-th-large.activity-type(title="swimlane")
else 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') template(name='cardNotificationIcon')
span.activity-type(title="card") 🗒️ i.fa.fa-clone.activity-type(title="card")
template(name='checklistNotificationIcon') template(name='checklistNotificationIcon')
span.activity-type(title="checklist") 📝 i.fa.fa-list.activity-type(title="checklist")
template(name='listNotificationIcon') 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 { section#notifications-drawer .remove-read:hover {
color: #eb4646 !important; color: #eb4646 !important;
} }
section#notifications-drawer .remove-read:hover i.fa {
color: inherit;
}
section#notifications-drawer ul.notifications { section#notifications-drawer ul.notifications {
display: block; display: block;
padding: 0px 16px 0px 16px; padding: 0px 16px 0px 16px;

View file

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

View file

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

View file

@ -1,11 +1,7 @@
import { ReactiveCache } from '/imports/reactiveCache'; import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
onCreated() { onCreated() {},
// Ensure boards are available for action dropdowns
this.subscribe('boards');
},
boards() { boards() {
const ret = ReactiveCache.getBoards( const ret = ReactiveCache.getBoards(
@ -23,16 +19,6 @@ BlazeComponent.extendComponent({
return ret; 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() { events() {
return [ return [
{ {

View file

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

View file

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

View file

@ -5,15 +5,6 @@ BlazeComponent.extendComponent({
this.currentActions = new ReactiveVar('board'); 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() { setBoardActions() {
this.currentActions.set('board'); this.currentActions.set('board');
$('.js-set-card-actions').removeClass('active'); $('.js-set-card-actions').removeClass('active');

View file

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

View file

@ -7,15 +7,6 @@ BlazeComponent.extendComponent({
this.showChecklistTrigger = new ReactiveVar(false); 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() { setBoardTriggers() {
this.showBoardTrigger.set(true); this.showBoardTrigger.set(true);
this.showCardTrigger.set(false); this.showCardTrigger.set(false);

View file

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

View file

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

View file

@ -1,301 +0,0 @@
/* Migration Progress Styles */
.migration-progress-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(2px);
}
.migration-progress-modal {
background: white;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow: hidden;
animation: migrationModalSlideIn 0.3s ease-out;
}
@keyframes migrationModalSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.migration-progress-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.migration-progress-title {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.migration-progress-close {
cursor: pointer;
font-size: 16px;
opacity: 0.8;
transition: opacity 0.2s ease;
}
.migration-progress-close:hover {
opacity: 1;
}
.migration-progress-content {
padding: 30px;
}
.migration-progress-overall {
margin-bottom: 25px;
}
.migration-progress-overall-label {
font-weight: 600;
color: #333;
margin-bottom: 8px;
font-size: 14px;
}
.migration-progress-overall-bar {
background: #e9ecef;
border-radius: 10px;
height: 12px;
overflow: hidden;
margin-bottom: 5px;
}
.migration-progress-overall-fill {
background: linear-gradient(90deg, #28a745, #20c997);
height: 100%;
border-radius: 10px;
transition: width 0.3s ease;
position: relative;
}
.migration-progress-overall-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
animation: migrationProgressShimmer 2s infinite;
}
@keyframes migrationProgressShimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.migration-progress-overall-percentage {
text-align: right;
font-size: 12px;
color: #666;
font-weight: 600;
}
.migration-progress-current-step {
margin-bottom: 25px;
}
.migration-progress-step-label {
font-weight: 600;
color: #333;
margin-bottom: 8px;
font-size: 14px;
}
.migration-progress-step-bar {
background: #e9ecef;
border-radius: 8px;
height: 8px;
overflow: hidden;
margin-bottom: 5px;
}
.migration-progress-step-fill {
background: linear-gradient(90deg, #007bff, #0056b3);
height: 100%;
border-radius: 8px;
transition: width 0.3s ease;
}
.migration-progress-step-percentage {
text-align: right;
font-size: 12px;
color: #666;
font-weight: 600;
}
.migration-progress-status {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #007bff;
}
.migration-progress-status-label {
font-weight: 600;
color: #333;
margin-bottom: 5px;
font-size: 13px;
}
.migration-progress-status-text {
color: #555;
font-size: 14px;
line-height: 1.4;
}
.migration-progress-details {
margin-bottom: 20px;
padding: 12px;
background: #e3f2fd;
border-radius: 6px;
border-left: 4px solid #2196f3;
}
.migration-progress-details-label {
font-weight: 600;
color: #1976d2;
margin-bottom: 5px;
font-size: 13px;
}
.migration-progress-details-text {
color: #1565c0;
font-size: 13px;
line-height: 1.4;
}
.migration-progress-footer {
padding: 20px 30px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
}
.migration-progress-note {
text-align: center;
color: #666;
font-size: 13px;
font-style: italic;
}
/* Responsive design */
@media (max-width: 600px) {
.migration-progress-modal {
width: 95%;
margin: 20px;
}
.migration-progress-content {
padding: 20px;
}
.migration-progress-header {
padding: 15px;
}
.migration-progress-title {
font-size: 16px;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.migration-progress-modal {
background: #2d3748;
color: #e2e8f0;
}
.migration-progress-overall-label,
.migration-progress-step-label,
.migration-progress-status-label {
color: #e2e8f0;
}
.migration-progress-status {
background: #4a5568;
border-left-color: #63b3ed;
}
.migration-progress-status-text {
color: #cbd5e0;
}
.migration-progress-details {
background: #2b6cb0;
border-left-color: #4299e1;
}
.migration-progress-details-label {
color: #bee3f8;
}
.migration-progress-details-text {
color: #90cdf4;
}
.migration-progress-footer {
background: #4a5568;
border-top-color: #718096;
}
.migration-progress-note {
color: #a0aec0;
}
}
/* 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

@ -1,43 +0,0 @@
template(name="migrationProgress")
if isMigrating
.migration-progress-overlay
.migration-progress-modal
.migration-progress-header
h3.migration-progress-title
| 🔄 {{_ 'migration-progress-title'}}
.migration-progress-close.js-close-migration-progress
| ❌
.migration-progress-content
.migration-progress-overall
.migration-progress-overall-label
| {{_ 'migration-progress-overall'}}: {{currentStep}} {{_ 'of'}} {{totalSteps}} {{_ 'steps'}}
.migration-progress-overall-bar
.migration-progress-overall-fill(style="{{progressBarStyle}}")
.migration-progress-overall-percentage
| {{overallProgress}}%
.migration-progress-current-step
.migration-progress-step-label
| {{_ 'migration-progress-current-step'}}: {{stepNameFormatted}}
.migration-progress-step-bar
.migration-progress-step-fill(style="{{stepProgressBarStyle}}")
.migration-progress-step-percentage
| {{stepProgress}}%
.migration-progress-status
.migration-progress-status-label
| {{_ 'migration-progress-status'}}:
.migration-progress-status-text
| {{stepStatus}}
if stepDetailsFormatted
.migration-progress-details
.migration-progress-details-label
| {{_ 'migration-progress-details'}}:
.migration-progress-details-text
| {{stepDetailsFormatted}}
.migration-progress-footer
.migration-progress-note
| {{_ 'migration-progress-note'}}

View file

@ -1,212 +0,0 @@
/**
* Migration Progress Component
* Displays detailed progress for comprehensive board migration
*/
import { ReactiveVar } from 'meteor/reactive-var';
import { ReactiveCache } from '/imports/reactiveCache';
// Reactive variables for migration progress
export const migrationProgress = new ReactiveVar(0);
export const migrationStatus = new ReactiveVar('');
export const migrationStepName = new ReactiveVar('');
export const migrationStepProgress = new ReactiveVar(0);
export const migrationStepStatus = new ReactiveVar('');
export const migrationStepDetails = new ReactiveVar(null);
export const migrationCurrentStep = new ReactiveVar(0);
export const migrationTotalSteps = new ReactiveVar(0);
export const isMigrating = new ReactiveVar(false);
class MigrationProgressManager {
constructor() {
this.progressHistory = [];
}
/**
* Update migration progress
*/
updateProgress(progressData) {
const {
overallProgress,
currentStep,
totalSteps,
stepName,
stepProgress,
stepStatus,
stepDetails,
boardId
} = progressData;
// Update reactive variables
migrationProgress.set(overallProgress);
migrationCurrentStep.set(currentStep);
migrationTotalSteps.set(totalSteps);
migrationStepName.set(stepName);
migrationStepProgress.set(stepProgress);
migrationStepStatus.set(stepStatus);
migrationStepDetails.set(stepDetails);
// Store in history
this.progressHistory.push({
timestamp: new Date(),
...progressData
});
// Update overall status
migrationStatus.set(`${stepName}: ${stepStatus}`);
}
/**
* Start migration
*/
startMigration() {
isMigrating.set(true);
migrationProgress.set(0);
migrationStatus.set('Starting migration...');
migrationStepName.set('');
migrationStepProgress.set(0);
migrationStepStatus.set('');
migrationStepDetails.set(null);
migrationCurrentStep.set(0);
migrationTotalSteps.set(0);
this.progressHistory = [];
}
/**
* Complete migration
*/
completeMigration() {
isMigrating.set(false);
migrationProgress.set(100);
migrationStatus.set('Migration completed successfully!');
// Clear step details after a delay
setTimeout(() => {
migrationStepName.set('');
migrationStepProgress.set(0);
migrationStepStatus.set('');
migrationStepDetails.set(null);
migrationCurrentStep.set(0);
migrationTotalSteps.set(0);
}, 3000);
}
/**
* Fail migration
*/
failMigration(error) {
isMigrating.set(false);
migrationStatus.set(`Migration failed: ${error.message || error}`);
migrationStepStatus.set('Error occurred');
}
/**
* Get progress history
*/
getProgressHistory() {
return this.progressHistory;
}
/**
* Clear progress
*/
clearProgress() {
isMigrating.set(false);
migrationProgress.set(0);
migrationStatus.set('');
migrationStepName.set('');
migrationStepProgress.set(0);
migrationStepStatus.set('');
migrationStepDetails.set(null);
migrationCurrentStep.set(0);
migrationTotalSteps.set(0);
this.progressHistory = [];
}
}
// Export singleton instance
export const migrationProgressManager = new MigrationProgressManager();
// Template helpers
Template.migrationProgress.helpers({
isMigrating() {
return isMigrating.get();
},
overallProgress() {
return migrationProgress.get();
},
overallStatus() {
return migrationStatus.get();
},
currentStep() {
return migrationCurrentStep.get();
},
totalSteps() {
return migrationTotalSteps.get();
},
stepName() {
return migrationStepName.get();
},
stepProgress() {
return migrationStepProgress.get();
},
stepStatus() {
return migrationStepStatus.get();
},
stepDetails() {
return migrationStepDetails.get();
},
progressBarStyle() {
const progress = migrationProgress.get();
return `width: ${progress}%`;
},
stepProgressBarStyle() {
const progress = migrationStepProgress.get();
return `width: ${progress}%`;
},
stepNameFormatted() {
const stepName = migrationStepName.get();
if (!stepName) return '';
// Convert snake_case to Title Case
return stepName
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
},
stepDetailsFormatted() {
const details = migrationStepDetails.get();
if (!details) return '';
const formatted = [];
for (const [key, value] of Object.entries(details)) {
const formattedKey = key
.split(/(?=[A-Z])/)
.join(' ')
.toLowerCase()
.replace(/^\w/, c => c.toUpperCase());
formatted.push(`${formattedKey}: ${value}`);
}
return formatted.join(', ');
}
});
// Template events
Template.migrationProgress.events({
'click .js-close-migration-progress'() {
migrationProgressManager.clearProgress();
}
});

View file

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

View file

@ -137,13 +137,8 @@
padding: 0.5rem 0.5rem; padding: 0.5rem 0.5rem;
} }
.setting-content .content-body .main-body ul li a .is-checked { .setting-content .content-body .main-body ul li a .is-checked {
border-bottom: 2px solid #3cb500; border-bottom: 2px solid #2980b9;
border-right: 2px solid #3cb500; border-right: 2px solid #2980b9;
}
/* 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;
} }
.setting-content .content-body .main-body ul li a span { .setting-content .content-body .main-body ul li a span {
padding: 0 0.5rem; padding: 0 0.5rem;

View file

@ -6,87 +6,87 @@ template(name="setting")
.content-title.ext-box .content-title.ext-box
if isGeneralSetting if isGeneralSetting
span span
span.emoji-icon 🔑 | 🔑
| {{_ 'registration'}} | {{_ 'registration'}}
else if isEmailSetting else if isEmailSetting
span span
span.emoji-icon ✉️ | ✉️
| {{_ 'email'}} | {{_ 'email'}}
else if isAccountSetting else if isAccountSetting
span span
span.emoji-icon 👥 | 👥
| {{_ 'accounts'}} | {{_ 'accounts'}}
else if isTableVisibilityModeSetting else if isTableVisibilityModeSetting
span span
span.emoji-icon 👁️ | 👁️
| {{_ 'tableVisibilityMode'}} | {{_ 'tableVisibilityMode'}}
else if isAnnouncementSetting else if isAnnouncementSetting
span span
span.emoji-icon 📢 | 📢
| {{_ 'admin-announcement'}} | {{_ 'admin-announcement'}}
else if isAccessibilitySetting else if isAccessibilitySetting
span span
span.emoji-icon |
| {{_ 'accessibility'}} | {{_ 'accessibility'}}
else if isLayoutSetting else if isLayoutSetting
span span
span.emoji-icon 🔗 | 🔗
| {{_ 'layout'}} | {{_ 'layout'}}
else if isWebhookSetting else if isWebhookSetting
span span
span.emoji-icon 🌐 | 🌐
| {{_ 'global-webhook'}} | {{_ 'global-webhook'}}
else if isAttachmentSettings else if isAttachmentSettings
span span
span.emoji-iconpan.emoji-icon 📎 | 📎
| {{_ 'attachments'}} | {{_ 'attachments'}}
else if isCronSettings else if isCronSettings
span span
span.emoji-icon |
| {{_ 'cron'}} | {{_ 'cron'}}
.content-body .content-body
.side-menu .side-menu
ul ul
li(class="{{#if isGeneralSetting}}active{{/if}}") li(class="{{#if isGeneralSetting}}active{{/if}}")
a.js-setting-menu(data-id="registration-setting") a.js-setting-menu(data-id="registration-setting")
span.emoji-icon 🔑 | 🔑
| {{_ 'registration'}} | {{_ 'registration'}}
unless isSandstorm unless isSandstorm
li(class="{{#if isEmailSetting}}active{{/if}}") li(class="{{#if isEmailSetting}}active{{/if}}")
a.js-setting-menu(data-id="email-setting") a.js-setting-menu(data-id="email-setting")
span.emoji-icon ✉️ | ✉️
| {{_ 'email'}} | {{_ 'email'}}
li(class="{{#if isAccountSetting}}active{{/if}}") li(class="{{#if isAccountSetting}}active{{/if}}")
a.js-setting-menu(data-id="account-setting") a.js-setting-menu(data-id="account-setting")
span.emoji-icon 👥 | 👥
| {{_ 'accounts'}} | {{_ 'accounts'}}
li(class="{{#if isTableVisibilityModeSetting}}active{{/if}}") li(class="{{#if isTableVisibilityModeSetting}}active{{/if}}")
a.js-setting-menu(data-id="tableVisibilityMode-setting") a.js-setting-menu(data-id="tableVisibilityMode-setting")
span.emoji-icon 👁️ | 👁️
| {{_ 'tableVisibilityMode'}} | {{_ 'tableVisibilityMode'}}
li(class="{{#if isAnnouncementSetting}}active{{/if}}") li(class="{{#if isAnnouncementSetting}}active{{/if}}")
a.js-setting-menu(data-id="announcement-setting") a.js-setting-menu(data-id="announcement-setting")
span.emoji-icon 📢 | 📢
| {{_ 'admin-announcement'}} | {{_ 'admin-announcement'}}
li(class="{{#if isAccessibilitySetting}}active{{/if}}") li(class="{{#if isAccessibilitySetting}}active{{/if}}")
a.js-setting-menu(data-id="accessibility-setting") a.js-setting-menu(data-id="accessibility-setting")
span.emoji-icon |
| {{_ 'accessibility'}} | {{_ 'accessibility'}}
li(class="{{#if isLayoutSetting}}active{{/if}}") li(class="{{#if isLayoutSetting}}active{{/if}}")
a.js-setting-menu(data-id="layout-setting") a.js-setting-menu(data-id="layout-setting")
span.emoji-icon 🔗 | 🔗
| {{_ 'layout'}} | {{_ 'layout'}}
li(class="{{#if isWebhookSetting}}active{{/if}}") li(class="{{#if isWebhookSetting}}active{{/if}}")
a.js-setting-menu(data-id="webhook-setting") a.js-setting-menu(data-id="webhook-setting")
span.emoji-icon 🌐 | 🌐
| {{_ 'global-webhook'}} | {{_ 'global-webhook'}}
li(class="{{#if isAttachmentSettings}}active{{/if}}") li(class="{{#if isAttachmentSettings}}active{{/if}}")
a.js-setting-menu(data-id="attachment-settings") a.js-setting-menu(data-id="attachment-settings")
span.emoji-icon 📎 | 📎
| {{_ 'attachments'}} | {{_ 'attachments'}}
li(class="{{#if isCronSettings}}active{{/if}}") li(class="{{#if isCronSettings}}active{{/if}}")
a.js-setting-menu(data-id="cron-settings") a.js-setting-menu(data-id="cron-settings")
span.emoji-icon |
| {{_ 'cron'}} | {{_ 'cron'}}
.main-body .main-body
if isLoading if isLoading
@ -170,10 +170,7 @@ template(name="setting")
label {{_ 'migration-status'}} label {{_ 'migration-status'}}
.status-indicator .status-indicator
span.status-label {{_ 'status'}}: span.status-label {{_ 'status'}}:
span.status-value {{#if isMigrating}}{{migrationStatus}}{{else}}{{_ 'idle'}}{{/if}} span.status-value {{migrationStatus}}
.current-step(class="{{#unless migrationCurrentStep}}hide{{/unless}}")
span.step-label {{_ 'current-step'}}:
span.step-value {{migrationCurrentStep}}
.progress-section .progress-section
.progress .progress
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100") .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'}} | {{migrationProgress}}% {{_ 'complete'}}
.form-group .form-group
button.js-start-all-migrations.btn.btn-primary {{#if isMigrating}}disabled{{/if}} {{_ 'start-all-migrations'}} button.js-start-all-migrations.btn.btn-primary {{_ 'start-all-migrations'}}
button.js-pause-all-migrations.btn.btn-warning {{#unless isMigrating}}disabled{{/unless}} {{_ 'pause-all-migrations'}} button.js-pause-all-migrations.btn.btn-warning {{_ 'pause-all-migrations'}}
button.js-stop-all-migrations.btn.btn-danger {{#unless isMigrating}}disabled{{/unless}} {{_ 'stop-all-migrations'}} button.js-stop-all-migrations.btn.btn-danger {{_ 'stop-all-migrations'}}
li
h3 {{_ 'migration-steps'}}
p Migration steps section temporarily removed
li li
h3 {{_ 'board-operations'}} h3 {{_ 'board-operations'}}
@ -207,7 +200,7 @@ template(name="setting")
.job-info .job-info
.job-name {{name}} .job-name {{name}}
.job-schedule {{schedule}} .job-schedule {{schedule}}
.job-status {{status}} .job-description {{description}}
.job-actions .job-actions
button.js-pause-job.btn.btn-sm.btn-warning(data-job-id="{{_id}}") {{_ 'pause'}} 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'}} button.js-delete-job.btn.btn-sm.btn-danger(data-job-id="{{_id}}") {{_ 'delete'}}
@ -281,7 +274,7 @@ template(name='email')
// li.smtp-form // li.smtp-form
// .title {{_ 'smtp-username'}} // .title {{_ 'smtp-username'}}
// .form-group // .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 // li.smtp-form
// .title {{_ 'smtp-password'}} // .title {{_ 'smtp-password'}}
// .form-group // .form-group
@ -382,29 +375,6 @@ template(name='layoutSettings')
ul#layout-setting.setting-detail ul#layout-setting.setting-detail
li li
button.js-all-boards-hide-activities.primary {{_ 'hide-activities-of-all-boards'}} 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 li.layout-form
.title {{_ 'oidc-button-text'}} .title {{_ 'oidc-button-text'}}
.form-group .form-group

View file

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

View file

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

View file

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

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