mirror of
https://github.com/wekan/wekan.git
synced 2026-01-12 20:48:50 +01:00
Compare commits
No commits in common. "main" and "v8.17" have entirely different histories.
316 changed files with 5909 additions and 26697 deletions
2
.github/workflows/depsreview.yaml
vendored
2
.github/workflows/depsreview.yaml
vendored
|
|
@ -9,6 +9,6 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
|
|
|
|||
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
|
|
@ -32,7 +32,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
|
|
@ -48,7 +48,7 @@ jobs:
|
|||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
|
|
|
|||
2
.github/workflows/dockerimage.yml
vendored
2
.github/workflows/dockerimage.yml
vendored
|
|
@ -15,6 +15,6 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
- name: Build the Docker image
|
||||
run: docker build . --file Dockerfile --tag wekan:$(date +%s)
|
||||
|
|
|
|||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
|
|
|||
20
.github/workflows/test_suite.yml
vendored
20
.github/workflows/test_suite.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v6
|
||||
# uses: actions/checkout@v5
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
# needs: [lintcode]
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v6
|
||||
# uses: actions/checkout@v5
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
|
|
@ -65,7 +65,7 @@ jobs:
|
|||
# needs: [lintcode,lintstyle]
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v6
|
||||
# uses: actions/checkout@v5
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
|
|
@ -90,12 +90,12 @@ jobs:
|
|||
|
||||
# CHECKOUTS
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# CACHING
|
||||
- name: Install Meteor
|
||||
id: cache-meteor-install
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.meteor
|
||||
key: v1-meteor-${{ hashFiles('.meteor/versions') }}
|
||||
|
|
@ -104,7 +104,7 @@ jobs:
|
|||
|
||||
- name: Cache NPM dependencies
|
||||
id: cache-meteor-npm
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: v1-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
|
@ -113,7 +113,7 @@ jobs:
|
|||
|
||||
- name: Cache Meteor build
|
||||
id: cache-meteor-build
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.meteor/local/resolver-result-cache.json
|
||||
|
|
@ -136,7 +136,7 @@ jobs:
|
|||
run: sh ./test-wekan.sh -cv
|
||||
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: coverage-folder
|
||||
path: .coverage/
|
||||
|
|
@ -147,10 +147,10 @@ jobs:
|
|||
needs: [tests]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Download coverage
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: coverage-folder
|
||||
path: .coverage/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = te_IN: te-IN, es_AR: es-AR, es_419: es-LA, es_TX: es-TX, he_IL: he-IL, zh_CN: zh-CN, ar_EG: ar-EG, cs_CZ: cs-CZ, fa_IR: fa-IR, ms_MY: ms-MY, nl_NL: nl-NL, de_CH: de-CH, en_IT: en-IT, uz_UZ: uz-UZ, fr_CH: fr-CH, hi_IN: hi-IN, et_EE: et-EE, es_PE: es-PE, es_MX: es-MX, gl_ES: gl-ES, mn_MN: mn, zh_TW: zh-TW, ast_ES: ast-ES, es_CL: es-CL, ja_JP: ja, lv_LV: lv, ro_RO: ro-RO, az_AZ: az-AZ, cy_GB: cy-GB, gu_IN: gu-IN, pl_PL: pl-PL, vep: ve-PP, en_BR: en-BR, en@ysv: en-YS, hu_HU: hu, ko_KR: ko-KR, pt_BR: pt-BR, zh_HK: zh-HK, zu_ZA: zu-ZA, en_MY: en-MY, ja-Hira: ja-HI, fi_FI: fi, vec: ve-CC, vi_VN: vi-VN, fr_FR: fr-FR, id_ID: id, zh_Hans: zh-Hans, en_DE: en-DE, en_GB: en-GB, el_GR: el-GR, uk_UA: uk-UA, az@latin: az-LA, de_AT: de-AT, uz@Latn: uz-LA, vls: vl-SS, ar_DZ: ar-DZ, bg_BG: bg, es_PY: es-PY, fy_NL: fy-NL, uz@Arab: uz-AR, ru_UA: ru-UA, war: wa-RR, zh_CN.GB2312: zh-GB
|
||||
lang_map = te_IN: te-IN, es_AR: es-AR, es_419: es-LA, es_TX: es-TX, he_IL: he-IL, zh_CN: zh-CN, ar_EG: ar-EG, cs_CZ: cs-CZ, fa_IR: fa-IR, ms_MY: ms-MY, nl_NL: nl-NL, de_CH: de-CH, en_IT: en-IT, uz_UZ: uz-UZ, fr_CH: fr-CH, hi_IN: hi-IN, et_EE: et-EE, es_PE: es-PE, es_MX: es-MX, gl_ES: gl-ES, mn_MN: mn, sl_SI: sl, zh_TW: zh-TW, ast_ES: ast-ES, es_CL: es-CL, ja_JP: ja, lv_LV: lv, ro_RO: ro-RO, az_AZ: az-AZ, cy_GB: cy-GB, gu_IN: gu-IN, pl_PL: pl-PL, vep: ve-PP, en_BR: en-BR, en@ysv: en-YS, hu_HU: hu, ko_KR: ko-KR, pt_BR: pt-BR, zh_HK: zh-HK, zu_ZA: zu-ZA, en_MY: en-MY, ja-Hira: ja-HI, fi_FI: fi, vec: ve-CC, vi_VN: vi-VN, fr_FR: fr-FR, id_ID: id, zh_Hans: zh-Hans, en_DE: en-DE, en_GB: en-GB, el_GR: el-GR, uk_UA: uk-UA, az@latin: az-LA, de_AT: de-AT, uz@Latn: uz-LA, vls: vl-SS, ar_DZ: ar-DZ, bg_BG: bg, es_PY: es-PY, fy_NL: fy-NL, uz@Arab: uz-AR, ru_UA: ru-UA, war: wa-RR, zh_CN.GB2312: zh-GB
|
||||
|
||||
[o:wekan:p:wekan:r:application]
|
||||
file_filter = imports/i18n/data/<lang>.i18n.json
|
||||
|
|
|
|||
177
CHANGELOG.md
177
CHANGELOG.md
|
|
@ -22,181 +22,6 @@ Fixing other platforms In Progress.
|
|||
WeKan 8.00-8.06 had wrong raw database directory setting /var/snap/wekan/common/wekan and some cards were not visible.
|
||||
Those are fixed at WeKan 8.07 where database directory is back to /var/snap/wekan/common and all cards are visible.
|
||||
|
||||
# Upcoming WeKan ® release
|
||||
|
||||
This release fixes the following CRITICAL SECURITY ISSUES of [Snowbleed](https://wekan.fi/hall-of-fame/snowbleed/):
|
||||
|
||||
- [Security Fix 1: There was not enough permission checks. Moved migrations to Admin Panel/Settings/Cron](https://github.com/wekan/wekan/commit/cbb1cd78de3e40264a5e047ace0ce27f8635b4e6).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 2: ](https://github.com/wekan/wekan/commit/).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 3: ](https://github.com/wekan/wekan/commit/).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 4: ](https://github.com/wekan/wekan/commit/).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 5: ](https://github.com/wekan/wekan/commit/).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 6: ](https://github.com/wekan/wekan/commit/).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 7: ](https://github.com/wekan/wekan/commit/).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 8: ](https://github.com/wekan/wekan/commit/).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 9: ](https://github.com/wekan/wekan/commit/).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 10: ](https://github.com/wekan/wekan/commit/).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 11: ](https://github.com/wekan/wekan/commit/).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 12: ](https://github.com/wekan/wekan/commit/).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 13: ](https://github.com/wekan/wekan/commit/).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 14: ](https://github.com/wekan/wekan/commit/).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Updated dependencies](https://github.com/wekan/wekan/pull/6059).
|
||||
Thanks to dependabot.
|
||||
- [Updated dependencies and published as @wekanteam npm packages to npmjs.com](https://github.com/wekan/wekan/commit/a9a89b501a91ffcdbdd611a05029d9483c59e4db).
|
||||
Thanks to xet7.
|
||||
- Added FerretDB2/PostgreSQL Docs.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/9fb1aeb8272b011c3d0b6b2c26ff7cb498c7b37f),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/f198421f10dd3be9d58f64a242d12ea1ef45fee3).
|
||||
Thanks to juri_ at WeKan Libera.Chat IRC and xet7.
|
||||
- [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:
|
||||
|
|
@ -235,7 +60,7 @@ Thanks to above GitHub users for their contributions and translators for their t
|
|||
|
||||
# v8.16 2025-11-02 WeKan ® release
|
||||
|
||||
This release fixes the following CRITICAL SECURITY ISSUES of [Spacebleed](https://wekan.fi/hall-of-fame/spacebleed/):
|
||||
This release fixes SpaceBleed that is the following CRITICAL SECURITY ISSUES:
|
||||
|
||||
- [Fix SECURITY ISSUE 1: File Attachments enables stored XSS (High)](https://github.com/wekan/wekan/commit/e9a727301d7b4f1689a703503df668c0f4f4cab8).
|
||||
Thanks to Siam Thanat Hack (STH) and xet7.
|
||||
|
|
|
|||
|
|
@ -249,9 +249,9 @@ cd /home/wekan/app
|
|||
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
||||
#rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
|
||||
#mv /home/wekan/app_build/bundle /build
|
||||
wget "https://github.com/wekan/wekan/releases/download/v8.19/wekan-8.19-amd64.zip"
|
||||
unzip wekan-8.19-amd64.zip
|
||||
rm wekan-8.19-amd64.zip
|
||||
wget "https://github.com/wekan/wekan/releases/download/v8.17/wekan-8.17-amd64.zip"
|
||||
unzip wekan-8.17-amd64.zip
|
||||
rm wekan-8.17-amd64.zip
|
||||
mv /home/wekan/app/bundle /build
|
||||
|
||||
# Put back the original tar
|
||||
|
|
|
|||
36
SECURITY.md
36
SECURITY.md
|
|
@ -1,20 +1,12 @@
|
|||
About money, see [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
## Responsible Security Disclosure
|
||||
Security is very important to us. If you discover any issue regarding security, please disclose
|
||||
the information responsibly by sending an email from Protonmail to security@wekan.fi
|
||||
that is Protomail email address, or by using this PGP key
|
||||
[security-at-wekan.fi.asc](security-at-wekan.fi.asc) to security@wekan.fi
|
||||
and not by creating a GitHub issue. We will respond swiftly to fix verifiable security issues.
|
||||
|
||||
- To send email, use [ProtonMail](https://proton.me) email address or use PGP key [security-at-wekan.fi.asc](security-at-wekan.fi.asc)
|
||||
- Send info about security issue ONLY to security@wekan.fi (that is Protomail email address). NOT TO ANYWHERE ELSE. NO CC, NO BCC.
|
||||
- Wait for new WeKan release that fixes security issue
|
||||
- If you approve, we thank you by adding you to Hall of Fame: https://wekan.fi/hall-of-fame/
|
||||
|
||||
## Bonus Points
|
||||
|
||||
- If you include code for fixing security issue
|
||||
|
||||
## Losing Points
|
||||
|
||||
- If you ask about [bounty](CONTRIBUTING.md). There is no bounty. WeKan is NOT Big Tech. WeKan is FLOSS.
|
||||
- If you forget to include vulnerability details.
|
||||
- If you send info about security issue to somewhere else than security@wekan.fi
|
||||
We thank you with a place at our hall of fame page, that is at https://wekan.fi/hall-of-fame
|
||||
|
||||
## How should reports be formatted?
|
||||
|
||||
|
|
@ -34,7 +26,7 @@ CWSS (optional): %cwss
|
|||
|
||||
Anyone who reports a unique security issue in scope and does not disclose it to
|
||||
a third party before we have patched and updated may be upon their approval
|
||||
added to the WeKan Hall of Fame https://wekan.fi/hall-of-fame/
|
||||
added to the Wekan Hall of Fame.
|
||||
|
||||
## Which domains are in scope?
|
||||
|
||||
|
|
@ -71,6 +63,11 @@ and by by companies that have 30k users.
|
|||
- If you are thinking about TLS MITM, look at https://github.com/caddyserver/caddy/issues/2530
|
||||
- Let's Encrypt TLS requires publicly accessible webserver, that Let's Encrypt TLS validation servers check.
|
||||
- If firewall limits to only allowed IP addresses, you may need non-Let's Encrypt TLS cert.
|
||||
- For On Premise:
|
||||
- https://caddyserver.com/docs/automatic-https#local-https
|
||||
- https://github.com/wekan/wekan/wiki/Caddy-Webserver-Config
|
||||
- https://github.com/wekan/wekan/wiki/Azure
|
||||
- https://github.com/wekan/wekan/wiki/Traefik-and-self-signed-SSL-certs
|
||||
|
||||
## XSS
|
||||
|
||||
|
|
@ -272,4 +269,9 @@ Typical already known or "no impact" bugs such as:
|
|||
- Email spoofing, SPF, DMARC & DKIM. Wekan does not include email server.
|
||||
|
||||
Wekan is Open Source with MIT license, and free to use also for commercial use.
|
||||
We welcome all fixes to improve security by email to security@wekan.fi
|
||||
We welcome all fixes to improve security by email to security@wekan.team
|
||||
|
||||
## Bonus Points
|
||||
|
||||
If your Responsible Security Disclosure includes code for fixing security issue,
|
||||
you get bonus points, as seen on [Hall of Fame](https://wekan.github.io/hall-of-fame).
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v8.19.0"
|
||||
appVersion: "v8.17.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import '/client/lib/boardConverter';
|
|||
import '/client/components/boardConversionProgress';
|
||||
|
||||
// Import migration manager and progress UI
|
||||
import '/client/lib/attachmentMigrationManager';
|
||||
import '/client/components/settings/migrationProgress';
|
||||
import '/client/lib/migrationManager';
|
||||
import '/client/components/migrationProgress';
|
||||
|
||||
// Import cron settings
|
||||
import '/client/components/settings/cronSettings';
|
||||
|
|
@ -62,20 +62,3 @@ Meteor.startup(() => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Subscribe to per-user small publications
|
||||
Meteor.startup(() => {
|
||||
Tracker.autorun(() => {
|
||||
if (Meteor.userId()) {
|
||||
Meteor.subscribe('userGreyIcons');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize mobile mode on startup for iOS devices
|
||||
// This ensures mobile mode is applied correctly on page load
|
||||
Tracker.afterFlush(() => {
|
||||
if (typeof Utils !== 'undefined' && Utils.initializeUserSettings) {
|
||||
Utils.initializeUserSettings();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -108,12 +108,15 @@
|
|||
text-decoration: none;
|
||||
height: 24px;
|
||||
}
|
||||
.comments .comment .comment-desc .reactions .open-comment-reaction-popup span {
|
||||
display: inline-block;
|
||||
font-size: clamp(14px, 2vw, 18px);
|
||||
.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-smile-o {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
margin-left: 4px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-plus {
|
||||
font-size: 8px;
|
||||
margin-top: -7px;
|
||||
margin-left: 1px;
|
||||
}
|
||||
.comments .comment .comment-desc .reactions .reaction {
|
||||
cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ template(name="comment")
|
|||
= text
|
||||
.edit-controls
|
||||
button.primary(type="submit") {{_ 'edit'}}
|
||||
a.js-close-inlined-form(title="{{_ 'close' }}") ❌
|
||||
.fa.fa-times-thin.js-close-inlined-form
|
||||
else
|
||||
.comment-text
|
||||
+viewer
|
||||
|
|
@ -55,8 +55,8 @@ template(name="commentReactions")
|
|||
span.reaction-count #{reaction.userIds.length}
|
||||
if (currentUser.isBoardMember)
|
||||
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
|
||||
span(title="{{_ 'reaction' }}") 😀
|
||||
span(title="{{_ 'add' }}") ➕
|
||||
i.fa.fa-smile-o
|
||||
i.fa.fa-plus
|
||||
|
||||
template(name="addReactionPopup")
|
||||
.reactions-popup
|
||||
|
|
|
|||
|
|
@ -57,9 +57,8 @@ BlazeComponent.extendComponent({
|
|||
|
||||
BlazeComponent.extendComponent({
|
||||
getComments() {
|
||||
const data = this.data();
|
||||
if (!data || typeof data.comments !== 'function') return [];
|
||||
return data.comments();
|
||||
const ret = this.data().comments();
|
||||
return ret;
|
||||
},
|
||||
}).register("comments");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
template(name="archivedBoards")
|
||||
h2
|
||||
span(title="{{_ 'archived-boards'}}") 📦
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archived-boards'}}
|
||||
|
||||
ul.archived-lists
|
||||
|
|
@ -8,10 +8,10 @@ template(name="archivedBoards")
|
|||
li.archived-lists-item
|
||||
div.board-header-btns
|
||||
button.board-header-btn.js-delete-board
|
||||
| 🗑️
|
||||
i.fa.fa-trash-o
|
||||
| {{_ 'delete-board'}}
|
||||
button.board-header-btn.js-restore-board
|
||||
| ↩️
|
||||
i.fa.fa-undo
|
||||
| {{_ 'restore-board'}}
|
||||
= title
|
||||
span {{ moment archivedAt 'LLL' }}
|
||||
|
|
|
|||
|
|
@ -231,30 +231,6 @@
|
|||
font-size: 1em !important; /* Keep original icon size */
|
||||
}
|
||||
|
||||
/* Mobile iPhone: scale card details text and icons to 2x */
|
||||
body.mobile-mode.iphone-device .card-details {
|
||||
font-size: 2em !important;
|
||||
}
|
||||
body.mobile-mode.iphone-device .card-details .fa,
|
||||
body.mobile-mode.iphone-device .card-details .icon,
|
||||
body.mobile-mode.iphone-device .card-details i,
|
||||
body.mobile-mode.iphone-device .card-details .emoji-icon,
|
||||
body.mobile-mode.iphone-device .card-details a,
|
||||
body.mobile-mode.iphone-device .card-details p,
|
||||
body.mobile-mode.iphone-device .card-details span,
|
||||
body.mobile-mode.iphone-device .card-details div,
|
||||
body.mobile-mode.iphone-device .card-details button,
|
||||
body.mobile-mode.iphone-device .card-details input,
|
||||
body.mobile-mode.iphone-device .card-details select,
|
||||
body.mobile-mode.iphone-device .card-details textarea {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
/* Section titles slightly larger than content but not as big as card title */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-item-title {
|
||||
font-size: 1.1em !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Ensure scrollbars are positioned correctly */
|
||||
#content[style*="overflow-x: auto"]::-webkit-scrollbar:vertical {
|
||||
width: 12px;
|
||||
|
|
@ -287,35 +263,6 @@ body.mobile-mode.iphone-device .card-details .card-details-item-title {
|
|||
animation: fadeIn 0.2s;
|
||||
z-index: 16;
|
||||
}
|
||||
|
||||
/* Fix for mobile Safari: ensure overlay stays behind card details */
|
||||
@media screen and (max-width: 800px) {
|
||||
.board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
|
||||
/* In desktop mode on small screens, still keep overlay behind card */
|
||||
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* In mobile mode, lower the overlay z-index to stay behind card details */
|
||||
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
|
||||
/* iPhone in desktop mode: remove overlay to avoid blocking card */
|
||||
body.desktop-mode.iphone-device .board-wrapper .board-canvas .board-overlay {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* Desktop mode: hide overlay to allow multiple cards and board interaction */
|
||||
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
.board-wrapper .board-canvas.is-dragging-active .open-minicard-composer,
|
||||
.board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked {
|
||||
display: none;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
template(name="board")
|
||||
|
||||
if isConverting.get
|
||||
if isMigrating.get
|
||||
+migrationProgress
|
||||
else if isConverting.get
|
||||
+boardConversionProgress
|
||||
else if isBoardReady.get
|
||||
if currentBoard
|
||||
|
|
@ -22,7 +24,7 @@ template(name="boardBody")
|
|||
// Debug information (remove in production)
|
||||
if debugBoardState
|
||||
.debug-info(style="position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.8); color: white; padding: 10px; z-index: 9999; font-size: 12px;")
|
||||
| {{_ 'board'}}: {{currentBoard.title}} | {{_ 'view'}}: {{boardView}} | {{_ 'has-swimlanes'}}: {{hasSwimlanes}} | {{_ 'swimlanes'}}: {{currentBoard.swimlanes.length}}
|
||||
| Board: {{currentBoard.title}} | View: {{boardView}} | HasSwimlanes: {{hasSwimlanes}} | Swimlanes: {{currentBoard.swimlanes.length}}
|
||||
.board-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
.board-canvas.js-swimlanes(
|
||||
class="{{#if hasSwimlanes}}dragscroll{{/if}}"
|
||||
|
|
@ -47,8 +49,6 @@ template(name="boardBody")
|
|||
+listsGroup(currentBoard)
|
||||
else if isViewCalendar
|
||||
+calendarView
|
||||
else if isViewGantt
|
||||
+ganttView
|
||||
else
|
||||
// Default view - show swimlanes if they exist, otherwise show lists
|
||||
if hasSwimlanes
|
||||
|
|
@ -56,10 +56,6 @@ template(name="boardBody")
|
|||
+swimlane(this)
|
||||
else
|
||||
+listsGroup(currentBoard)
|
||||
//- Render multiple open cards in desktop mode
|
||||
unless isMiniScreen
|
||||
each openCards
|
||||
+cardDetails(this cardIndex=@index)
|
||||
+sidebar
|
||||
|
||||
template(name="calendarView")
|
||||
|
|
@ -67,4 +63,4 @@ template(name="calendarView")
|
|||
.calendar-view.swimlane
|
||||
if currentCard
|
||||
+cardDetails(currentCard)
|
||||
+fullcalendar(calendarOptions)
|
||||
+fullcalendar(calendarOptions)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import '../gantt/gantt.js';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import dragscroll from '@wekanteam/dragscroll';
|
||||
import { boardConverter } from '/client/lib/boardConverter';
|
||||
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
|
||||
import { migrationManager } from '/client/lib/migrationManager';
|
||||
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
|
||||
import { migrationProgressManager } from '/client/components/migrationProgress';
|
||||
import Swimlanes from '/models/swimlanes';
|
||||
import Lists from '/models/lists';
|
||||
|
||||
|
|
@ -15,6 +16,7 @@ BlazeComponent.extendComponent({
|
|||
onCreated() {
|
||||
this.isBoardReady = new ReactiveVar(false);
|
||||
this.isConverting = new ReactiveVar(false);
|
||||
this.isMigrating = new ReactiveVar(false);
|
||||
this._swimlaneCreated = new Set(); // Track boards where we've created swimlanes
|
||||
this._boardProcessed = false; // Track if board has been processed
|
||||
this._lastProcessedBoardId = null; // Track last processed board ID
|
||||
|
|
@ -32,6 +34,7 @@ BlazeComponent.extendComponent({
|
|||
// Use a separate autorun for subscription ready state to avoid reactive loops
|
||||
this.subscriptionReadyAutorun = Tracker.autorun(() => {
|
||||
if (handle.ready()) {
|
||||
// Only run conversion/migration logic once per board
|
||||
if (!this._boardProcessed || this._lastProcessedBoardId !== currentBoardId) {
|
||||
this._boardProcessed = true;
|
||||
this._lastProcessedBoardId = currentBoardId;
|
||||
|
|
@ -96,31 +99,422 @@ BlazeComponent.extendComponent({
|
|||
return;
|
||||
}
|
||||
|
||||
// Automatic migration disabled - migrations must be run manually from sidebar
|
||||
// Board admins can run migrations from the sidebar Migrations menu
|
||||
this.isBoardReady.set(true);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during board conversion check:', error);
|
||||
this.isConverting.set(false);
|
||||
this.isMigrating.set(false);
|
||||
this.isBoardReady.set(true); // Show board even if conversion check failed
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if board needs comprehensive migration
|
||||
*/
|
||||
async checkComprehensiveMigration(boardId) {
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
Meteor.call('comprehensiveBoardMigration.needsMigration', boardId, (error, result) => {
|
||||
if (error) {
|
||||
console.error('Error checking comprehensive migration:', error);
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error checking comprehensive migration:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute comprehensive migration for a board
|
||||
*/
|
||||
async executeComprehensiveMigration(boardId) {
|
||||
try {
|
||||
// Start progress tracking
|
||||
migrationProgressManager.startMigration();
|
||||
|
||||
// Simulate progress updates since we can't easily pass callbacks through Meteor methods
|
||||
const progressSteps = [
|
||||
{ step: 'analyze_board_structure', name: 'Analyze Board Structure', duration: 1000 },
|
||||
{ step: 'fix_orphaned_cards', name: 'Fix Orphaned Cards', duration: 2000 },
|
||||
{ step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 3000 },
|
||||
{ step: 'ensure_per_swimlane_lists', name: 'Ensure Per-Swimlane Lists', duration: 1500 },
|
||||
{ step: 'validate_migration', name: 'Validate Migration', duration: 1000 },
|
||||
{ step: 'fix_avatar_urls', name: 'Fix Avatar URLs', duration: 1000 },
|
||||
{ step: 'fix_attachment_urls', name: 'Fix Attachment URLs', duration: 1000 }
|
||||
];
|
||||
|
||||
// Start the actual migration
|
||||
const migrationPromise = new Promise((resolve, reject) => {
|
||||
Meteor.call('comprehensiveBoardMigration.execute', boardId, (error, result) => {
|
||||
if (error) {
|
||||
console.error('Error executing comprehensive migration:', error);
|
||||
migrationProgressManager.failMigration(error);
|
||||
reject(error);
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Comprehensive migration completed for board:', boardId, result);
|
||||
}
|
||||
resolve(result.success);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Simulate progress updates
|
||||
const progressPromise = this.simulateMigrationProgress(progressSteps);
|
||||
|
||||
// Wait for both to complete
|
||||
const [migrationResult] = await Promise.all([migrationPromise, progressPromise]);
|
||||
|
||||
migrationProgressManager.completeMigration();
|
||||
return migrationResult;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error executing comprehensive migration:', error);
|
||||
migrationProgressManager.failMigration(error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulate migration progress updates
|
||||
*/
|
||||
async simulateMigrationProgress(progressSteps) {
|
||||
const totalSteps = progressSteps.length;
|
||||
|
||||
for (let i = 0; i < progressSteps.length; i++) {
|
||||
const step = progressSteps[i];
|
||||
const stepProgress = Math.round(((i + 1) / totalSteps) * 100);
|
||||
|
||||
// Update progress for this step
|
||||
migrationProgressManager.updateProgress({
|
||||
overallProgress: stepProgress,
|
||||
currentStep: i + 1,
|
||||
totalSteps,
|
||||
stepName: step.step,
|
||||
stepProgress: 0,
|
||||
stepStatus: `Starting ${step.name}...`,
|
||||
stepDetails: null,
|
||||
boardId: Session.get('currentBoard')
|
||||
});
|
||||
|
||||
// Simulate step progress
|
||||
const stepDuration = step.duration;
|
||||
const updateInterval = 100; // Update every 100ms
|
||||
const totalUpdates = stepDuration / updateInterval;
|
||||
|
||||
for (let j = 0; j < totalUpdates; j++) {
|
||||
const stepStepProgress = Math.round(((j + 1) / totalUpdates) * 100);
|
||||
|
||||
migrationProgressManager.updateProgress({
|
||||
overallProgress: stepProgress,
|
||||
currentStep: i + 1,
|
||||
totalSteps,
|
||||
stepName: step.step,
|
||||
stepProgress: stepStepProgress,
|
||||
stepStatus: `Processing ${step.name}...`,
|
||||
stepDetails: { progress: `${stepStepProgress}%` },
|
||||
boardId: Session.get('currentBoard')
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, updateInterval));
|
||||
}
|
||||
|
||||
// Complete the step
|
||||
migrationProgressManager.updateProgress({
|
||||
overallProgress: stepProgress,
|
||||
currentStep: i + 1,
|
||||
totalSteps,
|
||||
stepName: step.step,
|
||||
stepProgress: 100,
|
||||
stepStatus: `${step.name} completed`,
|
||||
stepDetails: { status: 'completed' },
|
||||
boardId: Session.get('currentBoard')
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async startBackgroundMigration(boardId) {
|
||||
try {
|
||||
// Start background migration using the cron system
|
||||
Meteor.call('boardMigration.startBoardMigration', boardId, (error, result) => {
|
||||
if (error) {
|
||||
console.error('Failed to start background migration:', error);
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Background migration started for board:', boardId);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error starting background migration:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async convertSharedListsToPerSwimlane(boardId) {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) return;
|
||||
|
||||
// Check if board has already been processed for shared lists conversion
|
||||
if (board.hasSharedListsConverted) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has already been processed for shared lists conversion`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all lists for this board
|
||||
const allLists = board.lists();
|
||||
const swimlanes = board.swimlanes();
|
||||
|
||||
if (swimlanes.length === 0) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has no swimlanes, skipping shared lists conversion`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Find shared lists (lists with empty swimlaneId or null swimlaneId)
|
||||
const sharedLists = allLists.filter(list => !list.swimlaneId || list.swimlaneId === '');
|
||||
|
||||
if (sharedLists.length === 0) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has no shared lists to convert`);
|
||||
}
|
||||
// Mark as processed even if no shared lists
|
||||
Boards.update(boardId, { $set: { hasSharedListsConverted: true } });
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Converting ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`);
|
||||
}
|
||||
|
||||
// Convert each shared list to per-swimlane lists
|
||||
for (const sharedList of sharedLists) {
|
||||
// Create a copy of the list for each swimlane
|
||||
for (const swimlane of swimlanes) {
|
||||
// Check if this list already exists in this swimlane
|
||||
const existingList = Lists.findOne({
|
||||
boardId: boardId,
|
||||
swimlaneId: swimlane._id,
|
||||
title: sharedList.title
|
||||
});
|
||||
|
||||
if (!existingList) {
|
||||
// Double-check to avoid race conditions
|
||||
const doubleCheckList = ReactiveCache.getList({
|
||||
boardId: boardId,
|
||||
swimlaneId: swimlane._id,
|
||||
title: sharedList.title
|
||||
});
|
||||
|
||||
if (!doubleCheckList) {
|
||||
// Create a new list in this swimlane
|
||||
const newListData = {
|
||||
title: sharedList.title,
|
||||
boardId: boardId,
|
||||
swimlaneId: swimlane._id,
|
||||
sort: sharedList.sort || 0,
|
||||
archived: sharedList.archived || false, // Preserve archived state from original list
|
||||
createdAt: new Date(),
|
||||
modifiedAt: new Date()
|
||||
};
|
||||
|
||||
// Copy other properties if they exist
|
||||
if (sharedList.color) newListData.color = sharedList.color;
|
||||
if (sharedList.wipLimit) newListData.wipLimit = sharedList.wipLimit;
|
||||
if (sharedList.wipLimitEnabled) newListData.wipLimitEnabled = sharedList.wipLimitEnabled;
|
||||
if (sharedList.wipLimitSoft) newListData.wipLimitSoft = sharedList.wipLimitSoft;
|
||||
|
||||
Lists.insert(newListData);
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
const archivedStatus = sharedList.archived ? ' (archived)' : ' (active)';
|
||||
console.log(`Created list "${sharedList.title}"${archivedStatus} for swimlane ${swimlane.title || swimlane._id}`);
|
||||
}
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`List "${sharedList.title}" already exists in swimlane ${swimlane.title || swimlane._id} (double-check), skipping`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`List "${sharedList.title}" already exists in swimlane ${swimlane.title || swimlane._id}, skipping`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the original shared list completely
|
||||
Lists.remove(sharedList._id);
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Removed shared list "${sharedList.title}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark board as processed
|
||||
Boards.update(boardId, { $set: { hasSharedListsConverted: true } });
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Successfully converted ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error converting shared lists to per-swimlane:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async fixMissingLists(boardId) {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) return;
|
||||
|
||||
// Check if board has already been processed for missing lists fix
|
||||
if (board.fixMissingListsCompleted) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has already been processed for missing lists fix`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if migration is needed
|
||||
const needsMigration = await new Promise((resolve, reject) => {
|
||||
Meteor.call('fixMissingListsMigration.needsMigration', boardId, (error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!needsMigration) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} does not need missing lists fix`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Starting fix missing lists migration for board ${boardId}`);
|
||||
}
|
||||
|
||||
// Execute the migration
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
Meteor.call('fixMissingListsMigration.execute', boardId, (error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (result && result.success) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Successfully fixed missing lists for board ${boardId}: created ${result.createdLists} lists, updated ${result.updatedCards} cards`);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fixing missing lists:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async fixDuplicateLists(boardId) {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) return;
|
||||
|
||||
// Check if board has already been processed for duplicate lists fix
|
||||
if (board.fixDuplicateListsCompleted) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has already been processed for duplicate lists fix`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Starting duplicate lists fix for board ${boardId}`);
|
||||
}
|
||||
|
||||
// Execute the duplicate lists fix
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
Meteor.call('fixDuplicateLists.fixBoard', boardId, (error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (result && result.fixed > 0) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Successfully fixed ${result.fixed} duplicate lists for board ${boardId}: ${result.fixedSwimlanes} swimlanes, ${result.fixedLists} lists`);
|
||||
}
|
||||
|
||||
// Mark board as processed
|
||||
Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } });
|
||||
} else if (process.env.DEBUG === 'true') {
|
||||
console.log(`No duplicate lists found for board ${boardId}`);
|
||||
// Still mark as processed to avoid repeated checks
|
||||
Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } });
|
||||
} else {
|
||||
// Still mark as processed to avoid repeated checks
|
||||
Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fixing duplicate lists:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async startAttachmentMigrationIfNeeded(boardId) {
|
||||
try {
|
||||
// Check if board has already been migrated
|
||||
if (attachmentMigrationManager.isBoardMigrated(boardId)) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has already been migrated, skipping`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there are unconverted attachments
|
||||
const unconvertedAttachments = attachmentMigrationManager.getUnconvertedAttachments(boardId);
|
||||
|
||||
if (unconvertedAttachments.length > 0) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Starting attachment migration for ${unconvertedAttachments.length} attachments in board ${boardId}`);
|
||||
}
|
||||
await attachmentMigrationManager.startAttachmentMigration(boardId);
|
||||
} else {
|
||||
// No attachments to migrate, mark board as migrated
|
||||
// This will be handled by the migration manager itself
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has no attachments to migrate`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting attachment migration:', error);
|
||||
}
|
||||
},
|
||||
|
||||
onlyShowCurrentCard() {
|
||||
const isMiniScreen = Utils.isMiniScreen();
|
||||
const currentCardId = Utils.getCurrentCardId(true);
|
||||
return isMiniScreen && currentCardId;
|
||||
},
|
||||
|
||||
openCards() {
|
||||
// In desktop mode, return array of all open cards
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (!isMobile) {
|
||||
const openCardIds = Session.get('openCards') || [];
|
||||
return openCardIds.map(id => ReactiveCache.getCard(id)).filter(card => card);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
goHome() {
|
||||
FlowRouter.go('home');
|
||||
},
|
||||
|
|
@ -129,6 +523,10 @@ BlazeComponent.extendComponent({
|
|||
return this.isConverting.get();
|
||||
},
|
||||
|
||||
isMigrating() {
|
||||
return this.isMigrating.get();
|
||||
},
|
||||
|
||||
isBoardReady() {
|
||||
return this.isBoardReady.get();
|
||||
},
|
||||
|
|
@ -580,19 +978,6 @@ BlazeComponent.extendComponent({
|
|||
return boardView === 'board-view-cal';
|
||||
},
|
||||
|
||||
isViewGantt() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
if (currentUser) {
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
|
||||
return boardView === 'board-view-gantt';
|
||||
},
|
||||
|
||||
hasSwimlanes() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
if (!currentBoard) {
|
||||
|
|
@ -636,6 +1021,7 @@ BlazeComponent.extendComponent({
|
|||
const currentBoardId = Session.get('currentBoard');
|
||||
const isBoardReady = this.isBoardReady.get();
|
||||
const isConverting = this.isConverting.get();
|
||||
const isMigrating = this.isMigrating.get();
|
||||
const boardView = Utils.boardView();
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
|
|
@ -644,6 +1030,7 @@ BlazeComponent.extendComponent({
|
|||
console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none');
|
||||
console.log('isBoardReady:', isBoardReady);
|
||||
console.log('isConverting:', isConverting);
|
||||
console.log('isMigrating:', isMigrating);
|
||||
console.log('boardView:', boardView);
|
||||
console.log('========================');
|
||||
}
|
||||
|
|
@ -654,6 +1041,7 @@ BlazeComponent.extendComponent({
|
|||
currentBoardTitle: currentBoard ? currentBoard.title : 'none',
|
||||
isBoardReady,
|
||||
isConverting,
|
||||
isMigrating,
|
||||
boardView
|
||||
};
|
||||
},
|
||||
|
|
@ -1020,8 +1408,3 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
},
|
||||
}).register('calendarView');
|
||||
/**
|
||||
* Gantt View Component
|
||||
* Displays cards as a Gantt chart with start/due dates
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ template(name="boardHeaderBar")
|
|||
| ❌
|
||||
|
||||
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
|
||||
span.emoji-icon 🔍
|
||||
| 🔍
|
||||
|
||||
unless currentBoard.isTemplatesBoard
|
||||
a.board-header-btn.js-toggle-board-view(
|
||||
|
|
@ -121,8 +121,6 @@ template(name="boardHeaderBar")
|
|||
| 📋
|
||||
if $eq boardView 'board-view-cal'
|
||||
| 📅
|
||||
if $eq boardView 'board-view-gantt'
|
||||
| 📊
|
||||
|
||||
if canModifyBoard
|
||||
a.board-header-btn.js-multiselection-activate(
|
||||
|
|
@ -210,13 +208,6 @@ template(name="boardChangeViewPopup")
|
|||
| {{_ 'board-view-cal'}}
|
||||
if $eq Utils.boardView "board-view-cal"
|
||||
| ✅
|
||||
li
|
||||
with "board-view-gantt"
|
||||
a.js-open-gantt-view
|
||||
| 📊
|
||||
| {{_ 'board-view-gantt'}}
|
||||
if $eq Utils.boardView "board-view-gantt"
|
||||
| ✅
|
||||
|
||||
template(name="createBoard")
|
||||
form
|
||||
|
|
|
|||
|
|
@ -208,10 +208,6 @@ Template.boardChangeViewPopup.events({
|
|||
Utils.setBoardView('board-view-cal');
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-open-gantt-view'() {
|
||||
Utils.setBoardView('board-view-gantt');
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
const CreateBoard = BlazeComponent.extendComponent({
|
||||
|
|
|
|||
|
|
@ -583,9 +583,9 @@
|
|||
}
|
||||
|
||||
.board-list .board-list-item .multi-selection-checkbox.is-checked {
|
||||
background: #3cb500;
|
||||
border-color: #3cb500;
|
||||
box-shadow: 0 2px 8px rgba(60, 181, 0, 0.6);
|
||||
background: #2196F3;
|
||||
border-color: #2196F3;
|
||||
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.6);
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
top: auto !important;
|
||||
|
|
@ -601,22 +601,10 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Grey checkboxes when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .board-list .board-list-item .multi-selection-checkbox.is-checked {
|
||||
background: #7a7a7a;
|
||||
border-color: #7a7a7a;
|
||||
box-shadow: 0 2px 8px rgba(122, 122, 122, 0.6);
|
||||
}
|
||||
|
||||
body.grey-icons-enabled .board-list.is-multiselection-active .js-board.is-checked {
|
||||
outline: 4px solid #7a7a7a;
|
||||
box-shadow: 0 4px 12px rgba(122, 122, 122, 0.4);
|
||||
}
|
||||
|
||||
.board-list.is-multiselection-active .js-board.is-checked {
|
||||
outline: 4px solid #3cb500;
|
||||
outline: 4px solid #2196F3;
|
||||
outline-offset: -4px;
|
||||
box-shadow: 0 4px 12px rgba(60, 181, 0, 0.4);
|
||||
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4);
|
||||
}
|
||||
|
||||
/* Visual hint when multiselection is active */
|
||||
|
|
@ -657,19 +645,8 @@ body.grey-icons-enabled .board-list.is-multiselection-active .js-board.is-checke
|
|||
}
|
||||
.board-backgrounds-list .board-background-select .background-box i.fa-check {
|
||||
font-size: 25px;
|
||||
color: #3cb500;
|
||||
color: #fff;
|
||||
}
|
||||
/* Grey check icons when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .board-backgrounds-list .board-background-select .background-box i.fa-check {
|
||||
color: #7a7a7a;
|
||||
}
|
||||
|
||||
/* Prevent Grey Icons from affecting checkmarks in background color list */
|
||||
body.grey-icons-enabled .checkmark-no-grey {
|
||||
filter: none !important;
|
||||
-webkit-filter: none !important;
|
||||
}
|
||||
|
||||
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
|
||||
.board-list.mobile-view {
|
||||
height: calc(100vh - 120px);
|
||||
|
|
|
|||
|
|
@ -8,26 +8,18 @@ template(name="boardList")
|
|||
ul.menu
|
||||
li(class="menu-item {{#if isSelectedMenu 'starred'}}active{{/if}}")
|
||||
a.js-select-menu(data-type="starred")
|
||||
span.menu-label
|
||||
span.emoji-icon ⭐
|
||||
| {{_ 'allboards.starred'}}
|
||||
span.menu-label ⭐ {{_ 'allboards.starred'}}
|
||||
span.menu-count {{menuItemCount 'starred'}}
|
||||
li(class="menu-item {{#if isSelectedMenu 'templates'}}active{{/if}}")
|
||||
a.js-select-menu(data-type="templates")
|
||||
span.menu-label
|
||||
span.emoji-icon 📋
|
||||
| {{_ 'allboards.templates'}}
|
||||
span.menu-label 📋 {{_ 'allboards.templates'}}
|
||||
span.menu-count {{menuItemCount 'templates'}}
|
||||
li(class="menu-item {{#if isSelectedMenu 'remaining'}}active{{/if}}")
|
||||
a.js-select-menu(data-type="remaining")
|
||||
span.menu-label
|
||||
span.emoji-icon 📂
|
||||
| {{_ 'allboards.remaining'}}
|
||||
span.menu-label 📂 {{_ 'allboards.remaining'}}
|
||||
span.menu-count {{menuItemCount 'remaining'}}
|
||||
.workspaces-header
|
||||
span
|
||||
span.emoji-icon 🗂️
|
||||
| {{_ 'allboards.workspaces'}}
|
||||
span 🗂️ {{_ 'allboards.workspaces'}}
|
||||
a.js-add-workspace(title="{{_ 'allboards.add-workspace'}}") +
|
||||
// Workspaces tree
|
||||
+workspaceTree(nodes=workspacesTree selectedWorkspaceId=selectedWorkspaceId)
|
||||
|
|
@ -51,49 +43,44 @@ template(name="boardList")
|
|||
li.AllBoardBtns
|
||||
div.AllBoardButtonsContainer
|
||||
if userHasOrgsOrTeams
|
||||
span.emoji-icon 🔍
|
||||
span 🔍
|
||||
input#filterBtn(type="button" value="{{_ 'filter'}}")
|
||||
button#resetBtn.filter-reset-btn
|
||||
span.reset-icon
|
||||
span.emoji-icon ❌
|
||||
span.reset-icon ❌
|
||||
span {{_ 'filter-clear'}}
|
||||
|
||||
// Right boards grid
|
||||
.boards-right-grid
|
||||
.boards-path-header
|
||||
.path-left
|
||||
span.path-icon.emoji-icon {{currentMenuPath.icon}}
|
||||
span.path-icon {{currentMenuPath.icon}}
|
||||
span.path-text {{currentMenuPath.text}}
|
||||
if BoardMultiSelection.isActive
|
||||
span.multiselection-hint
|
||||
span.emoji-icon 📌
|
||||
| {{_ 'multi-selection-active'}}
|
||||
span.multiselection-hint 📌 {{_ 'multi-selection-active'}}
|
||||
.path-right
|
||||
if canModifyBoards
|
||||
if hasBoardsSelected
|
||||
button.js-archive-selected-boards.board-header-btn
|
||||
span.emoji-icon 📦
|
||||
span 📦
|
||||
span {{_ 'archive-board'}}
|
||||
button.js-duplicate-selected-boards.board-header-btn
|
||||
span.emoji-icon 📋
|
||||
span 📋
|
||||
span {{_ 'duplicate-board'}}
|
||||
a.board-header-btn.js-multiselection-activate(
|
||||
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
|
||||
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
|
||||
span.emoji-icon ☑️
|
||||
| ☑️
|
||||
if BoardMultiSelection.isActive
|
||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||
span.emoji-icon ✖
|
||||
| ✖
|
||||
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
|
||||
li.js-add-board
|
||||
if isSelectedMenu 'templates'
|
||||
a.board-list-item.label(title="{{_ 'add-template-container'}}")
|
||||
span.emoji-icon ➕
|
||||
| {{_ 'add-template-container'}}
|
||||
| ➕ {{_ 'add-template-container'}}
|
||||
else
|
||||
a.board-list-item.label(title="{{_ 'add-board'}}")
|
||||
span.emoji-icon ➕
|
||||
| {{_ 'add-board'}}
|
||||
| ➕ {{_ 'add-board'}}
|
||||
each boards
|
||||
li.js-board(class="{{_id}} {{#if isStarred}}starred{{/if}} {{colorClass}} {{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}", draggable="true")
|
||||
if isInvited
|
||||
|
|
@ -106,8 +93,7 @@ template(name="boardList")
|
|||
span.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
span.emoji-icon
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
p.board-list-item-desc {{_ 'just-invited'}}
|
||||
button.js-accept-invite.primary {{_ 'accept'}}
|
||||
button.js-decline-invite {{_ 'decline'}}
|
||||
|
|
@ -117,9 +103,7 @@ template(name="boardList")
|
|||
if BoardMultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
span.board-handle(title="{{_ 'drag-board'}}")
|
||||
span.emoji-icon ↕️
|
||||
|
||||
span.board-handle(title="{{_ 'drag-board'}}") ↕️
|
||||
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
span.details
|
||||
span.board-list-item-name(title="{{_ 'template-container'}}")
|
||||
|
|
@ -132,20 +116,17 @@ template(name="boardList")
|
|||
span.js-has-spenttime-cards(
|
||||
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
span.emoji-icon ⏱️
|
||||
| ⏱️
|
||||
span.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
span.emoji-icon
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
else
|
||||
.board-list-item
|
||||
if BoardMultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
span.board-handle(title="{{_ 'drag-board'}}")
|
||||
span.emoji-icon ↕️
|
||||
|
||||
span.board-handle(title="{{_ 'drag-board'}}") ↕️
|
||||
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
span.details
|
||||
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
|
||||
|
|
@ -170,12 +151,11 @@ template(name="boardList")
|
|||
span.js-has-spenttime-cards(
|
||||
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
span.emoji-icon ⏱️
|
||||
| ⏱️
|
||||
a.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
span.emoji-icon
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
|
||||
template(name="boardListHeaderBar")
|
||||
h1 {{_ title }}
|
||||
|
|
@ -194,19 +174,16 @@ template(name="workspaceTree")
|
|||
each nodes
|
||||
li.workspace-node(class="{{#if $eq id selectedWorkspaceId}}active{{/if}}" data-workspace-id="{{id}}" draggable="true")
|
||||
.workspace-node-content
|
||||
span.workspace-drag-handle
|
||||
span.emoji-icon ↕️
|
||||
|
||||
span.workspace-drag-handle ↕️
|
||||
a.js-select-workspace(data-id="{{id}}")
|
||||
span.workspace-icon
|
||||
if icon
|
||||
+viewer
|
||||
= icon
|
||||
else
|
||||
span.emoji-icon 📁
|
||||
| 📁
|
||||
span.workspace-name= name
|
||||
a.js-edit-workspace(data-id="{{id}}" title="{{_ 'allboards.edit-workspace'}}")
|
||||
span.emoji-icon ✏️
|
||||
a.js-edit-workspace(data-id="{{id}}" title="{{_ 'allboards.edit-workspace'}}") ✏️
|
||||
span.workspace-count {{workspaceCount id}}
|
||||
a.js-add-subworkspace(data-id="{{id}}" title="{{_ 'allboards.add-subworkspace'}}") +
|
||||
if children
|
||||
|
|
|
|||
|
|
@ -232,18 +232,20 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
boards() {
|
||||
let query = {
|
||||
// { type: 'board' },
|
||||
// { type: { $in: ['board','template-container'] } },
|
||||
$and: [
|
||||
{ archived: false },
|
||||
{ type: { $in: ['board', 'template-container'] } },
|
||||
{ $or: [] },
|
||||
{ title: { $not: { $regex: /^\^.*\^$/ } } }
|
||||
]
|
||||
};
|
||||
const membershipOrs = [];
|
||||
|
||||
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
|
||||
|
||||
if (FlowRouter.getRouteName() === 'home') {
|
||||
membershipOrs.push({ 'members.userId': Meteor.userId() });
|
||||
query.$and[2].$or.push({ 'members.userId': Meteor.userId() });
|
||||
|
||||
if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue) {
|
||||
query.$and.push({ 'permission': 'private' });
|
||||
|
|
@ -258,7 +260,7 @@ BlazeComponent.extendComponent({
|
|||
// }
|
||||
|
||||
//query.$and[2].$or.push({'orgs': {$elemMatch : {orgId: orgsIds[0]}}});
|
||||
membershipOrs.push({ 'orgs.orgId': { $in: orgsIds } });
|
||||
query.$and[2].$or.push({ 'orgs.orgId': { $in: orgsIds } });
|
||||
}
|
||||
|
||||
let teamIdsUserBelongs = currUser?.teamIdsUserBelongs() || '';
|
||||
|
|
@ -268,11 +270,8 @@ BlazeComponent.extendComponent({
|
|||
// query.$or[2].$or.push({'teams.teamId': teamsIds[i]});
|
||||
// }
|
||||
//query.$and[2].$or.push({'teams': { $elemMatch : {teamId: teamsIds[0]}}});
|
||||
membershipOrs.push({ 'teams.teamId': { $in: teamsIds } });
|
||||
query.$and[2].$or.push({ 'teams.teamId': { $in: teamsIds } });
|
||||
}
|
||||
if (membershipOrs.length) {
|
||||
query.$and.splice(2, 0, { $or: membershipOrs });
|
||||
}
|
||||
}
|
||||
else if (allowPrivateVisibilityOnly !== undefined && !allowPrivateVisibilityOnly.booleanValue) {
|
||||
query = {
|
||||
|
|
@ -546,18 +545,15 @@ BlazeComponent.extendComponent({
|
|||
const query = {
|
||||
$and: [
|
||||
{ archived: false },
|
||||
{ type: 'board' }
|
||||
{ type: 'board' },
|
||||
{ $or: [] }
|
||||
]
|
||||
};
|
||||
const ors = [];
|
||||
if (selectedTeamsValues.length > 0) {
|
||||
ors.push({ 'teams.teamId': { $in: selectedTeamsValues } });
|
||||
query.$and[2].$or.push({ 'teams.teamId': { $in: selectedTeamsValues } });
|
||||
}
|
||||
if (selectedOrgsValues.length > 0) {
|
||||
ors.push({ 'orgs.orgId': { $in: selectedOrgsValues } });
|
||||
}
|
||||
if (ors.length) {
|
||||
query.$and.push({ $or: ors });
|
||||
query.$and[2].$or.push({ 'orgs.orgId': { $in: selectedOrgsValues } });
|
||||
}
|
||||
|
||||
let filteredBoards = ReactiveCache.getBoards(query, {});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ template(name="miniboard")
|
|||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
span.drag-handle(title="{{_ 'dragBoard'}}") ↕️
|
||||
.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
||||
|
|
|
|||
|
|
@ -336,3 +336,36 @@
|
|||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Attachment migration styles */
|
||||
.attachment-item.migrating {
|
||||
position: relative;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.attachment-migration-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.migration-spinner {
|
||||
font-size: 24px;
|
||||
color: #007cba;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.migration-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,9 +55,10 @@ template(name="cardCustomField-number")
|
|||
template(name="cardCustomField-checkbox")
|
||||
.js-checklist-item.checklist-item(class="{{#if data.value }}is-checked{{/if}}")
|
||||
if canModifyCard
|
||||
span.check-box-unicode {{#if data.value }}✅{{else}}⬜{{/if}}
|
||||
.check-box-container
|
||||
.check-box.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
|
||||
else
|
||||
span.check-box-unicode {{#if data.value }}✅{{else}}⬜{{/if}}
|
||||
.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
|
||||
|
||||
template(name="cardCustomField-currency")
|
||||
if canModifyCard
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@ CardCustomField.register('cardCustomField');
|
|||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-checklist-item .check-box-unicode': this.toggleItem,
|
||||
'click .js-checklist-item .check-box-container': this.toggleItem,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@
|
|||
display: block;
|
||||
position: relative;
|
||||
float: left;
|
||||
height: clamp(24px, 3.5vw, 36px);
|
||||
width: clamp(24px, 3.5vw, 36px);
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
margin: .3vh;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
|
@ -118,65 +118,6 @@
|
|||
transition: flex-basis 0.1s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Desktop mode: position card below board header */
|
||||
body.desktop-mode .card-details:not(.card-details-popup) {
|
||||
position: fixed;
|
||||
width: auto;
|
||||
max-width: 800px;
|
||||
flex-basis: auto;
|
||||
border-radius: 8px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Default position for first card or when dragged */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):not([style*="left"]):not([style*="top"]) {
|
||||
top: 50px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
/* Stagger positions for multiple cards using nth-of-type */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(1) {
|
||||
top: 50px;
|
||||
left: 20px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(2) {
|
||||
top: 80px;
|
||||
left: 50px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(3) {
|
||||
top: 110px;
|
||||
left: 80px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(4) {
|
||||
top: 140px;
|
||||
left: 110px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(5) {
|
||||
top: 170px;
|
||||
left: 140px;
|
||||
}
|
||||
|
||||
/* For expanded cards, set dimensions */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):not(.card-details-collapsed) {
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
/* Collapsed card state - hide content and set height to title row only */
|
||||
.card-details.card-details-collapsed .card-details-canvas > *:not(.card-details-header) {
|
||||
display: none;
|
||||
}
|
||||
.card-details.card-details-collapsed {
|
||||
height: auto !important;
|
||||
bottom: auto !important;
|
||||
overflow: visible;
|
||||
}
|
||||
body.desktop-mode .card-details.card-details-collapsed {
|
||||
bottom: auto !important;
|
||||
}
|
||||
.card-details .mCustomScrollBox {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
|
@ -198,30 +139,6 @@ body.desktop-mode .card-details.card-details-collapsed {
|
|||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Collapse toggle triangle */
|
||||
.card-details .card-details-header .card-collapse-toggle {
|
||||
float: left;
|
||||
font-size: 20px;
|
||||
padding: 7px 10px;
|
||||
margin-left: -10px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Drag handle */
|
||||
.card-details .card-details-header .card-drag-handle {
|
||||
font-size: 20px;
|
||||
padding: 8px 10px;
|
||||
margin-right: 10px;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.card-details .card-details-header .close-card-details,
|
||||
.card-details .card-details-header .maximize-card-details,
|
||||
.card-details .card-details-header .minimize-card-details,
|
||||
|
|
@ -239,16 +156,11 @@ body.desktop-mode .card-details.card-details-collapsed {
|
|||
font-size: 24px;
|
||||
padding: 5px 10px 5px 10px;
|
||||
margin-right: -8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details-mobile-web,
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
.card-details .card-details-header .close-card-details-mobile-web {
|
||||
font-size: 24px;
|
||||
padding: 5px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
margin-right: 40px;
|
||||
}
|
||||
.card-details .card-details-header .card-copy-button {
|
||||
font-size: 17px;
|
||||
|
|
@ -269,36 +181,6 @@ body.desktop-mode .card-details.card-details-collapsed {
|
|||
padding: 10px;
|
||||
margin-right: 30px;
|
||||
}
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle,
|
||||
.card-details .card-details-header .card-zoom-in,
|
||||
.card-details .card-details-header .card-zoom-out {
|
||||
font-size: 24px;
|
||||
padding: 5px 10px 5px 10px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* Unify all card text to match title size */
|
||||
.card-details {
|
||||
font-size: 1em;
|
||||
}
|
||||
.card-details p,
|
||||
.card-details span,
|
||||
.card-details div,
|
||||
.card-details a,
|
||||
.card-details label,
|
||||
.card-details input,
|
||||
.card-details textarea,
|
||||
.card-details select,
|
||||
.card-details button,
|
||||
.card-details .card-details-item-title,
|
||||
.card-details .card-label,
|
||||
.card-details .viewer {
|
||||
font-size: inherit;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.card-details .card-details-header .card-details-watch {
|
||||
font-size: 17px;
|
||||
padding-left: 7px;
|
||||
|
|
@ -402,19 +284,6 @@ body.desktop-mode .card-details.card-details-collapsed {
|
|||
position: fixed;
|
||||
resize: both;
|
||||
}
|
||||
|
||||
/* Override for mobile mode even on larger screens */
|
||||
body.mobile-mode .card-details {
|
||||
width: 100vw !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
.card-details-maximized {
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
|
|
@ -466,53 +335,19 @@ input[type="submit"].attachment-add-link-submit {
|
|||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
.card-details {
|
||||
width: 100% !important;
|
||||
padding: 0px 0px 0px 0px !important;
|
||||
margin: 0px !important;
|
||||
width: calc(100% - 1px);
|
||||
padding: 0px 20px 0px 20px;
|
||||
margin: 0px;
|
||||
transition: none;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
/* iOS Safari specific fixes */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 100 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Ensure card details are above everything on mobile */
|
||||
body.mobile-mode .card-details {
|
||||
z-index: 100 !important;
|
||||
width: 100vw !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
overflow-y: revert;
|
||||
overflow-x: revert;
|
||||
}
|
||||
.card-details .card-details-canvas {
|
||||
width: 100%;
|
||||
padding-left: 0px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details {
|
||||
margin-right: 0px;
|
||||
display: block !important;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details-mobile-web {
|
||||
display: block !important;
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
display: block !important;
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
display: block !important;
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
.card-details .card-details-header .card-details-menu {
|
||||
margin-right: 40px;
|
||||
|
|
@ -538,62 +373,6 @@ input[type="submit"].attachment-add-link-submit {
|
|||
.pop-over > .content-wrapper > .popup-container-depth-0 .card-details-header {
|
||||
margin: 0;
|
||||
}
|
||||
/* iPhone mobile: enlarge header buttons and increase spacing */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header {
|
||||
padding-right: 16px;
|
||||
}
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .maximize-card-details,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .minimize-card-details,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-details-menu-mobile-web,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-copy-mobile-button,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-mobile-desktop-toggle,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-in,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-out {
|
||||
font-size: 2em !important; /* 2x bigger */
|
||||
padding: 0.3em !important;
|
||||
margin-right: 0.75em !important; /* 2x space compared to default */
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
/* Avoid clipping of the close button on the right edge */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details {
|
||||
margin-right: 0.75em !important;
|
||||
}
|
||||
/* Enlarge the header title too */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-details-title {
|
||||
font-size: 1.2em !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile mode styles - apply when body has mobile-mode class regardless of screen size */
|
||||
body.mobile-mode .card-details {
|
||||
width: 100vw !important;
|
||||
padding: 0px !important;
|
||||
margin: 0px !important;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 100 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body.mobile-mode .card-details .card-details-canvas {
|
||||
width: 100% !important;
|
||||
padding: 0 15px !important;
|
||||
}
|
||||
|
||||
body.mobile-mode .card-details .card-details-header .close-card-details,
|
||||
body.mobile-mode .card-details .card-details-header .close-card-details-mobile-web {
|
||||
display: block !important;
|
||||
}
|
||||
.card-details-white {
|
||||
background: #fff !important;
|
||||
|
|
|
|||
|
|
@ -5,18 +5,13 @@ template(name="cardDetails")
|
|||
|
||||
+attachmentViewer
|
||||
|
||||
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}' class='{{#if cardCollapsed}}card-details-collapsed{{/if}}'): .card-details-canvas
|
||||
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}'): .card-details-canvas
|
||||
.card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
|
||||
+inlinedForm(classNames="js-card-details-title")
|
||||
+editCardTitleForm
|
||||
else
|
||||
unless isMiniScreen
|
||||
unless isPopup
|
||||
span.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
|
||||
if cardCollapsed
|
||||
| ▶
|
||||
else
|
||||
| 🔽
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
| ❌
|
||||
if canModifyCard
|
||||
|
|
@ -31,40 +26,24 @@ template(name="cardDetails")
|
|||
| ☰
|
||||
a.card-copy-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
class="fa-link"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.emoji-icon 🔗
|
||||
span.card-drag-handle.js-card-drag-handle(title="Drag card")
|
||||
| ↕️
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
else
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
| ❌
|
||||
a.card-zoom-out.js-card-zoom-out(title="{{_ 'zoom-out'}}")
|
||||
| 🔍➖
|
||||
a.card-zoom-in.js-card-zoom-in(title="{{_ 'zoom-in'}}")
|
||||
| 🔍➕
|
||||
a.card-mobile-desktop-toggle.js-card-mobile-desktop-toggle(title="{{_ 'mobile-desktop-toggle'}}")
|
||||
if mobileMode
|
||||
| 🖥️
|
||||
else
|
||||
| 📱
|
||||
unless isPopup
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
| ❌
|
||||
if canModifyCard
|
||||
if cardMaximized
|
||||
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
| 🔽
|
||||
else
|
||||
a.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
| 🔼
|
||||
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
| ☰
|
||||
a.card-copy-mobile-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
class="fa-link"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.emoji-icon 🔗
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
h2.card-details-title.js-card-title(
|
||||
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
|
||||
|
|
@ -325,7 +304,7 @@ template(name="cardDetails")
|
|||
hr
|
||||
.card-details-item.card-details-item-customfield
|
||||
h3.card-details-item-title
|
||||
| 📋
|
||||
| 📋-alt
|
||||
= definition.name
|
||||
+cardCustomField
|
||||
|
||||
|
|
@ -699,7 +678,7 @@ template(name="cardDetailsActionsPopup")
|
|||
| 👁️
|
||||
| {{_ 'unwatch'}}
|
||||
else
|
||||
| 👁️
|
||||
| 👁️-slash
|
||||
| {{_ 'watch'}}
|
||||
hr
|
||||
if canModifyCard
|
||||
|
|
@ -719,7 +698,7 @@ template(name="cardDetailsActionsPopup")
|
|||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-custom-fields
|
||||
| 📋
|
||||
| 📋-alt
|
||||
| {{_ 'card-edit-custom-fields'}}
|
||||
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
|
||||
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
|
||||
|
|
@ -739,7 +718,7 @@ template(name="cardDetailsActionsPopup")
|
|||
| 👁️
|
||||
| {{_ 'hide-list-on-minicard'}}
|
||||
else
|
||||
| 👁️
|
||||
| 👁️-slash
|
||||
| {{_ 'show-list-on-minicard'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
|
|
@ -788,7 +767,7 @@ template(name="cardDetailsActionsPopup")
|
|||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
span.emoji-icon 🔗
|
||||
| 🔗
|
||||
| {{_ 'cardMorePopup-title'}}
|
||||
|
||||
template(name="exportCardPopup")
|
||||
|
|
@ -824,29 +803,17 @@ template(name="copyAndMoveCard")
|
|||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import CardComments from '/models/cardComments';
|
|||
import { ALLOWED_COLORS } from '/config/const';
|
||||
import { UserAvatar } from '../users/userAvatar';
|
||||
import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList';
|
||||
import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard';
|
||||
import { handleFileUpload } from './attachments';
|
||||
import uploadProgressManager from '../../lib/uploadProgressManager';
|
||||
|
||||
|
|
@ -64,11 +63,7 @@ BlazeComponent.extendComponent({
|
|||
const boardBody = this.parentComponent().parentComponent();
|
||||
//in Miniview parent is Board, not BoardBody.
|
||||
if (boardBody !== null) {
|
||||
// Only show overlay in mobile mode, not in desktop mode
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (isMobile) {
|
||||
boardBody.showOverlay.set(true);
|
||||
}
|
||||
boardBody.showOverlay.set(true);
|
||||
boardBody.mouseHasEnterCardDetails = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -86,7 +81,6 @@ BlazeComponent.extendComponent({
|
|||
|
||||
isWatching() {
|
||||
const card = this.currentData();
|
||||
if (!card || typeof card.findWatcher !== 'function') return false;
|
||||
return card.findWatcher(Meteor.userId());
|
||||
},
|
||||
|
||||
|
|
@ -99,18 +93,6 @@ BlazeComponent.extendComponent({
|
|||
return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized();
|
||||
},
|
||||
|
||||
cardCollapsed() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && user.profile) {
|
||||
return !!user.profile.cardCollapsed;
|
||||
}
|
||||
if (Users.getPublicCardCollapsed) {
|
||||
const stored = Users.getPublicCardCollapsed();
|
||||
if (typeof stored === 'boolean') return stored;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
presentParentTask() {
|
||||
let result = this.currentBoard.presentParentTask;
|
||||
if (result === null || result === undefined) {
|
||||
|
|
@ -163,9 +145,8 @@ BlazeComponent.extendComponent({
|
|||
* @return is the list id the current list id ?
|
||||
*/
|
||||
isCurrentListId(listId) {
|
||||
const data = this.data();
|
||||
if (!data || typeof data.listId === 'undefined') return false;
|
||||
return data.listId == listId;
|
||||
const ret = this.data().listId == listId;
|
||||
return ret;
|
||||
},
|
||||
|
||||
onRendered() {
|
||||
|
|
@ -315,74 +296,8 @@ BlazeComponent.extendComponent({
|
|||
return [
|
||||
{
|
||||
...events,
|
||||
'click .js-card-collapse-toggle'() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const currentState = user && user.profile ? !!user.profile.cardCollapsed : !!Users.getPublicCardCollapsed();
|
||||
if (user) {
|
||||
Meteor.call('setCardCollapsed', !currentState);
|
||||
} else if (Users.setPublicCardCollapsed) {
|
||||
Users.setPublicCardCollapsed(!currentState);
|
||||
}
|
||||
},
|
||||
'mousedown .js-card-drag-handle'(event) {
|
||||
event.preventDefault();
|
||||
const $card = $(event.target).closest('.card-details');
|
||||
const startX = event.clientX;
|
||||
const startY = event.clientY;
|
||||
const startLeft = $card.offset().left;
|
||||
const startTop = $card.offset().top;
|
||||
|
||||
const onMouseMove = (e) => {
|
||||
const deltaX = e.clientX - startX;
|
||||
const deltaY = e.clientY - startY;
|
||||
$card.css({
|
||||
left: startLeft + deltaX + 'px',
|
||||
top: startTop + deltaY + 'px'
|
||||
});
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
$(document).off('mousemove', onMouseMove);
|
||||
$(document).off('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
$(document).on('mousemove', onMouseMove);
|
||||
$(document).on('mouseup', onMouseUp);
|
||||
},
|
||||
'click .js-close-card-details'() {
|
||||
// Get board ID from either the card data or current board in session
|
||||
const card = this.currentData() || this.data();
|
||||
const boardId = (card && card.boardId) || Utils.getCurrentBoard()._id;
|
||||
const cardId = card && card._id;
|
||||
|
||||
if (boardId) {
|
||||
// In desktop mode, remove from openCards array
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (!isMobile && cardId) {
|
||||
const openCards = Session.get('openCards') || [];
|
||||
const filtered = openCards.filter(id => id !== cardId);
|
||||
Session.set('openCards', filtered);
|
||||
|
||||
// If this was the current card, clear it
|
||||
if (Session.get('currentCard') === cardId) {
|
||||
Session.set('currentCard', null);
|
||||
}
|
||||
// Don't navigate away in desktop mode - just close the card
|
||||
return;
|
||||
}
|
||||
|
||||
// Mobile mode: Clear the current card session to close the card
|
||||
Session.set('currentCard', null);
|
||||
|
||||
// Navigate back to board without card
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (board) {
|
||||
FlowRouter.go('board', {
|
||||
id: board._id,
|
||||
slug: board.slug,
|
||||
});
|
||||
}
|
||||
}
|
||||
Utils.goBoardId(this.data().boardId);
|
||||
},
|
||||
'click .js-copy-link'(event) {
|
||||
event.preventDefault();
|
||||
|
|
@ -396,34 +311,6 @@ BlazeComponent.extendComponent({
|
|||
Meteor.call('changeDateFormat', dateFormat);
|
||||
},
|
||||
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
|
||||
// Mobile: switch to desktop popup view (maximize)
|
||||
'click .js-mobile-switch-to-desktop'(event) {
|
||||
event.preventDefault();
|
||||
// Switch global mode to desktop so the card appears as desktop popup
|
||||
Utils.setMobileMode(false);
|
||||
},
|
||||
'click .js-card-zoom-in'(event) {
|
||||
event.preventDefault();
|
||||
const current = Utils.getCardZoom();
|
||||
const newZoom = Math.min(3.0, current + 0.1);
|
||||
Utils.setCardZoom(newZoom);
|
||||
},
|
||||
'click .js-card-zoom-out'(event) {
|
||||
event.preventDefault();
|
||||
const current = Utils.getCardZoom();
|
||||
const newZoom = Math.max(0.5, current - 0.1);
|
||||
Utils.setCardZoom(newZoom);
|
||||
},
|
||||
'click .js-card-mobile-desktop-toggle'(event) {
|
||||
event.preventDefault();
|
||||
const currentMode = Utils.getMobileMode();
|
||||
Utils.setMobileMode(!currentMode);
|
||||
},
|
||||
'click .js-card-mobile-desktop-toggle'(event) {
|
||||
event.preventDefault();
|
||||
const currentMode = Utils.getMobileMode();
|
||||
Utils.setMobileMode(!currentMode);
|
||||
},
|
||||
'submit .js-card-description'(event) {
|
||||
event.preventDefault();
|
||||
const description = this.currentComponent().getValue();
|
||||
|
|
@ -796,7 +683,6 @@ Template.editCardSortOrderForm.onRendered(function () {
|
|||
|
||||
Template.cardDetailsActionsPopup.helpers({
|
||||
isWatching() {
|
||||
if (!this || typeof this.findWatcher !== 'function') return false;
|
||||
return this.findWatcher(Meteor.userId());
|
||||
},
|
||||
|
||||
|
|
@ -950,42 +836,26 @@ Template.editCardAssignerForm.events({
|
|||
});
|
||||
|
||||
/** Move Card Dialog */
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
(class extends DialogWithBoardSwimlaneList {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(cardId, options) {
|
||||
setDone(boardId, swimlaneId, listId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
||||
const card = this.data();
|
||||
let sortIndex = 0;
|
||||
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
const position = this.$('input[name="position"]:checked').val();
|
||||
if (position === 'above') {
|
||||
sortIndex = targetCard.sort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetCard.sort + 0.5;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no card selected, move to end
|
||||
sortIndex = card.getMaxSort(options.listId, options.swimlaneId) + 1;
|
||||
}
|
||||
|
||||
card.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
||||
const minOrder = card.getMinSort(listId, swimlaneId);
|
||||
card.move(boardId, swimlaneId, listId, minOrder - 1);
|
||||
}
|
||||
}).register('moveCardPopup');
|
||||
|
||||
/** Copy Card Dialog */
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
(class extends DialogWithBoardSwimlaneList {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(cardId, options) {
|
||||
setDone(boardId, swimlaneId, listId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
||||
const card = this.data();
|
||||
|
||||
|
|
@ -994,30 +864,8 @@ Template.editCardAssignerForm.events({
|
|||
const title = textarea.val().trim();
|
||||
|
||||
if (title) {
|
||||
const newCardId = Meteor.call('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, true, {title: title});
|
||||
|
||||
// Position the copied card
|
||||
if (newCardId) {
|
||||
const newCard = ReactiveCache.getCard(newCardId);
|
||||
let sortIndex = 0;
|
||||
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
const position = this.$('input[name="position"]:checked').val();
|
||||
if (position === 'above') {
|
||||
sortIndex = targetCard.sort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetCard.sort + 0.5;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no card selected, copy to end
|
||||
sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1;
|
||||
}
|
||||
|
||||
newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
||||
}
|
||||
// insert new card to the top of new list
|
||||
const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, listId, true, {title: title});
|
||||
|
||||
// In case the filter is active we need to add the newly inserted card in
|
||||
// the list of exceptions -- cards that are not filtered. Otherwise the
|
||||
|
|
@ -1029,12 +877,12 @@ Template.editCardAssignerForm.events({
|
|||
}).register('copyCardPopup');
|
||||
|
||||
/** Convert Checklist-Item to card dialog */
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
(class extends DialogWithBoardSwimlaneList {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(cardId, options) {
|
||||
setDone(boardId, swimlaneId, listId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
||||
const card = this.data();
|
||||
|
||||
|
|
@ -1044,29 +892,14 @@ Template.editCardAssignerForm.events({
|
|||
if (title) {
|
||||
const _id = Cards.insert({
|
||||
title: title,
|
||||
listId: options.listId,
|
||||
boardId: options.boardId,
|
||||
swimlaneId: options.swimlaneId,
|
||||
listId: listId,
|
||||
boardId: boardId,
|
||||
swimlaneId: swimlaneId,
|
||||
sort: 0,
|
||||
});
|
||||
const newCard = ReactiveCache.getCard(_id);
|
||||
|
||||
let sortIndex = 0;
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
const position = this.$('input[name="position"]:checked').val();
|
||||
if (position === 'above') {
|
||||
sortIndex = targetCard.sort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetCard.sort + 0.5;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1;
|
||||
}
|
||||
|
||||
newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
||||
const card = ReactiveCache.getCard(_id);
|
||||
const minOrder = card.getMinSort();
|
||||
card.move(card.boardId, card.swimlaneId, card.listId, minOrder - 1);
|
||||
|
||||
Filter.addException(_id);
|
||||
}
|
||||
|
|
@ -1074,12 +907,12 @@ Template.editCardAssignerForm.events({
|
|||
}).register('convertChecklistItemToCardPopup');
|
||||
|
||||
/** Copy many cards dialog */
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
(class extends DialogWithBoardSwimlaneList {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(cardId, options) {
|
||||
setDone(boardId, swimlaneId, listId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
||||
const card = this.data();
|
||||
|
||||
|
|
@ -1089,29 +922,7 @@ Template.editCardAssignerForm.events({
|
|||
if (title) {
|
||||
const titleList = JSON.parse(title);
|
||||
for (const obj of titleList) {
|
||||
const newCardId = Meteor.call('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, false, {title: obj.title, description: obj.description});
|
||||
|
||||
// Position the copied card
|
||||
if (newCardId) {
|
||||
const newCard = ReactiveCache.getCard(newCardId);
|
||||
let sortIndex = 0;
|
||||
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
const position = this.$('input[name="position"]:checked').val();
|
||||
if (position === 'above') {
|
||||
sortIndex = targetCard.sort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetCard.sort + 0.5;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1;
|
||||
}
|
||||
|
||||
newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
||||
}
|
||||
const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, listId, false, {title: obj.title, description: obj.description});
|
||||
|
||||
// In case the filter is active we need to add the newly inserted card in
|
||||
// the list of exceptions -- cards that are not filtered. Otherwise the
|
||||
|
|
@ -1161,51 +972,6 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
}).register('setCardColorPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentColor = new ReactiveVar(null);
|
||||
},
|
||||
|
||||
colors() {
|
||||
return ALLOWED_COLORS.map((color) => ({ color, name: '' }));
|
||||
},
|
||||
|
||||
isSelected(color) {
|
||||
return this.currentColor.get() === color;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-palette-color'(event) {
|
||||
// Extract color from class name like "card-details-red"
|
||||
const classes = $(event.currentTarget).attr('class').split(' ');
|
||||
const colorClass = classes.find(cls => cls.startsWith('card-details-'));
|
||||
const color = colorClass ? colorClass.replace('card-details-', '') : null;
|
||||
this.currentColor.set(color);
|
||||
},
|
||||
'click .js-submit'(event) {
|
||||
event.preventDefault();
|
||||
const color = this.currentColor.get();
|
||||
// Use MultiSelection to get selected cards and set color on each
|
||||
ReactiveCache.getCards(MultiSelection.getMongoSelector()).forEach(card => {
|
||||
card.setColor(color);
|
||||
});
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-remove-color'(event) {
|
||||
event.preventDefault();
|
||||
// Use MultiSelection to get selected cards and remove color from each
|
||||
ReactiveCache.getCards(MultiSelection.getMongoSelector()).forEach(card => {
|
||||
card.setColor(null);
|
||||
});
|
||||
Popup.back();
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('setSelectionColorPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentCard = this.currentData();
|
||||
|
|
|
|||
|
|
@ -37,23 +37,14 @@ textarea.js-edit-checklist-item {
|
|||
.checklist-progress-bar-container .checklist-progress-bar {
|
||||
width: 80%;
|
||||
height: 10px;
|
||||
background-color: #d6ebff !important;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
|
||||
color: #fff !important;
|
||||
background-color: #3cb500 !important;
|
||||
background-color: #2196f3 !important;
|
||||
padding: 0.01em 16px;
|
||||
border-radius: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
/* Grey progress bar when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-bar {
|
||||
background-color: #d9d9d9;
|
||||
}
|
||||
body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
|
||||
background-color: #7a7a7a !important;
|
||||
}
|
||||
.checklist-title {
|
||||
padding: 10px;
|
||||
}
|
||||
|
|
@ -76,14 +67,14 @@ body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-ba
|
|||
.checklist-title .checklist-stat.is-finished {
|
||||
color: #3cb500;
|
||||
}
|
||||
.checklist-title span.checklist-handle {
|
||||
.checklist-title span.fa.checklist-handle {
|
||||
padding-right: 20px;
|
||||
padding-top: 3px;
|
||||
float: left;
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
.checklist-title span.fa.checklist-handle.fa-arrows::before {
|
||||
content: "↕️" !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
#card-details-overlay {
|
||||
top: 0;
|
||||
|
|
@ -114,25 +105,6 @@ body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-ba
|
|||
height: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* iPhone mobile: larger checklist titles and more spacing between items */
|
||||
body.mobile-mode.iphone-device .checklist-title .title {
|
||||
font-size: 1.3em !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
body.mobile-mode.iphone-device .checklist-item {
|
||||
margin-top: 12px !important;
|
||||
margin-bottom: 8px !important;
|
||||
padding: 8px 4px !important;
|
||||
min-height: 44px; /* iOS recommended touch target size */
|
||||
}
|
||||
|
||||
body.mobile-mode.iphone-device .checklist-item span.checklistitem-handle {
|
||||
font-size: 1.5em !important;
|
||||
padding-right: 15px !important;
|
||||
width: 1.5em !important;
|
||||
}
|
||||
.checklist-item.is-checked.invisible {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
|
|
@ -162,27 +134,6 @@ body.mobile-mode.iphone-device .checklist-item span.checklistitem-handle {
|
|||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
}
|
||||
/* Unicode checkbox icons styling */
|
||||
.checklist-item .check-box-unicode,
|
||||
.cardCustomField-checkbox .check-box-unicode {
|
||||
font-size: 1.3em;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .checklist-item .check-box.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
body.grey-icons-enabled .checklist-item .check-box-unicode,
|
||||
body.grey-icons-enabled .cardCustomField-checkbox .check-box-unicode {
|
||||
filter: grayscale(100%);
|
||||
-webkit-filter: grayscale(100%);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.checklist-item .item-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
|
@ -197,14 +148,13 @@ body.grey-icons-enabled .cardCustomField-checkbox .check-box-unicode {
|
|||
word-wrap: break-word;
|
||||
max-width: 420px;
|
||||
}
|
||||
.checklist-item span.checklistitem-handle {
|
||||
.checklist-item span.fa.checklistitem-handle {
|
||||
padding-top: 2px;
|
||||
padding-right: 10px;
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
.checklist-item span.fa.checklistitem-handle.fa-arrows::before {
|
||||
content: "↕️" !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
.js-delete-checklist-item,
|
||||
.js-convert-checklist-item-to-card {
|
||||
|
|
|
|||
|
|
@ -9,10 +9,19 @@ template(name="checklists")
|
|||
else
|
||||
a.add-checklist-top.js-open-inlined-form(title="{{_ 'add-checklist'}}")
|
||||
| ➕
|
||||
if currentUser.isBoardMember
|
||||
.material-toggle-switch(title="{{_ 'hide-finished-checklist'}}")
|
||||
//span.toggle-switch-title
|
||||
if card.hideFinishedChecklistIfItemsAreHidden
|
||||
input.toggle-switch(type="checkbox" id="toggleHideFinishedChecklist" checked="checked")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleHideFinishedChecklist")
|
||||
label.toggle-label(for="toggleHideFinishedChecklist")
|
||||
|
||||
.card-checklist-items
|
||||
each checklist in checklists
|
||||
+checklistDetail(checklist = checklist card = card)
|
||||
if checklist.showChecklist card.hideFinishedChecklistIfItemsAreHidden
|
||||
+checklistDetail(checklist = checklist card = card)
|
||||
|
||||
if canModifyCard
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId)
|
||||
|
|
@ -29,12 +38,12 @@ template(name="checklistDetail")
|
|||
.checklist-title
|
||||
span
|
||||
if canModifyCard
|
||||
a.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}") ☰
|
||||
a.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
|
||||
|
||||
if canModifyCard
|
||||
h4.title.js-open-inlined-form.is-editable
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
span.checklist-handle(title="{{_ 'dragChecklist'}}") ↕️
|
||||
span.fa.checklist-handle(class="fa-arrows" title="{{_ 'dragChecklist'}}")
|
||||
+viewer
|
||||
= checklist.title
|
||||
else
|
||||
|
|
@ -53,10 +62,6 @@ template(name="checklistDeletePopup")
|
|||
p {{_ 'confirm-checklist-delete-popup'}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="checklistItemDeletePopup")
|
||||
p {{_ 'confirm-checklist-delete-popup'}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="addChecklistItemForm")
|
||||
a(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
|
|
@ -90,13 +95,10 @@ template(name="editChecklistItemForm")
|
|||
| ❌
|
||||
span(title=createdAt) {{ moment createdAt }}
|
||||
if canModifyCard
|
||||
if $eq type 'item'
|
||||
a.js-delete-checklist-item {{_ "delete"}}...
|
||||
a.js-convert-checklist-item-to-card
|
||||
| 📋
|
||||
| {{_ 'convertChecklistItemToCardPopup-title'}}
|
||||
else
|
||||
a.js-delete-checklist {{_ "delete"}}...
|
||||
a.js-delete-checklist-item {{_ "delete"}}...
|
||||
a.js-convert-checklist-item-to-card
|
||||
| 📋
|
||||
| {{_ 'convertChecklistItemToCardPopup-title'}}
|
||||
|
||||
template(name="checklistItems")
|
||||
if checklist.items.length
|
||||
|
|
@ -123,13 +125,14 @@ template(name='checklistItemDetail')
|
|||
.js-checklist-item.checklist-item(class="{{#if item.isFinished }}is-checked{{#if checklist.hideCheckedChecklistItems}} invisible{{/if}}{{/if}}{{#if checklist.hideAllChecklistItems}} is-checked invisible{{/if}}"
|
||||
role="checkbox" aria-checked="{{#if item.isFinished }}true{{else}}false{{/if}}" tabindex="0")
|
||||
if canModifyCard
|
||||
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
|
||||
span.checklistitem-handle(title="{{_ 'dragChecklistItem'}}") ↕️
|
||||
.check-box-container
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
|
||||
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
else
|
||||
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
|
||||
.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
|
|
@ -164,62 +167,34 @@ template(name="checklistActionsPopup")
|
|||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleHideAllChecklistItems_{{checklist._id}}")
|
||||
label.toggle-label(for="toggleHideAllChecklistItems_{{checklist._id}}")
|
||||
a.js-toggle-show-checklist-at-minicard
|
||||
| 📋
|
||||
| {{_ "showChecklistAtMinicard"}} ...
|
||||
.material-toggle-switch(title="{{_ 'showChecklistAtMinicard'}}")
|
||||
if checklist.showChecklistAtMinicard
|
||||
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}" checked="checked")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}")
|
||||
label.toggle-label(for="toggleShowChecklistAtMinicard_{{checklist._id}}")
|
||||
|
||||
template(name="copyChecklistPopup")
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'card'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
+copyAndMoveChecklist
|
||||
|
||||
template(name="moveChecklistPopup")
|
||||
+copyAndMoveChecklist
|
||||
|
||||
template(name="copyAndMoveChecklist")
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
label {{_ 'card'}}:
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ BlazeComponent.extendComponent({
|
|||
$(self.itemsDom).sortable('option', 'disabled', !userIsMember());
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$(self.itemsDom).sortable({
|
||||
handle: 'span.checklistitem-handle',
|
||||
handle: 'span.fa.checklistitem-handle',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -157,6 +157,14 @@ BlazeComponent.extendComponent({
|
|||
textarea.focus();
|
||||
},
|
||||
|
||||
deleteItem() {
|
||||
const checklist = this.currentData().checklist;
|
||||
const item = this.currentData().item;
|
||||
if (checklist && item && item._id) {
|
||||
ChecklistItems.remove(item._id);
|
||||
}
|
||||
},
|
||||
|
||||
editChecklist(event) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-edit-checklist-item');
|
||||
|
|
@ -208,28 +216,14 @@ BlazeComponent.extendComponent({
|
|||
'submit .js-add-checklist-item': this.addChecklistItem,
|
||||
'submit .js-edit-checklist-item': this.editChecklistItem,
|
||||
'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
|
||||
'click .js-delete-checklist-item'(event) {
|
||||
const item = this.currentData().item;
|
||||
const confirmFunc = Popup.afterConfirm('checklistItemDelete', function () {
|
||||
if (item && item._id) {
|
||||
ChecklistItems.remove(item._id);
|
||||
}
|
||||
});
|
||||
confirmFunc.call(this, event);
|
||||
},
|
||||
'click .js-delete-checklist'(event) {
|
||||
const checklist = this.currentData().checklist;
|
||||
const confirmFunc = Popup.afterConfirm('checklistDelete', function () {
|
||||
Popup.back(2);
|
||||
if (checklist && checklist._id) {
|
||||
Checklists.remove(checklist._id);
|
||||
}
|
||||
});
|
||||
confirmFunc.call(this, event);
|
||||
},
|
||||
'click .js-delete-checklist-item': this.deleteItem,
|
||||
'focus .js-add-checklist-item': this.focusChecklistItem,
|
||||
// add and delete checklist / checklist-item
|
||||
'click .js-open-inlined-form': this.closeAllInlinedForms,
|
||||
'click #toggleHideFinishedChecklist'(event) {
|
||||
event.preventDefault();
|
||||
this.data().card.toggleHideFinishedChecklist();
|
||||
},
|
||||
keydown: this.pressKey,
|
||||
},
|
||||
];
|
||||
|
|
@ -281,8 +275,8 @@ BlazeComponent.extendComponent({
|
|||
Template.checklists.helpers({
|
||||
checklists() {
|
||||
const card = ReactiveCache.getCard(this.cardId);
|
||||
if (!card || typeof card.checklists !== 'function') return [];
|
||||
return card.checklists();
|
||||
const ret = card.checklists();
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -309,16 +303,13 @@ BlazeComponent.extendComponent({
|
|||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-delete-checklist'(event) {
|
||||
const checklist = this.data().checklist;
|
||||
const confirmFunc = Popup.afterConfirm('checklistDelete', function () {
|
||||
Popup.back(2);
|
||||
if (checklist && checklist._id) {
|
||||
Checklists.remove(checklist._id);
|
||||
}
|
||||
});
|
||||
confirmFunc.call(this, event);
|
||||
},
|
||||
'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () {
|
||||
Popup.back(2);
|
||||
const checklist = this.checklist;
|
||||
if (checklist && checklist._id) {
|
||||
Checklists.remove(checklist._id);
|
||||
}
|
||||
}),
|
||||
'click .js-move-checklist': Popup.open('moveChecklist'),
|
||||
'click .js-copy-checklist': Popup.open('copyChecklist'),
|
||||
'click .js-hide-checked-checklist-items'(event) {
|
||||
|
|
@ -331,12 +322,6 @@ BlazeComponent.extendComponent({
|
|||
this.data().checklist.toggleHideAllChecklistItems();
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-toggle-show-checklist-at-minicard'(event) {
|
||||
event.preventDefault();
|
||||
const checklist = this.data().checklist;
|
||||
checklist.toggleShowChecklistAtMinicard();
|
||||
Popup.back();
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -375,7 +360,6 @@ BlazeComponent.extendComponent({
|
|||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-checklist-item .check-box-unicode': this.toggleItem,
|
||||
'click .js-checklist-item .check-box-container': this.toggleItem,
|
||||
},
|
||||
];
|
||||
|
|
@ -390,12 +374,7 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
|
||||
const checklist = this.data().checklist;
|
||||
Meteor.call('moveChecklist', checklist._id, cardId, (error) => {
|
||||
if (error) {
|
||||
console.error('Error moving checklist:', error);
|
||||
}
|
||||
});
|
||||
this.data().checklist.move(cardId);
|
||||
}
|
||||
}).register('moveChecklistPopup');
|
||||
|
||||
|
|
|
|||
|
|
@ -223,13 +223,9 @@
|
|||
.card-label-edit-button:hover {
|
||||
background: #dbdbdb;
|
||||
}
|
||||
ul.edit-labels-pop-over span.label-handle {
|
||||
ul.edit-labels-pop-over span.fa.label-handle {
|
||||
padding-right: 10px;
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
ul.edit-labels-pop-over span.label-handle + .card-label {
|
||||
ul.edit-labels-pop-over span.fa.label-handle + .card-label {
|
||||
max-width: 180px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ template(name="cardLabelsPopup")
|
|||
a.card-label-edit-button.js-edit-label
|
||||
| ✏️
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
span.label-handle(title="{{_ 'dragLabel'}}") ↕️
|
||||
span.fa.label-handle(class="fa-arrows" title="{{_ 'dragLabel'}}")
|
||||
span.card-label.card-label-selectable.js-select-label.card-label-wrapper(class="card-label-{{color}}"
|
||||
class="{{# if isLabelSelected ../_id }}active{{/if}}")
|
||||
+viewer
|
||||
|
|
|
|||
|
|
@ -142,12 +142,9 @@
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
.minicard .handle .drag-handle {
|
||||
.minicard .handle .fa-arrows {
|
||||
font-size: clamp(16px, 3vw, 20px);
|
||||
color: #ccc;
|
||||
display: inline-block;
|
||||
width: 1.4em;
|
||||
text-align: center;
|
||||
}
|
||||
.minicard .minicard-title .card-number {
|
||||
color: #b3b3b3;
|
||||
|
|
@ -300,6 +297,19 @@
|
|||
background-color: #1976d2 !important;
|
||||
}
|
||||
|
||||
/* Font Awesome icons in minicard dates */
|
||||
.minicard .card-date i.fa {
|
||||
margin-right: 0.3vw;
|
||||
font-size: 0.9em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Font Awesome icons in minicard spent time */
|
||||
.minicard .card-time i.fa {
|
||||
margin-right: 0.3vw;
|
||||
font-size: 0.9em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.minicard .badges {
|
||||
float: left;
|
||||
margin-top: 1vh;
|
||||
|
|
@ -731,80 +741,7 @@
|
|||
gap: 0.3vw;
|
||||
}
|
||||
|
||||
/* Checklist display on minicard */
|
||||
.minicard-checklist {
|
||||
width: 100%;
|
||||
margin-top: 0.5vh;
|
||||
margin-bottom: 0.5vh;
|
||||
padding: 0.3vh 0.5vw;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 0.3vw;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.3vh;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-title {
|
||||
.minicard-list-name i.fa {
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
color: #4d4d4d;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-menu {
|
||||
font-size: 1.2em;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
padding: 0 0.3vw;
|
||||
border-radius: 0.2vw;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-menu:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item {
|
||||
font-size: 0.75em;
|
||||
color: #666;
|
||||
margin-bottom: 0.2vh;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.3vw;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
padding: 0.2vh 0;
|
||||
border-radius: 0.2vw;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item.is-checked {
|
||||
text-decoration: line-through;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item .check-box-unicode {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.8em;
|
||||
margin-top: 0.1vh;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item:hover .check-box-unicode {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item .item-title {
|
||||
flex: 1;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,6 +167,10 @@ template(name="minicard")
|
|||
.badge
|
||||
span.badge-icon 📎
|
||||
span.badge-text= attachments.length
|
||||
if checklists.length
|
||||
.badge(class="{{#if checklistFinished}}is-finished{{/if}}")
|
||||
span.badge-icon ☑️
|
||||
span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
|
||||
if allSubtasks.count
|
||||
.badge
|
||||
span.badge-icon 🌐
|
||||
|
|
@ -177,9 +181,6 @@ template(name="minicard")
|
|||
.badge
|
||||
span.badge-icon 🔢
|
||||
span.badge-text.check-list-sort {{ sort }}
|
||||
if shouldShowChecklistAtMinicard
|
||||
each shouldShowChecklistAtMinicard
|
||||
+minicardChecklist(checklist=. card=..)
|
||||
if currentBoard.allowsDescriptionTextOnMinicard
|
||||
if getDescription
|
||||
.minicard-description
|
||||
|
|
@ -201,12 +202,55 @@ template(name="editCardSortOrderPopup")
|
|||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-sort-popup(type="submit") {{_ 'save'}}
|
||||
|
||||
template(name="minicardChecklist")
|
||||
.minicard-checklist
|
||||
.checklist-header
|
||||
.checklist-title= checklist.title
|
||||
if canModifyCard
|
||||
a.checklist-menu.js-open-checklist-menu(title="{{_ 'checklistActionsPopup-title'}}") ☰
|
||||
each visibleItems
|
||||
+checklistItemDetail(item = . checklist = checklist card = card)
|
||||
template(name="minicardDetailsActionsPopup")
|
||||
ul.pop-over-list
|
||||
if canModifyCard
|
||||
li
|
||||
a.js-move-card
|
||||
| ➡️
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
li
|
||||
a.js-copy-card
|
||||
| 📋
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
hr
|
||||
li
|
||||
a.js-archive
|
||||
| ➡️
|
||||
| 📦
|
||||
| {{_ 'archive-card'}}
|
||||
hr
|
||||
li
|
||||
a.js-move-card-to-top
|
||||
| ⬆️
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
| ⬇️
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
li
|
||||
a.js-add-labels
|
||||
| 🏷️
|
||||
| {{_ 'card-edit-labels'}}
|
||||
li
|
||||
a.js-due-date
|
||||
| 📥
|
||||
| {{_ 'editCardDueDatePopup-title'}}
|
||||
li
|
||||
a.js-set-card-color
|
||||
| 🎨
|
||||
| {{_ 'setCardColorPopup-title'}}
|
||||
li
|
||||
a.js-link
|
||||
| 🔗
|
||||
| {{_ 'link-card'}}
|
||||
li
|
||||
a.js-toggle-watch-card
|
||||
if isWatching
|
||||
| 👁️
|
||||
| {{_ 'unwatch'}}
|
||||
else
|
||||
| 👁️-slash
|
||||
| {{_ 'watch'}}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,13 +91,6 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
},
|
||||
|
||||
toggleChecklistItem() {
|
||||
const item = this.currentData();
|
||||
if (item && item._id) {
|
||||
item.toggleItem();
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
|
|
@ -115,7 +108,7 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
|
||||
'click .minicard-labels' : this.cardLabelsPopup,
|
||||
'click .js-open-minicard-details-menu': Popup.open('cardDetailsActions'),
|
||||
'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
|
||||
// Drag and drop file upload handlers
|
||||
'dragover .minicard'(event) {
|
||||
// Only prevent default for file drags to avoid interfering with sortable
|
||||
|
|
@ -177,43 +170,6 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
}).register('minicard');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'minicardChecklist';
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-open-checklist-menu'(event) {
|
||||
const data = this.currentData();
|
||||
const checklist = data.checklist || data;
|
||||
const card = data.card || this.data();
|
||||
const context = { currentData: () => ({ checklist, card }) };
|
||||
Popup.open('checklistActions').call(context, event);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
visibleItems() {
|
||||
const checklist = this.currentData().checklist || this.currentData();
|
||||
const items = checklist.items();
|
||||
|
||||
return items.filter(item => {
|
||||
// Hide finished items if hideCheckedChecklistItems is true
|
||||
if (item.isFinished && checklist.hideCheckedChecklistItems) {
|
||||
return false;
|
||||
}
|
||||
// Hide all items if hideAllChecklistItems is true
|
||||
if (checklist.hideAllChecklistItems) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
}).register('minicardChecklist');
|
||||
|
||||
Template.minicard.helpers({
|
||||
hiddenMinicardLabelText() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
|
|
@ -253,29 +209,9 @@ Template.minicard.helpers({
|
|||
// Show list name if either:
|
||||
// 1. Board-wide setting is enabled, OR
|
||||
// 2. This specific card has the setting enabled
|
||||
const currentBoard = this.board();
|
||||
const currentBoard = this.currentBoard;
|
||||
if (!currentBoard) return false;
|
||||
return currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
|
||||
},
|
||||
|
||||
shouldShowChecklistAtMinicard() {
|
||||
// Return checklists that should be shown on minicard
|
||||
const currentBoard = this.board();
|
||||
if (!currentBoard) return [];
|
||||
|
||||
const checklists = this.checklists();
|
||||
const visibleChecklists = [];
|
||||
|
||||
checklists.forEach(checklist => {
|
||||
// Show checklist if either:
|
||||
// 1. Board-wide setting is enabled, OR
|
||||
// 2. This specific checklist has the setting enabled
|
||||
if (currentBoard.allowsChecklistAtMinicard || checklist.showChecklistAtMinicard) {
|
||||
visibleChecklists.push(checklist);
|
||||
}
|
||||
});
|
||||
|
||||
return visibleChecklists;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -306,7 +242,7 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
}).register('editCardSortOrderPopup');
|
||||
|
||||
Template.cardDetailsActionsPopup.events({
|
||||
Template.minicardDetailsActionsPopup.events({
|
||||
'click .js-due-date': Popup.open('editCardDueDate'),
|
||||
'click .js-move-card': Popup.open('moveCard'),
|
||||
'click .js-copy-card': Popup.open('copyCard'),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ template(name="resultCard")
|
|||
= getBoard.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| {{_ 'no-name'}}
|
||||
| NULL
|
||||
if getBoard.archived
|
||||
| 📦
|
||||
li.result-card-context.result-card-context-separator
|
||||
|
|
@ -25,7 +25,7 @@ template(name="resultCard")
|
|||
= getSwimlane.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| {{_ 'no-name'}}
|
||||
| NULL
|
||||
if getSwimlane.archived
|
||||
| 📦
|
||||
li.result-card-context.result-card-context-separator
|
||||
|
|
@ -39,6 +39,6 @@ template(name="resultCard")
|
|||
= getList.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| {{_ 'no-name'}}
|
||||
| NULL
|
||||
if getList.archived
|
||||
| 📦
|
||||
|
|
|
|||
|
|
@ -87,15 +87,6 @@ textarea.js-edit-subtask-item {
|
|||
top: 0;
|
||||
bottom: -600px;
|
||||
right: 0;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
/* Fix for mobile Safari: ensure this doesn't block card interaction */
|
||||
@media screen and (max-width: 800px) {
|
||||
#card-details-overlay {
|
||||
z-index: 15;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
.subtasks {
|
||||
background: #f7f7f7;
|
||||
|
|
@ -136,25 +127,6 @@ textarea.js-edit-subtask-item {
|
|||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
}
|
||||
/* Unicode checkbox icons styling */
|
||||
.subtasks-item .check-box-unicode {
|
||||
font-size: 1.3em;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .subtasks-item .check-box.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
body.grey-icons-enabled .subtasks-item .check-box-unicode {
|
||||
filter: grayscale(100%);
|
||||
-webkit-filter: grayscale(100%);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.subtasks-item .item-title {
|
||||
flex: 1;
|
||||
padding-left: 10px;
|
||||
|
|
|
|||
|
|
@ -74,12 +74,12 @@ template(name="subtasksItems")
|
|||
template(name='subtaskItemDetail')
|
||||
.js-subtasks-item.subtasks-item
|
||||
if canModifyCard
|
||||
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
else
|
||||
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
|
||||
.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
|
|
|
|||
|
|
@ -104,19 +104,7 @@ BlazeComponent.extendComponent({
|
|||
}).register('subtasks');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
toggleItem() {
|
||||
const item = this.currentData().item;
|
||||
if (item && item._id) {
|
||||
item.toggleItem();
|
||||
}
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-subtasks-item .check-box-unicode': this.toggleItem,
|
||||
},
|
||||
];
|
||||
},
|
||||
// ...
|
||||
}).register('subtaskItemDetail');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
|
|
|
|||
|
|
@ -2,17 +2,19 @@
|
|||
<div class="original-position-info">
|
||||
{{#if isLoading}}
|
||||
<div class="original-position-loading">
|
||||
⏳ Loading original position...
|
||||
<i class="fa fa-spinner fa-spin"></i> Loading original position...
|
||||
</div>
|
||||
{{else if showOriginalPosition}}
|
||||
<div class="original-position-details">
|
||||
{{#if hasMovedFromOriginal}}
|
||||
<div class="original-position-moved">
|
||||
<span class="original-position-text">ℹ️ {{getOriginalPositionDescription}}</span>
|
||||
<i class="fa fa-info-circle"></i>
|
||||
<span class="original-position-text">{{getOriginalPositionDescription}}</span>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="original-position-unchanged">
|
||||
<span class="original-position-text">✅ In original position</span>
|
||||
<i class="fa fa-check-circle"></i>
|
||||
<span class="original-position-text">In original position</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
|
|
|||
|
|
@ -315,18 +315,11 @@ textarea::-moz-placeholder {
|
|||
margin-right: 6px;
|
||||
border-top: 2px solid transparent;
|
||||
border-left: 2px solid transparent;
|
||||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
transform: rotate(40deg);
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
transform-origin: 100% 100%;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .materialCheckBox.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
.button-link {
|
||||
background: #fff;
|
||||
background: linear-gradient(#fff, #f5f5f5);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 .. .}}
|
||||
|
||||
|
|
@ -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 });
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
template(name="ganttCard")
|
||||
+cardDetails(selectedCard)
|
||||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
template(name="importHeaderBar")
|
||||
h1
|
||||
a.back-btn(href="{{pathFor 'home'}}")
|
||||
| ⬅️
|
||||
i.fa.fa-chevron-left
|
||||
| {{_ title}}
|
||||
|
||||
template(name="import")
|
||||
|
|
@ -36,7 +36,7 @@ template(name="importMapMembers")
|
|||
+userAvatar(userId=wekanId)
|
||||
else
|
||||
a.member.add-member
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
//-
|
||||
Due to the way the flewbox layout is working, we need to set some
|
||||
invisible items so that the last row items have a consistent width.
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ body.list-resizing-active * {
|
|||
margin: 0 auto;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
margin: 0 auto 0 auto;
|
||||
margin: 0 auto 20px auto;
|
||||
z-index: 10;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
|
|
@ -290,12 +290,6 @@ body.list-resizing-active * {
|
|||
display: block;
|
||||
width: fit-content;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-header-handle {
|
||||
position: absolute !important;
|
||||
top: 30px !important;
|
||||
right: 1.5vw !important;
|
||||
z-index: 15 !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
|
|
@ -303,6 +297,7 @@ body.list-resizing-active * {
|
|||
position: relative !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
text-align: left;
|
||||
overflow: visible;
|
||||
|
|
@ -313,15 +308,15 @@ body.list-resizing-active * {
|
|||
color: #333;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ddd;
|
||||
padding: 0;
|
||||
padding: 8px 4px;
|
||||
border-radius: 4px;
|
||||
margin: 0;
|
||||
width: 100vh;
|
||||
height: 30px;
|
||||
margin: 0 auto;
|
||||
width: 25vh;
|
||||
height: 60vh;
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateY(calc(-50% + 20px)) rotate(0deg);
|
||||
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
|
||||
z-index: 10;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
|
|
@ -373,18 +368,6 @@ body.list-resizing-active * {
|
|||
text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
/* Sum badge shown before list title */
|
||||
.list-header .list-sum-badge {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: #8c8c8c;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.list-rotated {
|
||||
width: 1.3vw;
|
||||
height: 35vh;
|
||||
|
|
@ -420,42 +403,22 @@ body.list-resizing-active * {
|
|||
color: #a6a6a6;
|
||||
margin-right: 15px;
|
||||
}
|
||||
/* List header collapse button styling */
|
||||
.list-header .list-header-collapse-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.list-header .js-collapse {
|
||||
color: #a6a6a6;
|
||||
margin-right: 15px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 5px 8px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: #f5f5f5;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
text-decoration: none;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
.list-header .js-collapse:hover {
|
||||
background-color: transparent;
|
||||
background-color: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.list-header .list-header-collapse-container > div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
display: inline-block !important;
|
||||
visibility: visible !important;
|
||||
|
|
@ -484,18 +447,17 @@ body.list-resizing-active * {
|
|||
position: relative !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
width: 100vh;
|
||||
width: 15vh;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
line-height: 1.2;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
padding: 8px 4px;
|
||||
margin: 0 auto;
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateY(calc(-50% + 120px)) rotate(0deg);
|
||||
text-align: center;
|
||||
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
|
||||
text-align: left;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
display: block !important;
|
||||
|
|
@ -525,18 +487,17 @@ body.list-resizing-active * {
|
|||
position: relative !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
width: 100vh;
|
||||
width: 15vh;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
line-height: 1.2;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
padding: 8px 4px;
|
||||
margin: 0 auto;
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateY(calc(-50% + 120px)) rotate(0deg);
|
||||
text-align: center;
|
||||
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
|
||||
text-align: left;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
display: block !important;
|
||||
|
|
@ -566,17 +527,16 @@ body.list-resizing-active * {
|
|||
position: relative !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
width: 100vh;
|
||||
width: 15vh;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
line-height: 1.2;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
padding: 8px 4px;
|
||||
margin: 0 auto;
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateY(calc(-50% + 40px)) rotate(0deg);
|
||||
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
|
||||
text-align: left;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
|
|
@ -790,9 +750,6 @@ body.list-resizing-active * {
|
|||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
align-self: start;
|
||||
text-align: left;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
|
@ -1007,9 +964,6 @@ body.list-resizing-active * {
|
|||
grid-row: 2 !important;
|
||||
grid-column: 2 !important;
|
||||
align-self: start !important;
|
||||
text-align: left !important;
|
||||
padding-left: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
|
|
@ -1081,23 +1035,6 @@ body.list-resizing-active * {
|
|||
grid-row: 1/3 !important;
|
||||
grid-column: 1 !important;
|
||||
}
|
||||
|
||||
/* Allow long list titles to expand on desktop (non-mobile, non-collapsed) */
|
||||
.list:not(.mobile-view):not(.list-collapsed) .list-header {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.list:not(.mobile-view):not(.list-collapsed) .list-header .list-header-name {
|
||||
/* Permit wrapping and full visibility */
|
||||
white-space: normal !important;
|
||||
overflow: visible !important;
|
||||
text-overflow: clip !important;
|
||||
display: inline-block !important;
|
||||
/* Reserve space for right-side controls (menu, handle, count) */
|
||||
max-width: calc(100% - 120px) !important;
|
||||
/* Break long words to avoid overflow */
|
||||
word-break: break-word !important;
|
||||
}
|
||||
.link-board-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ template(name='list')
|
|||
style="{{#unless collapsed}}min-width:{{listWidth}}px;max-width:{{listConstraint}}px;{{/unless}}"
|
||||
class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}} {{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
+listHeader
|
||||
unless collapsed
|
||||
+listBody
|
||||
.list-resize-handle.js-list-resize-handle.nodragscroll
|
||||
+listBody
|
||||
.list-resize-handle.js-list-resize-handle.nodragscroll
|
||||
|
||||
template(name='miniList')
|
||||
a.mini-list.js-select-list.js-list(id="js-list-{{_id}}" class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
|
|
|
|||
|
|
@ -279,8 +279,7 @@ BlazeComponent.extendComponent({
|
|||
|
||||
// Only enable resize for non-collapsed, non-auto-width lists
|
||||
const isAutoWidth = this.autoWidth();
|
||||
const isCollapsed = Utils.getListCollapseState(list);
|
||||
if (isCollapsed || isAutoWidth) {
|
||||
if (list.collapsed || isAutoWidth) {
|
||||
$resizeHandle.hide();
|
||||
return;
|
||||
}
|
||||
|
|
@ -434,10 +433,9 @@ BlazeComponent.extendComponent({
|
|||
});
|
||||
|
||||
|
||||
// Reactively update resize handle visibility when auto-width or collapse changes
|
||||
// Reactively update resize handle visibility when auto-width changes
|
||||
component.autorun(() => {
|
||||
const collapsed = Utils.getListCollapseState(list);
|
||||
if (component.autoWidth() || collapsed) {
|
||||
if (component.autoWidth()) {
|
||||
$resizeHandle.hide();
|
||||
} else {
|
||||
$resizeHandle.show();
|
||||
|
|
@ -454,12 +452,6 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
}).register('list');
|
||||
|
||||
Template.list.helpers({
|
||||
collapsed() {
|
||||
return Utils.getListCollapseState(this);
|
||||
},
|
||||
});
|
||||
|
||||
Template.miniList.events({
|
||||
'click .js-select-list'() {
|
||||
const listId = this._id;
|
||||
|
|
|
|||
|
|
@ -85,19 +85,16 @@ template(name="linkCardPopup")
|
|||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
option(value="") {{_ 'custom-field-dropdown-none'}}
|
||||
each swimlanes
|
||||
option(value="{{_id}}") {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
option(value="") {{_ 'custom-field-dropdown-none'}}
|
||||
each lists
|
||||
option(value="{{_id}}") {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
option(value="") {{_ 'custom-field-dropdown-none'}}
|
||||
each cards
|
||||
option(value="{{getRealId}}") {{getTitle}}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,50 +16,11 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
|
||||
customFieldsSum() {
|
||||
const list = Template.currentData();
|
||||
if (!list) return [];
|
||||
const boardId = Session.get('currentBoard');
|
||||
const fields = ReactiveCache.getCustomFields({
|
||||
boardIds: { $in: [boardId] },
|
||||
const ret = ReactiveCache.getCustomFields({
|
||||
boardIds: { $in: [Session.get('currentBoard')] },
|
||||
showSumAtTopOfList: true,
|
||||
});
|
||||
|
||||
if (!fields || !fields.length) return [];
|
||||
|
||||
const cards = ReactiveCache.getCards({
|
||||
listId: list._id,
|
||||
archived: false,
|
||||
});
|
||||
|
||||
const result = fields.map(field => {
|
||||
let sum = 0;
|
||||
if (cards && cards.length) {
|
||||
cards.forEach(card => {
|
||||
const cfs = (card.customFields || []);
|
||||
const cf = cfs.find(f => f && f._id === field._id);
|
||||
if (!cf || cf.value === null || cf.value === undefined) return;
|
||||
let v = cf.value;
|
||||
if (typeof v === 'string') {
|
||||
// try to parse string numbers, accept comma decimal
|
||||
const parsed = parseFloat(v.replace(',', '.'));
|
||||
if (isNaN(parsed)) return;
|
||||
v = parsed;
|
||||
}
|
||||
if (typeof v === 'number' && isFinite(v)) {
|
||||
sum += v;
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
_id: field._id,
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
settings: field.settings || {},
|
||||
value: sum,
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
return ret;
|
||||
},
|
||||
|
||||
openForm(options) {
|
||||
|
|
@ -293,22 +254,6 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
}).register('listBody');
|
||||
|
||||
// Helpers for listBody template context
|
||||
Template.listBody.helpers({
|
||||
formattedCurrencyCustomFieldValue(val) {
|
||||
// `this` is the custom field sum object from customFieldsSum each-iteration
|
||||
const field = this || {};
|
||||
const code = (field.settings && field.settings.currencyCode) || 'USD';
|
||||
try {
|
||||
const n = typeof val === 'number' ? val : parseFloat(val);
|
||||
if (!isFinite(n)) return val;
|
||||
return new Intl.NumberFormat(undefined, { style: 'currency', currency: code }).format(n);
|
||||
} catch (e) {
|
||||
return `${code} ${val}`;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function toggleValueInReactiveArray(reactiveValue, value) {
|
||||
const array = reactiveValue.get();
|
||||
const valueIndex = array.indexOf(value);
|
||||
|
|
@ -542,6 +487,8 @@ BlazeComponent.extendComponent({
|
|||
{
|
||||
sort: { sort: 1 },
|
||||
});
|
||||
if (swimlanes.length)
|
||||
this.selectedSwimlaneId.set(swimlanes[0]._id);
|
||||
return swimlanes;
|
||||
},
|
||||
|
||||
|
|
@ -556,6 +503,7 @@ BlazeComponent.extendComponent({
|
|||
{
|
||||
sort: { sort: 1 },
|
||||
});
|
||||
if (lists.length) this.selectedListId.set(lists[0]._id);
|
||||
return lists;
|
||||
},
|
||||
|
||||
|
|
@ -564,17 +512,19 @@ BlazeComponent.extendComponent({
|
|||
return [];
|
||||
}
|
||||
const ownCardsIds = this.board.cards().map(card => card.getRealId());
|
||||
const selector = {
|
||||
const ret = ReactiveCache.getCards(
|
||||
{
|
||||
boardId: this.selectedBoardId.get(),
|
||||
swimlaneId: this.selectedSwimlaneId.get(),
|
||||
listId: this.selectedListId.get(),
|
||||
archived: false,
|
||||
linkedId: { $nin: ownCardsIds },
|
||||
_id: { $nin: ownCardsIds },
|
||||
type: { $nin: ['template-card'] },
|
||||
};
|
||||
if (this.selectedBoardId.get()) selector.boardId = this.selectedBoardId.get();
|
||||
if (this.selectedSwimlaneId.get()) selector.swimlaneId = this.selectedSwimlaneId.get();
|
||||
if (this.selectedListId.get()) selector.listId = this.selectedListId.get();
|
||||
|
||||
const ret = ReactiveCache.getCards(selector, { sort: { sort: 1 } });
|
||||
},
|
||||
{
|
||||
sort: { sort: 1 },
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
|
||||
|
|
@ -595,12 +545,8 @@ BlazeComponent.extendComponent({
|
|||
return [
|
||||
{
|
||||
'change .js-select-boards'(evt) {
|
||||
const val = $(evt.currentTarget).val();
|
||||
subManager.subscribe('board', val, false);
|
||||
// Clear selections to allow linking only board or re-choose swimlane/list
|
||||
this.selectedSwimlaneId.set('');
|
||||
this.selectedListId.set('');
|
||||
this.selectedBoardId.set(val);
|
||||
subManager.subscribe('board', $(evt.currentTarget).val(), false);
|
||||
this.selectedBoardId.set($(evt.currentTarget).val());
|
||||
},
|
||||
'change .js-select-swimlanes'(evt) {
|
||||
this.selectedSwimlaneId.set($(evt.currentTarget).val());
|
||||
|
|
|
|||
|
|
@ -26,32 +26,24 @@ template(name="listHeader")
|
|||
|/#{wipLimit.value})
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
|
||||
if hasNumberFieldsSum
|
||||
|
|
||||
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
|
||||
else
|
||||
div.list-header-collapse-container
|
||||
a.list-collapse-indicator.js-collapse(title="{{_ 'collapse'}}")
|
||||
if collapsed
|
||||
| ▶
|
||||
else
|
||||
| 🔽
|
||||
div(class="{{#if collapsed}}list-rotated{{/if}}")
|
||||
h2.list-header-name(
|
||||
title="{{ moment modifiedAt 'LLL' }}"
|
||||
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
|
||||
+viewer
|
||||
= title
|
||||
if wipLimit.enabled
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
|/#{wipLimit.value})
|
||||
if collapsed
|
||||
a.js-collapse(title="{{_ 'uncollapse'}}")
|
||||
| ⬅️
|
||||
| ➡️
|
||||
div(class="{{#if collapsed}}list-rotated{{/if}}")
|
||||
h2.list-header-name(
|
||||
title="{{ moment modifiedAt 'LLL' }}"
|
||||
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
|
||||
+viewer
|
||||
= title
|
||||
if wipLimit.enabled
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
|/#{wipLimit.value})
|
||||
unless collapsed
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
|
||||
if hasNumberFieldsSum
|
||||
|
|
||||
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
|
||||
if isMiniScreen
|
||||
if currentList
|
||||
if isWatching
|
||||
|
|
@ -66,10 +58,6 @@ template(name="listHeader")
|
|||
unless currentUser.isWorker
|
||||
a.list-header-handle.handle.js-list-handle ↕️
|
||||
else if currentUser.isBoardMember
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isWorker
|
||||
a.list-header-handle.handle.js-list-handle ↕️
|
||||
if isWatching
|
||||
i.list-header-watch-icon | 👁️
|
||||
unless collapsed
|
||||
|
|
@ -79,8 +67,14 @@ template(name="listHeader")
|
|||
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
|
||||
if canSeeAddCard
|
||||
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") ➕
|
||||
|
||||
a.js-collapse(title="{{_ 'collapse'}}")
|
||||
| ⬅️
|
||||
| ➡️
|
||||
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isWorker
|
||||
a.list-header-handle.handle.js-list-handle ↕️
|
||||
|
||||
template(name="editListTitleForm")
|
||||
.list-composer
|
||||
|
|
|
|||
|
|
@ -34,14 +34,13 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
collapsed(check = undefined) {
|
||||
const list = Template.currentData();
|
||||
const status = Utils.getListCollapseState(list);
|
||||
const status = list.isCollapsed();
|
||||
if (check === undefined) {
|
||||
// just check
|
||||
return status;
|
||||
} else {
|
||||
const next = typeof check === 'boolean' ? check : !status;
|
||||
Utils.setListCollapseState(list, next);
|
||||
return next;
|
||||
list.collapse(!status);
|
||||
return !status;
|
||||
}
|
||||
},
|
||||
editTitle(event) {
|
||||
|
|
@ -143,48 +142,7 @@ BlazeComponent.extendComponent({
|
|||
Template.listHeader.helpers({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
numberFieldsSum() {
|
||||
const list = Template.currentData();
|
||||
if (!list) return 0;
|
||||
const boardId = Session.get('currentBoard');
|
||||
const fields = ReactiveCache.getCustomFields({
|
||||
boardIds: { $in: [boardId] },
|
||||
showSumAtTopOfList: true,
|
||||
type: 'number',
|
||||
});
|
||||
if (!fields || !fields.length) return 0;
|
||||
const cards = ReactiveCache.getCards({ listId: list._id, archived: false });
|
||||
let total = 0;
|
||||
if (cards && cards.length) {
|
||||
cards.forEach(card => {
|
||||
const cfs = (card.customFields || []);
|
||||
fields.forEach(field => {
|
||||
const cf = cfs.find(f => f && f._id === field._id);
|
||||
if (!cf || cf.value === null || cf.value === undefined) return;
|
||||
let v = cf.value;
|
||||
if (typeof v === 'string') {
|
||||
const parsed = parseFloat(v.replace(',', '.'));
|
||||
if (isNaN(parsed)) return;
|
||||
v = parsed;
|
||||
}
|
||||
if (typeof v === 'number' && isFinite(v)) {
|
||||
total += v;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return total;
|
||||
},
|
||||
hasNumberFieldsSum() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const fields = ReactiveCache.getCustomFields({
|
||||
boardIds: { $in: [boardId] },
|
||||
showSumAtTopOfList: true,
|
||||
type: 'number',
|
||||
});
|
||||
return !!(fields && fields.length);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
Template.listActionPopup.helpers({
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ template(name="minilist")
|
|||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
span.drag-handle(title="{{_ 'dragList'}}") ↕️
|
||||
.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
||||
|
|
|
|||
|
|
@ -232,24 +232,6 @@ class DueCardsComponent extends BlazeComponent {
|
|||
});
|
||||
}
|
||||
|
||||
// Normalize dueAt to timestamps for stable client-side ordering
|
||||
const future = new Date('2100-12-31').getTime();
|
||||
const toTime = v => {
|
||||
if (v === null || v === undefined || v === '') return future;
|
||||
if (v instanceof Date) return v.getTime();
|
||||
const t = new Date(v);
|
||||
if (!isNaN(t.getTime())) return t.getTime();
|
||||
return future;
|
||||
};
|
||||
|
||||
filteredCards.sort((a, b) => {
|
||||
const x = toTime(a.dueAt);
|
||||
const y = toTime(b.dueAt);
|
||||
if (x > y) return 1;
|
||||
if (x < y) return -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards client: filtered to', filteredCards.length, 'cards');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -339,20 +339,15 @@
|
|||
width: 100%;
|
||||
min-width: 3vw;
|
||||
font-size: clamp(12px, 2vw, 14px);
|
||||
box-sizing: border-box;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* Make zoom input wider on all mobile screens */
|
||||
@media screen and (max-width: 800px),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||
#header-quick-access .zoom-controls .zoom-input {
|
||||
min-width: 80px !important; /* Wider on mobile to show 3 digits */
|
||||
width: 80px !important; /* Fixed width to show 100 fully */
|
||||
font-size: 16px !important; /* Slightly larger text */
|
||||
flex: 0 0 80px !important; /* Prevent shrinking in flex */
|
||||
min-width: 50px !important; /* Wider on mobile */
|
||||
width: 50px !important; /* Fixed width to show all numbers */
|
||||
font-size: 14px !important; /* Slightly larger text */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -855,9 +850,8 @@
|
|||
#header-quick-access .zoom-controls .zoom-input {
|
||||
font-size: 16px !important; /* Larger input text */
|
||||
padding: 0.5vh 0.8vw !important;
|
||||
min-width: 80px !important; /* Wider to fit 100 */
|
||||
width: 80px !important; /* Fixed width to show 100 fully */
|
||||
flex: 0 0 80px !important; /* Prevent shrinking in flex */
|
||||
min-width: 6vw !important; /* Much wider for mobile */
|
||||
width: 60px !important; /* Fixed width to show all numbers */
|
||||
}
|
||||
|
||||
/* Make mobile mode toggle larger */
|
||||
|
|
|
|||
|
|
@ -111,9 +111,7 @@ template(name="header")
|
|||
| 📢
|
||||
+viewer
|
||||
| #{announcement}
|
||||
a
|
||||
.js-close-announcement
|
||||
| ❌
|
||||
| ❌
|
||||
|
||||
template(name="offlineWarning")
|
||||
.offline-warning
|
||||
|
|
|
|||
|
|
@ -81,27 +81,6 @@ body {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
/* iOS Safari fixes */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Mobile mode specific fixes for iOS Safari */
|
||||
body.mobile-mode {
|
||||
overflow-x: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
/* Prevent iOS Safari bounce scroll */
|
||||
overscroll-behavior: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Ensure content area is scrollable in mobile mode */
|
||||
body.mobile-mode #content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
height: calc(100vh - 48px);
|
||||
}
|
||||
#content {
|
||||
position: relative;
|
||||
|
|
@ -548,7 +527,7 @@ a:not(.disabled).is-active i.fa {
|
|||
|
||||
/* Board canvas */
|
||||
.board-canvas {
|
||||
padding: 0 8px 8px 0;
|
||||
padding: 8px;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
|
@ -696,7 +675,7 @@ a:not(.disabled).is-active i.fa {
|
|||
}
|
||||
|
||||
.board-canvas {
|
||||
padding: 0 12px 12px 0;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#header {
|
||||
|
|
@ -777,7 +756,7 @@ a:not(.disabled).is-active i.fa {
|
|||
.inline-input {
|
||||
height: 37px;
|
||||
margin: 8px 10px 0 0;
|
||||
width: 100px;
|
||||
width: 50px;
|
||||
}
|
||||
.select-authentication {
|
||||
width: 100%;
|
||||
|
|
@ -920,40 +899,6 @@ a:not(.disabled).is-active i.fa {
|
|||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* iOS Safari Mobile Mode Fixes */
|
||||
@media screen and (max-width: 800px) {
|
||||
/* Prevent scrolling issues on iOS Safari when card popup is open */
|
||||
body.mobile-mode {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Fix z-index stacking for mobile Safari */
|
||||
body.mobile-mode .board-wrapper {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
|
||||
body.mobile-mode .card-details {
|
||||
z-index: 100 !important;
|
||||
}
|
||||
|
||||
body.mobile-mode .pop-over {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* Ensure smooth scrolling on iOS */
|
||||
body.mobile-mode .card-details,
|
||||
body.mobile-mode .pop-over .content-wrapper {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
@-moz-keyframes lds-roller {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@ template(name="main")
|
|||
html(lang="{{TAPi18n.getLanguage}}")
|
||||
head
|
||||
title
|
||||
meta(name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes, viewport-fit=cover")
|
||||
meta(name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes")
|
||||
meta(http-equiv="X-UA-Compatible" content="IE=edge")
|
||||
meta(name="apple-mobile-web-app-capable" content="yes")
|
||||
meta(name="apple-mobile-web-app-status-bar-style" content="black-translucent")
|
||||
//- XXX We should use pathFor in the following `href` to support the case
|
||||
where the application is deployed with a path prefix, but it seems to be
|
||||
difficult to do that cleanly with Blaze -- at least without adding extra
|
||||
|
|
@ -79,6 +77,7 @@ template(name="defaultLayout")
|
|||
| {{{afterBodyStart}}}
|
||||
+Template.dynamic(template=content)
|
||||
| {{{beforeBodyEnd}}}
|
||||
+migrationProgress
|
||||
+boardConversionProgress
|
||||
if (Modal.isOpen)
|
||||
#modal
|
||||
|
|
|
|||
|
|
@ -94,24 +94,24 @@
|
|||
}
|
||||
|
||||
/* Admin edit popups: use full height */
|
||||
.pop-over[data-popup="editUserPopup"],
|
||||
.pop-over[data-popup="editOrgPopup"],
|
||||
.pop-over[data-popup="editTeamPopup"] {
|
||||
.pop-over[data-popup="editUser"],
|
||||
.pop-over[data-popup="editOrg"],
|
||||
.pop-over[data-popup="editTeam"] {
|
||||
height: calc(100vh - 20px) !important;
|
||||
max-height: calc(100vh - 20px) !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editUserPopup"] .content-wrapper,
|
||||
.pop-over[data-popup="editOrgPopup"] .content-wrapper,
|
||||
.pop-over[data-popup="editTeamPopup"] .content-wrapper {
|
||||
.pop-over[data-popup="editUser"] .content-wrapper,
|
||||
.pop-over[data-popup="editOrg"] .content-wrapper,
|
||||
.pop-over[data-popup="editTeam"] .content-wrapper {
|
||||
max-height: calc(100vh - 80px) !important; /* Subtract header height */
|
||||
height: calc(100vh - 80px) !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editUserPopup"] .content-container,
|
||||
.pop-over[data-popup="editOrgPopup"] .content-container,
|
||||
.pop-over[data-popup="editTeamPopup"] .content-container {
|
||||
.pop-over[data-popup="editUser"] .content-container,
|
||||
.pop-over[data-popup="editOrg"] .content-container,
|
||||
.pop-over[data-popup="editTeam"] .content-container {
|
||||
max-height: calc(100vh - 80px) !important; /* Subtract header height */
|
||||
height: calc(100vh - 80px) !important;
|
||||
}
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
}
|
||||
|
||||
/* Specific styling for language popup list */
|
||||
.pop-over[data-popup="changeLanguagePopup"] .pop-over-list {
|
||||
.pop-over[data-popup="changeLanguage"] .pop-over-list {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
height: auto;
|
||||
|
|
@ -131,69 +131,46 @@
|
|||
}
|
||||
|
||||
/* Ensure content div in language popup contains all items */
|
||||
.pop-over[data-popup="changeLanguagePopup"] .content {
|
||||
.pop-over[data-popup="changeLanguage"] .content {
|
||||
height: auto;
|
||||
/* Remove forced min-height to avoid top gap */
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Ensure hidden stack pages truly take no space */
|
||||
.pop-over[data-popup="changeLanguagePopup"] .content.no-height {
|
||||
min-height: 0 !important;
|
||||
height: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
visibility: hidden !important;
|
||||
/* Allow dynamic height for Change Language popup */
|
||||
.pop-over[data-popup="changeLanguage"] .content-wrapper {
|
||||
max-height: inherit; /* Use dynamic height from JavaScript */
|
||||
}
|
||||
|
||||
.pop-over[data-popup="changeLanguage"] .content-container {
|
||||
max-height: inherit; /* Use dynamic height from JavaScript */
|
||||
}
|
||||
|
||||
/* Make language popup extend to bottom of browser window */
|
||||
.pop-over[data-popup="changeLanguagePopup"] {
|
||||
position: fixed !important;
|
||||
bottom: 0 !important;
|
||||
top: auto !important;
|
||||
left: auto !important;
|
||||
right: 20px !important;
|
||||
width: auto !important;
|
||||
max-width: 450px !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
min-height: 300px !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
margin: 0 !important;
|
||||
.pop-over[data-popup="changeLanguage"] {
|
||||
height: calc(100vh - 30px);
|
||||
min-height: 300px;
|
||||
/* Adjust positioning to move popup 30px higher */
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
|
||||
/* Allow dynamic height for Change Language popup */
|
||||
.pop-over[data-popup="changeLanguagePopup"] .header {
|
||||
flex-shrink: 0 !important;
|
||||
height: auto !important;
|
||||
.pop-over[data-popup="changeLanguage"] .content-wrapper {
|
||||
height: calc(100% - 50px); /* Subtract header height more precisely */
|
||||
min-height: 250px;
|
||||
overflow-y: auto;
|
||||
max-height: none; /* Remove any max-height constraints */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="changeLanguagePopup"] .content-wrapper {
|
||||
flex: 1 !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
height: auto !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="changeLanguagePopup"] .content-container {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
flex: 1 !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="changeLanguagePopup"] .content {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
padding-bottom: 50px !important;
|
||||
width: 100% !important;
|
||||
.pop-over[data-popup="changeLanguage"] .content-container {
|
||||
height: auto; /* Let content determine height */
|
||||
min-height: 250px;
|
||||
max-height: none; /* Remove any max-height constraints */
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Date popup sizing for native HTML inputs */
|
||||
|
|
@ -538,7 +515,6 @@
|
|||
position: absolute;
|
||||
top: 6px;
|
||||
right: 12px;
|
||||
color: #3cb500;
|
||||
}
|
||||
.pop-over-list .pop-over-list.checkable li.active a {
|
||||
padding-right: 28px;
|
||||
|
|
@ -546,10 +522,6 @@
|
|||
.pop-over-list .pop-over-list.checkable li.active a .fa-check {
|
||||
display: block;
|
||||
}
|
||||
/* Grey check icons when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
|
||||
color: #7a7a7a;
|
||||
}
|
||||
.pop-over.miniprofile .header {
|
||||
border-bottom-color: transparent;
|
||||
height: 30px;
|
||||
|
|
@ -595,10 +567,6 @@ body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
|
|||
overflow: hidden;
|
||||
margin-top: 0px;
|
||||
border: 0px solid #dbdbdb;
|
||||
/* Ensure popups appear above card details on mobile */
|
||||
z-index: 999999 !important;
|
||||
/* iOS Safari scrolling fix */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.pop-over .header {
|
||||
color: #fff;
|
||||
|
|
@ -683,23 +651,3 @@ body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
|
|||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Force full-screen popups in mobile mode regardless of screen width */
|
||||
body.mobile-mode .pop-over {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
max-width: 100vw !important;
|
||||
max-height: 100vh !important;
|
||||
}
|
||||
body.mobile-mode .pop-over .content-wrapper {
|
||||
width: 100% !important;
|
||||
height: calc(100vh - 48px) !important;
|
||||
max-height: calc(100vh - 48px) !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
class="{{#unless title}}miniprofile{{/unless}}"
|
||||
class=currentBoard.colorClass
|
||||
class="{{#unless title}}no-title{{/unless}}"
|
||||
data-popup="{{popupName}}"
|
||||
style="left:{{offset.left}}px; top:{{offset.top}}px;{{#if offset.maxHeight}} max-height:{{offset.maxHeight}}px;{{/if}}")
|
||||
.header
|
||||
a.back-btn.js-back-view(class="{{#unless hasPopupParent}}is-hidden{{/unless}}")
|
||||
|
|
|
|||
|
|
@ -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'}}
|
||||
|
|
@ -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');
|
||||
|
|
@ -266,36 +266,4 @@
|
|||
.migration-progress-note {
|
||||
color: #a0aec0;
|
||||
}
|
||||
}
|
||||
/* Attachment migration styles */
|
||||
.attachment-item.migrating {
|
||||
position: relative;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.attachment-migration-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.migration-spinner {
|
||||
font-size: 24px;
|
||||
color: #007cba;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.migration-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,14 +4,14 @@ template(name="migrationProgress")
|
|||
.migration-progress-modal
|
||||
.migration-progress-header
|
||||
h3.migration-progress-title
|
||||
| 🔄 {{_ 'migration-progress-title'}}
|
||||
| 🔄 Board Migration in Progress
|
||||
.migration-progress-close.js-close-migration-progress
|
||||
| ❌
|
||||
|
||||
.migration-progress-content
|
||||
.migration-progress-overall
|
||||
.migration-progress-overall-label
|
||||
| {{_ 'migration-progress-overall'}}: {{currentStep}} {{_ 'of'}} {{totalSteps}} {{_ 'steps'}}
|
||||
| Overall Progress: {{currentStep}} of {{totalSteps}} steps
|
||||
.migration-progress-overall-bar
|
||||
.migration-progress-overall-fill(style="{{progressBarStyle}}")
|
||||
.migration-progress-overall-percentage
|
||||
|
|
@ -19,7 +19,7 @@ template(name="migrationProgress")
|
|||
|
||||
.migration-progress-current-step
|
||||
.migration-progress-step-label
|
||||
| {{_ 'migration-progress-current-step'}}: {{stepNameFormatted}}
|
||||
| Current Step: {{stepNameFormatted}}
|
||||
.migration-progress-step-bar
|
||||
.migration-progress-step-fill(style="{{stepProgressBarStyle}}")
|
||||
.migration-progress-step-percentage
|
||||
|
|
@ -27,17 +27,17 @@ template(name="migrationProgress")
|
|||
|
||||
.migration-progress-status
|
||||
.migration-progress-status-label
|
||||
| {{_ 'migration-progress-status'}}:
|
||||
| Status:
|
||||
.migration-progress-status-text
|
||||
| {{stepStatus}}
|
||||
|
||||
if stepDetailsFormatted
|
||||
.migration-progress-details
|
||||
.migration-progress-details-label
|
||||
| {{_ 'migration-progress-details'}}:
|
||||
| Details:
|
||||
.migration-progress-details-text
|
||||
| {{stepDetailsFormatted}}
|
||||
|
||||
.migration-progress-footer
|
||||
.migration-progress-note
|
||||
| {{_ 'migration-progress-note'}}
|
||||
| Please wait while we migrate your board to the latest structure...
|
||||
|
|
@ -20,10 +20,10 @@
|
|||
height: 3vw;
|
||||
}
|
||||
#notifications-drawer .notification .read-status .activity-type {
|
||||
margin: 8px 0 0;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
font-size: clamp(14px, 2vw, 17px);
|
||||
margin: 2vh 0 0;
|
||||
width: 2.2vw;
|
||||
height: 2.2vw;
|
||||
font-size: clamp(14px, 2.5vw, 17px);
|
||||
display: block;
|
||||
color: #bbb;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@ template(name='notification')
|
|||
+activity(activity=activityData mode='none')
|
||||
if read
|
||||
.remove
|
||||
a(title="{{_ 'delete'}}") 🗑️
|
||||
a.fa.fa-trash
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
template(name='notificationIcon')
|
||||
if($in activityType 'deleteAttachment' 'addAttachment')
|
||||
span.activity-type(title="attachment") 📎
|
||||
i.fa.fa-paperclip.activity-type(title="attachment")
|
||||
else if($in activityType 'createBoard' 'importBoard')
|
||||
span.activity-type(title="board") 🗂️
|
||||
i.fa.fa-chalkboard.activity-type(title="board")
|
||||
|
||||
else if($in activityType 'createCard' 'importCard' 'moveCard')
|
||||
+cardNotificationIcon
|
||||
|
|
@ -19,17 +19,17 @@ template(name='notificationIcon')
|
|||
//- DRY and consistant
|
||||
|
||||
else if($in activityType 'checkedItem' 'uncheckedItem' 'addChecklistItem' 'removedChecklistItem')
|
||||
span.activity-type(title="checklist item") ☑️
|
||||
i.fa.fa-check-square.activity-type(title="checklist item")
|
||||
else if($in activityType 'addComment')
|
||||
span.activity-type(title="comment") 💬
|
||||
i.fa.fa-comment-o.activity-type(title="comment")
|
||||
else if($in activityType 'createCustomField' 'setCustomField' 'unsetCustomField')
|
||||
span.activity-type(title="custom field") 🧩
|
||||
i.fa.fa-code.activity-type(title="custom field")
|
||||
else if($in activityType 'addedLabel' 'removedLabel')
|
||||
span.activity-type(title="label") 🏷️
|
||||
i.fa.fa-tag.activity-type(title="label")
|
||||
else if($in activityType 'a-startAt' 'a-receivedAt')
|
||||
span.activity-type(title="date") ⏰
|
||||
i.fa.fa-clock-o.activity-type(title="date")
|
||||
else if($in activityType 'a-dueAt' 'a-endAt')
|
||||
span.activity-type(title="date") ⏰
|
||||
i.fa.fa-clock-o.activity-type(title="date")
|
||||
|
||||
else if($in activityType 'createList' 'removeList' 'archivedList')
|
||||
+listNotificationIcon
|
||||
|
|
@ -41,17 +41,17 @@ template(name='notificationIcon')
|
|||
//- elswhere in the app we use fa-trello to indicate lists...
|
||||
//- i personally like fa-columns a bit better
|
||||
else if($in activityType 'unjoinMember' 'addBoardMember' 'joinMember' 'removeBoardMember')
|
||||
span.activity-type(title="member") 👤
|
||||
i.fa.fa-user.activity-type(title="member")
|
||||
else if($in activityType 'createSwimlane' 'archivedSwimlane')
|
||||
span.activity-type(title="swimlane") 🧭
|
||||
i.fa.fa-th-large.activity-type(title="swimlane")
|
||||
else
|
||||
span.activity-type(title="can't find icon for #{activityType}") 🐞
|
||||
i.fa.fa-bug.activity-type(title="can't find icon for #{activityType}")
|
||||
|
||||
template(name='cardNotificationIcon')
|
||||
span.activity-type(title="card") 🗒️
|
||||
i.fa.fa-clone.activity-type(title="card")
|
||||
|
||||
template(name='checklistNotificationIcon')
|
||||
span.activity-type(title="checklist") 📝
|
||||
i.fa.fa-list.activity-type(title="checklist")
|
||||
|
||||
template(name='listNotificationIcon')
|
||||
span.activity-type(title="list") 📋
|
||||
i.fa.fa-columns.activity-type(title="list")
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ section#notifications-drawer .remove-read {
|
|||
section#notifications-drawer .remove-read:hover {
|
||||
color: #eb4646 !important;
|
||||
}
|
||||
section#notifications-drawer .remove-read:hover i.fa {
|
||||
color: inherit;
|
||||
}
|
||||
section#notifications-drawer ul.notifications {
|
||||
display: block;
|
||||
padding: 0px 16px 0px 16px;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ template(name='notificationsDrawer')
|
|||
h5 {{_ 'notifications'}}
|
||||
if($gt unreadNotifications 0)
|
||||
|(#{unreadNotifications})
|
||||
a.close ❌
|
||||
a.fa.fa-times-thin.close
|
||||
ul.notifications
|
||||
each transformedProfile.notifications
|
||||
+notification(activityData=activityObj index=dbIndex read=read)
|
||||
|
|
@ -16,5 +16,5 @@ template(name='notificationsDrawer')
|
|||
a.all-read {{_ 'mark-all-as-read'}}
|
||||
if ($and ($.Session.get 'showReadNotifications') ($gt readNotifications 0))
|
||||
a.remove-read
|
||||
| 🗑️
|
||||
i.fa.fa-trash
|
||||
| {{_ 'remove-all-read'}}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ template(name="boardActions")
|
|||
| {{_'r-the-board'}}
|
||||
div.trigger-dropdown
|
||||
select(id="board-id")
|
||||
option(value="" disabled selected if=not boards.length) {{loadingBoardsLabel}}
|
||||
each boards
|
||||
if $eq _id currentBoard._id
|
||||
option(value="{{_id}}" selected) {{_ 'current'}}
|
||||
|
|
@ -86,7 +85,6 @@ template(name="boardActions")
|
|||
| {{_'r-the-board'}}
|
||||
div.trigger-dropdown
|
||||
select(id="board-id-link")
|
||||
option(value="" disabled selected if=not boards.length) {{loadingBoardsLabel}}
|
||||
each boards
|
||||
if $eq _id currentBoard._id
|
||||
option(value="{{_id}}" selected) {{_ 'current'}}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
// Ensure boards are available for action dropdowns
|
||||
this.subscribe('boards');
|
||||
},
|
||||
onCreated() {},
|
||||
|
||||
boards() {
|
||||
const ret = ReactiveCache.getBoards(
|
||||
|
|
@ -23,16 +19,6 @@ BlazeComponent.extendComponent({
|
|||
return ret;
|
||||
},
|
||||
|
||||
loadingBoardsLabel() {
|
||||
try {
|
||||
const txt = TAPi18n.__('loading-boards');
|
||||
if (txt && !txt.startsWith("key '")) return txt;
|
||||
} catch (e) {
|
||||
// ignore translation lookup errors
|
||||
}
|
||||
return 'Loading boards...';
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ BlazeComponent.extendComponent({
|
|||
this.subscribe('allRules');
|
||||
this.subscribe('allTriggers');
|
||||
this.subscribe('allActions');
|
||||
this.subscribe('boards');
|
||||
},
|
||||
|
||||
trigger() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
template(name="rulesActions")
|
||||
h2
|
||||
| ✨
|
||||
| {{_ 'r-rule' }} "{{ruleNameStr}}" - {{_ 'r-add-action'}}
|
||||
| {{_ 'r-rule' }} "#{data.ruleName.get}" - {{_ 'r-add-action'}}
|
||||
.triggers-content
|
||||
.triggers-body
|
||||
.triggers-side-menu
|
||||
|
|
@ -15,13 +15,13 @@ template(name="rulesActions")
|
|||
li.js-set-mail-actions
|
||||
| @
|
||||
.triggers-main-body
|
||||
if $eq currentActions.get 'board'
|
||||
if ($eq currentActions.get 'board')
|
||||
+boardActions(ruleName=data.ruleName triggerVar=data.triggerVar)
|
||||
else if $eq currentActions.get 'card'
|
||||
else if ($eq currentActions.get 'card')
|
||||
+cardActions(ruleName=data.ruleName triggerVar=data.triggerVar)
|
||||
else if $eq currentActions.get 'checklist'
|
||||
else if ($eq currentActions.get 'checklist')
|
||||
+checklistActions(ruleName=data.ruleName triggerVar=data.triggerVar)
|
||||
else if $eq currentActions.get 'mail'
|
||||
else if ($eq currentActions.get 'mail')
|
||||
+mailActions(ruleName=data.ruleName triggerVar=data.triggerVar)
|
||||
div.rules-back
|
||||
button.js-goback
|
||||
|
|
|
|||
|
|
@ -5,15 +5,6 @@ BlazeComponent.extendComponent({
|
|||
this.currentActions = new ReactiveVar('board');
|
||||
},
|
||||
|
||||
ruleNameStr() {
|
||||
const rn = this.data() && this.data().ruleName;
|
||||
try {
|
||||
return rn && typeof rn.get === 'function' ? rn.get() : '';
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
setBoardActions() {
|
||||
this.currentActions.set('board');
|
||||
$('.js-set-card-actions').removeClass('active');
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
template(name="rulesTriggers")
|
||||
h2
|
||||
| ✨
|
||||
| {{_ 'r-rule' }} "{{ruleNameStr}}" - {{_ 'r-add-trigger'}}
|
||||
| {{_ 'r-rule' }} "#{data.ruleName.get}" - {{_ 'r-add-trigger'}}
|
||||
.triggers-content
|
||||
.triggers-body
|
||||
.triggers-side-menu
|
||||
|
|
|
|||
|
|
@ -7,15 +7,6 @@ BlazeComponent.extendComponent({
|
|||
this.showChecklistTrigger = new ReactiveVar(false);
|
||||
},
|
||||
|
||||
ruleNameStr() {
|
||||
const rn = this.data() && this.data().ruleName;
|
||||
try {
|
||||
return rn && typeof rn.get === 'function' ? rn.get() : '';
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
setBoardTriggers() {
|
||||
this.showBoardTrigger.set(true);
|
||||
this.showCardTrigger.set(false);
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ template(name="boardTriggers")
|
|||
template(name="boardCardTitlePopup")
|
||||
form
|
||||
label
|
||||
| {{_ 'boardCardTitlePopup-title'}}
|
||||
| Card Title Filter
|
||||
input.js-card-filter-name(type="text" value=title autofocus)
|
||||
input.js-card-filter-button.primary.wide(type="submit" value="{{_ 'set-filter'}}")
|
||||
|
||||
|
|
|
|||
|
|
@ -8,27 +8,27 @@ template(name="adminReports")
|
|||
ul
|
||||
li
|
||||
a.js-report-broken(data-id="report-broken")
|
||||
span.emoji-icon 🔗
|
||||
| 🔗
|
||||
| {{_ 'broken-cards'}}
|
||||
|
||||
li
|
||||
a.js-report-files(data-id="report-files")
|
||||
span.emoji-icon 📎
|
||||
| 📎
|
||||
| {{_ 'filesReportTitle'}}
|
||||
|
||||
li
|
||||
a.js-report-rules(data-id="report-rules")
|
||||
span.emoji-icon ✨
|
||||
| ✨
|
||||
| {{_ 'rulesReportTitle'}}
|
||||
|
||||
li
|
||||
a.js-report-boards(data-id="report-boards")
|
||||
span.emoji-icon ✨
|
||||
| ✨
|
||||
| {{_ 'boardsReportTitle'}}
|
||||
|
||||
li
|
||||
a.js-report-cards(data-id="report-cards")
|
||||
span.emoji-icon ✨
|
||||
| ✨
|
||||
| {{_ 'cardsReportTitle'}}
|
||||
|
||||
.main-body
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ template(name="people")
|
|||
option(value="inactive") {{_ 'admin-people-filter-inactive'}}
|
||||
option(value="admin") Admin
|
||||
button#unlockAllUsers.unlock-all-btn
|
||||
span.emoji-icon 🔓
|
||||
| 🔓
|
||||
| {{_ 'accounts-lockout-unlock-all'}}
|
||||
.ext-box-right
|
||||
span {{#unless isMiniScreen}}{{_ 'people-number'}}{{/unless}} #{peopleNumber}
|
||||
|
|
@ -58,7 +58,7 @@ template(name="people")
|
|||
| {{_ 'add'}} / {{_ 'delete'}} {{_ 'teams'}}
|
||||
else if lockedUsersSetting.get
|
||||
span
|
||||
span.emoji-icon.text-red 🔒
|
||||
span.text-red 🔒
|
||||
unless isMiniScreen
|
||||
| {{_ 'accounts-lockout-locked-users'}}
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ template(name="people")
|
|||
| {{_ 'people'}}
|
||||
li
|
||||
a.js-locked-users-menu(data-id="locked-users-setting")
|
||||
span.emoji-icon.text-red 🔒
|
||||
span.text-red 🔒
|
||||
| {{_ 'accounts-lockout-locked-users'}}
|
||||
.main-body
|
||||
if loading.get
|
||||
|
|
@ -247,9 +247,9 @@ template(name="peopleRow")
|
|||
input.selectUserChkBox(type="checkbox", id="{{userData._id}}")
|
||||
td.account-status
|
||||
if isUserLocked
|
||||
span.text-red.js-toggle-lock-status.emoji-icon(data-user-id=userData._id, data-is-locked="true", title="{{_ 'accounts-lockout-click-to-unlock'}}") 🔒
|
||||
span.text-red.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="true", title="{{_ 'accounts-lockout-click-to-unlock'}}") 🔒
|
||||
else
|
||||
span.text-green.js-toggle-lock-status.emoji-icon(data-user-id=userData._id, data-is-locked="false", title="{{_ 'accounts-lockout-user-unlocked'}}") 🔓
|
||||
span.text-green.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="false", title="{{_ 'accounts-lockout-user-unlocked'}}") 🔓
|
||||
td.account-active-status
|
||||
if userData.loginDisabled
|
||||
span.text-red.js-toggle-active-status(data-user-id=userData._id, data-is-active="false", title="{{_ 'admin-people-user-inactive'}}") 🚫
|
||||
|
|
|
|||
|
|
@ -137,13 +137,8 @@
|
|||
padding: 0.5rem 0.5rem;
|
||||
}
|
||||
.setting-content .content-body .main-body ul li a .is-checked {
|
||||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .setting-content .content-body .main-body ul li a .is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
border-bottom: 2px solid #2980b9;
|
||||
border-right: 2px solid #2980b9;
|
||||
}
|
||||
.setting-content .content-body .main-body ul li a span {
|
||||
padding: 0 0.5rem;
|
||||
|
|
|
|||
|
|
@ -6,87 +6,87 @@ template(name="setting")
|
|||
.content-title.ext-box
|
||||
if isGeneralSetting
|
||||
span
|
||||
span.emoji-icon 🔑
|
||||
| 🔑
|
||||
| {{_ 'registration'}}
|
||||
else if isEmailSetting
|
||||
span
|
||||
span.emoji-icon ✉️
|
||||
| ✉️
|
||||
| {{_ 'email'}}
|
||||
else if isAccountSetting
|
||||
span
|
||||
span.emoji-icon 👥
|
||||
| 👥
|
||||
| {{_ 'accounts'}}
|
||||
else if isTableVisibilityModeSetting
|
||||
span
|
||||
span.emoji-icon 👁️
|
||||
| 👁️
|
||||
| {{_ 'tableVisibilityMode'}}
|
||||
else if isAnnouncementSetting
|
||||
span
|
||||
span.emoji-icon 📢
|
||||
| 📢
|
||||
| {{_ 'admin-announcement'}}
|
||||
else if isAccessibilitySetting
|
||||
span
|
||||
span.emoji-icon ♿
|
||||
| ♿
|
||||
| {{_ 'accessibility'}}
|
||||
else if isLayoutSetting
|
||||
span
|
||||
span.emoji-icon 🔗
|
||||
| 🔗
|
||||
| {{_ 'layout'}}
|
||||
else if isWebhookSetting
|
||||
span
|
||||
span.emoji-icon 🌐
|
||||
| 🌐
|
||||
| {{_ 'global-webhook'}}
|
||||
else if isAttachmentSettings
|
||||
span
|
||||
span.emoji-iconpan.emoji-icon 📎
|
||||
| 📎
|
||||
| {{_ 'attachments'}}
|
||||
else if isCronSettings
|
||||
span
|
||||
span.emoji-icon ⏰
|
||||
| ⏰
|
||||
| {{_ 'cron'}}
|
||||
.content-body
|
||||
.side-menu
|
||||
ul
|
||||
li(class="{{#if isGeneralSetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="registration-setting")
|
||||
span.emoji-icon 🔑
|
||||
| 🔑
|
||||
| {{_ 'registration'}}
|
||||
unless isSandstorm
|
||||
li(class="{{#if isEmailSetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="email-setting")
|
||||
span.emoji-icon ✉️
|
||||
| ✉️
|
||||
| {{_ 'email'}}
|
||||
li(class="{{#if isAccountSetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="account-setting")
|
||||
span.emoji-icon 👥
|
||||
| 👥
|
||||
| {{_ 'accounts'}}
|
||||
li(class="{{#if isTableVisibilityModeSetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="tableVisibilityMode-setting")
|
||||
span.emoji-icon 👁️
|
||||
| 👁️
|
||||
| {{_ 'tableVisibilityMode'}}
|
||||
li(class="{{#if isAnnouncementSetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="announcement-setting")
|
||||
span.emoji-icon 📢
|
||||
| 📢
|
||||
| {{_ 'admin-announcement'}}
|
||||
li(class="{{#if isAccessibilitySetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="accessibility-setting")
|
||||
span.emoji-icon ♿
|
||||
| ♿
|
||||
| {{_ 'accessibility'}}
|
||||
li(class="{{#if isLayoutSetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="layout-setting")
|
||||
span.emoji-icon 🔗
|
||||
| 🔗
|
||||
| {{_ 'layout'}}
|
||||
li(class="{{#if isWebhookSetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="webhook-setting")
|
||||
span.emoji-icon 🌐
|
||||
| 🌐
|
||||
| {{_ 'global-webhook'}}
|
||||
li(class="{{#if isAttachmentSettings}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="attachment-settings")
|
||||
span.emoji-icon 📎
|
||||
| 📎
|
||||
| {{_ 'attachments'}}
|
||||
li(class="{{#if isCronSettings}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="cron-settings")
|
||||
span.emoji-icon ⏰
|
||||
| ⏰
|
||||
| {{_ 'cron'}}
|
||||
.main-body
|
||||
if isLoading
|
||||
|
|
@ -170,10 +170,7 @@ template(name="setting")
|
|||
label {{_ 'migration-status'}}
|
||||
.status-indicator
|
||||
span.status-label {{_ 'status'}}:
|
||||
span.status-value {{#if isMigrating}}{{migrationStatus}}{{else}}{{_ 'idle'}}{{/if}}
|
||||
.current-step(class="{{#unless migrationCurrentStep}}hide{{/unless}}")
|
||||
span.step-label {{_ 'current-step'}}:
|
||||
span.step-value {{migrationCurrentStep}}
|
||||
span.status-value {{migrationStatus}}
|
||||
.progress-section
|
||||
.progress
|
||||
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
|
||||
|
|
@ -182,13 +179,9 @@ template(name="setting")
|
|||
| {{migrationProgress}}% {{_ 'complete'}}
|
||||
|
||||
.form-group
|
||||
button.js-start-all-migrations.btn.btn-primary {{#if isMigrating}}disabled{{/if}} {{_ 'start-all-migrations'}}
|
||||
button.js-pause-all-migrations.btn.btn-warning {{#unless isMigrating}}disabled{{/unless}} {{_ 'pause-all-migrations'}}
|
||||
button.js-stop-all-migrations.btn.btn-danger {{#unless isMigrating}}disabled{{/unless}} {{_ 'stop-all-migrations'}}
|
||||
|
||||
li
|
||||
h3 {{_ 'migration-steps'}}
|
||||
p Migration steps section temporarily removed
|
||||
button.js-start-all-migrations.btn.btn-primary {{_ 'start-all-migrations'}}
|
||||
button.js-pause-all-migrations.btn.btn-warning {{_ 'pause-all-migrations'}}
|
||||
button.js-stop-all-migrations.btn.btn-danger {{_ 'stop-all-migrations'}}
|
||||
|
||||
li
|
||||
h3 {{_ 'board-operations'}}
|
||||
|
|
@ -207,7 +200,7 @@ template(name="setting")
|
|||
.job-info
|
||||
.job-name {{name}}
|
||||
.job-schedule {{schedule}}
|
||||
.job-status {{status}}
|
||||
.job-description {{description}}
|
||||
.job-actions
|
||||
button.js-pause-job.btn.btn-sm.btn-warning(data-job-id="{{_id}}") {{_ 'pause'}}
|
||||
button.js-delete-job.btn.btn-sm.btn-danger(data-job-id="{{_id}}") {{_ 'delete'}}
|
||||
|
|
@ -281,7 +274,7 @@ template(name='email')
|
|||
// li.smtp-form
|
||||
// .title {{_ 'smtp-username'}}
|
||||
// .form-group
|
||||
// input.wekan-form-control#mail-server-username(type="text", placeholder="{{_ 'username'}}" value="{{currentSetting.mailServer.username}}")
|
||||
// input.wekan-form-control#mail-server-u"accounts-allowUserNameChange": "Allow Username Change",sername(type="text", placeholder="{{_ 'username'}}" value="{{currentSetting.mailServer.username}}")
|
||||
// li.smtp-form
|
||||
// .title {{_ 'smtp-password'}}
|
||||
// .form-group
|
||||
|
|
@ -382,29 +375,6 @@ template(name='layoutSettings')
|
|||
ul#layout-setting.setting-detail
|
||||
li
|
||||
button.js-all-boards-hide-activities.primary {{_ 'hide-activities-of-all-boards'}}
|
||||
li
|
||||
a(href="/support" style="text-decoration: underline; color: blue;") {{_ 'support'}}
|
||||
li
|
||||
a.flex.js-toggle-support
|
||||
.materialCheckBox(class="{{#if currentSetting.supportPageEnabled}}is-checked{{/if}}")
|
||||
|
||||
span {{_ 'support-page-enabled'}}
|
||||
li
|
||||
.support-content(class="{{#if currentSetting.supportPageEnabled}}{{else}}hide{{/if}}")
|
||||
ul
|
||||
li
|
||||
a.flex.js-toggle-support-public
|
||||
.materialCheckBox(class="{{#if currentSetting.supportPagePublic}}is-checked{{/if}}")
|
||||
|
||||
span {{_ 'public'}}
|
||||
li
|
||||
.title {{_ 'support-title'}}
|
||||
textarea#support-title.wekan-form-control= currentSetting.supportTitle
|
||||
li
|
||||
.title {{_ 'support-content'}}
|
||||
textarea#support-page-text.wekan-form-control= currentSetting.supportPageText
|
||||
li
|
||||
button.js-support-save.primary {{_ 'save'}}
|
||||
li.layout-form
|
||||
.title {{_ 'oidc-button-text'}}
|
||||
.form-group
|
||||
|
|
|
|||
|
|
@ -2,14 +2,6 @@ import { ReactiveCache } from '/imports/reactiveCache';
|
|||
import { TAPi18n } from '/imports/i18n';
|
||||
import { ALLOWED_WAIT_SPINNERS } from '/config/const';
|
||||
import LockoutSettings from '/models/lockoutSettings';
|
||||
import {
|
||||
cronMigrationProgress,
|
||||
cronMigrationStatus,
|
||||
cronMigrationCurrentStep,
|
||||
cronMigrationSteps,
|
||||
cronIsMigrating,
|
||||
cronJobs
|
||||
} from '/imports/cronMigrationClient';
|
||||
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
|
|
@ -123,27 +115,15 @@ BlazeComponent.extendComponent({
|
|||
|
||||
// Cron settings helpers
|
||||
migrationStatus() {
|
||||
return cronMigrationStatus.get() || TAPi18n.__('idle');
|
||||
return TAPi18n.__('idle'); // Placeholder
|
||||
},
|
||||
|
||||
migrationProgress() {
|
||||
return cronMigrationProgress.get() || 0;
|
||||
},
|
||||
|
||||
migrationCurrentStep() {
|
||||
return cronMigrationCurrentStep.get() || '';
|
||||
},
|
||||
|
||||
isMigrating() {
|
||||
return cronIsMigrating.get() || false;
|
||||
},
|
||||
|
||||
migrationSteps() {
|
||||
return cronMigrationSteps.get() || [];
|
||||
return 0; // Placeholder
|
||||
},
|
||||
|
||||
cronJobs() {
|
||||
return cronJobs.get() || [];
|
||||
return []; // Placeholder
|
||||
},
|
||||
|
||||
setLoading(w) {
|
||||
|
|
@ -189,9 +169,7 @@ BlazeComponent.extendComponent({
|
|||
// Event handlers for cron settings
|
||||
'click button.js-start-all-migrations'(event) {
|
||||
event.preventDefault();
|
||||
this.setLoading(true);
|
||||
Meteor.call('cron.startAllMigrations', (error, result) => {
|
||||
this.setLoading(false);
|
||||
Meteor.call('startAllMigrations', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
|
|
@ -202,9 +180,7 @@ BlazeComponent.extendComponent({
|
|||
|
||||
'click button.js-pause-all-migrations'(event) {
|
||||
event.preventDefault();
|
||||
this.setLoading(true);
|
||||
Meteor.call('cron.pauseAllMigrations', (error, result) => {
|
||||
this.setLoading(false);
|
||||
Meteor.call('pauseAllMigrations', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
|
|
@ -216,9 +192,7 @@ BlazeComponent.extendComponent({
|
|||
'click button.js-stop-all-migrations'(event) {
|
||||
event.preventDefault();
|
||||
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
|
||||
this.setLoading(true);
|
||||
Meteor.call('cron.stopAllMigrations', (error, result) => {
|
||||
this.setLoading(false);
|
||||
Meteor.call('stopAllMigrations', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
|
|
@ -230,28 +204,41 @@ BlazeComponent.extendComponent({
|
|||
|
||||
'click button.js-schedule-board-cleanup'(event) {
|
||||
event.preventDefault();
|
||||
// Placeholder - board cleanup scheduling
|
||||
alert(TAPi18n.__('board-cleanup-scheduled'));
|
||||
Meteor.call('scheduleBoardCleanup', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('board-cleanup-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('board-cleanup-scheduled'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click button.js-schedule-board-archive'(event) {
|
||||
event.preventDefault();
|
||||
// Placeholder - board archive scheduling
|
||||
alert(TAPi18n.__('board-archive-scheduled'));
|
||||
Meteor.call('scheduleBoardArchive', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('board-archive-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('board-archive-scheduled'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click button.js-schedule-board-backup'(event) {
|
||||
event.preventDefault();
|
||||
// Placeholder - board backup scheduling
|
||||
alert(TAPi18n.__('board-backup-scheduled'));
|
||||
Meteor.call('scheduleBoardBackup', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('board-backup-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('board-backup-scheduled'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click button.js-pause-job'(event) {
|
||||
event.preventDefault();
|
||||
const jobId = $(event.target).data('job-id');
|
||||
this.setLoading(true);
|
||||
Meteor.call('cron.pauseJob', jobId, (error, result) => {
|
||||
this.setLoading(false);
|
||||
Meteor.call('pauseCronJob', jobId, (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('cron-job-pause-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
|
|
@ -264,9 +251,7 @@ BlazeComponent.extendComponent({
|
|||
event.preventDefault();
|
||||
const jobId = $(event.target).data('job-id');
|
||||
if (confirm(TAPi18n.__('cron-job-delete-confirm'))) {
|
||||
this.setLoading(true);
|
||||
Meteor.call('cron.removeJob', jobId, (error, result) => {
|
||||
this.setLoading(false);
|
||||
Meteor.call('deleteCronJob', jobId, (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('cron-job-delete-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
|
|
@ -533,45 +518,6 @@ BlazeComponent.extendComponent({
|
|||
DocHead.setTitle(productName);
|
||||
},
|
||||
|
||||
toggleSupportPage() {
|
||||
this.setLoading(true);
|
||||
const supportPageEnabled = !$('.js-toggle-support .materialCheckBox').hasClass('is-checked');
|
||||
$('.js-toggle-support .materialCheckBox').toggleClass('is-checked');
|
||||
$('.support-content').toggleClass('hide');
|
||||
Settings.update(Settings.findOne()._id, {
|
||||
$set: { supportPageEnabled },
|
||||
});
|
||||
this.setLoading(false);
|
||||
},
|
||||
|
||||
toggleSupportPublic() {
|
||||
this.setLoading(true);
|
||||
const supportPagePublic = !$('.js-toggle-support-public .materialCheckBox').hasClass('is-checked');
|
||||
$('.js-toggle-support-public .materialCheckBox').toggleClass('is-checked');
|
||||
Settings.update(Settings.findOne()._id, {
|
||||
$set: { supportPagePublic },
|
||||
});
|
||||
this.setLoading(false);
|
||||
},
|
||||
|
||||
saveSupportSettings() {
|
||||
this.setLoading(true);
|
||||
const supportTitle = ($('#support-title').val() || '').trim();
|
||||
const supportPageText = ($('#support-page-text').val() || '').trim();
|
||||
try {
|
||||
Settings.update(Settings.findOne()._id, {
|
||||
$set: {
|
||||
supportTitle,
|
||||
supportPageText,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
return;
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
},
|
||||
|
||||
sendSMTPTestEmail() {
|
||||
Meteor.call('sendSMTPTestEmail', (err, ret) => {
|
||||
if (!err && ret) {
|
||||
|
|
@ -600,9 +546,6 @@ BlazeComponent.extendComponent({
|
|||
'click a.js-toggle-hide-card-counter-list': this.toggleHideCardCounterList,
|
||||
'click a.js-toggle-hide-board-member-list': this.toggleHideBoardMemberList,
|
||||
'click button.js-save-layout': this.saveLayout,
|
||||
'click a.js-toggle-support': this.toggleSupportPage,
|
||||
'click a.js-toggle-support-public': this.toggleSupportPublic,
|
||||
'click button.js-support-save': this.saveSupportSettings,
|
||||
'click a.js-toggle-display-authentication-method': this
|
||||
.toggleDisplayAuthenticationMethod,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,31 +5,31 @@ template(name="settingHeaderBar")
|
|||
.setting-header-btns.left
|
||||
if currentUser
|
||||
a.setting-header-btn.settings(href="{{pathFor 'setting'}}")
|
||||
span.emoji-icon ⚙️
|
||||
| ⚙️
|
||||
span {{_ 'settings'}}
|
||||
|
||||
a.setting-header-btn.people(href="{{pathFor 'people'}}")
|
||||
span.emoji-icon 👥
|
||||
| 👥
|
||||
span {{_ 'people'}}
|
||||
|
||||
a.setting-header-btn.informations(href="{{pathFor 'admin-reports'}}")
|
||||
span.emoji-icon 📋
|
||||
| 📋
|
||||
span {{_ 'reports'}}
|
||||
|
||||
a.setting-header-btn.informations(href="{{pathFor 'attachments'}}")
|
||||
span.emoji-icon 📎
|
||||
| 📎
|
||||
span {{_ 'attachments'}}
|
||||
|
||||
a.setting-header-btn.informations(href="{{pathFor 'translation'}}")
|
||||
span.emoji-icon 🔤
|
||||
| 🔤
|
||||
span {{_ 'translation'}}
|
||||
|
||||
a.setting-header-btn.informations(href="{{pathFor 'information'}}")
|
||||
span.emoji-icon ℹ️
|
||||
| ℹ️
|
||||
span {{_ 'info'}}
|
||||
|
||||
else
|
||||
a.setting-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
span.emoji-icon 🚪
|
||||
| 🚪
|
||||
span {{_ 'log-in'}}
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ Template.newTranslationPopup.events({
|
|||
Template.settingsTranslationPopup.events({
|
||||
'click #deleteButton'(event) {
|
||||
event.preventDefault();
|
||||
Meteor.call('deleteTranslation', this.translationId);
|
||||
Translation.remove(this.translationId);
|
||||
Popup.back();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -68,14 +68,6 @@
|
|||
transform-origin: 100% 100% !important;
|
||||
}
|
||||
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .sidebar .materialCheckBox.is-checked,
|
||||
body.grey-icons-enabled .boardCardSettingsPopup .materialCheckBox.is-checked,
|
||||
body.grey-icons-enabled .boardSubtaskSettingsPopup .materialCheckBox.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a !important;
|
||||
border-right: 2px solid #7a7a7a !important;
|
||||
}
|
||||
|
||||
/* Card Settings 3-column grid layout */
|
||||
.card-settings-grid {
|
||||
display: grid;
|
||||
|
|
@ -138,11 +130,6 @@ body.grey-icons-enabled .boardSubtaskSettingsPopup .materialCheckBox.is-checked
|
|||
}
|
||||
.sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-check {
|
||||
margin: 0 4px;
|
||||
color: #3cb500;
|
||||
}
|
||||
/* Grey check icons when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-check {
|
||||
color: #7a7a7a;
|
||||
}
|
||||
.sidebar .sidebar-content ul.sidebar-list li .minicard {
|
||||
padding: 6px 8px 4px;
|
||||
|
|
@ -162,9 +149,6 @@ body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-
|
|||
border-radius: 3px;
|
||||
background: #e6e6e6;
|
||||
}
|
||||
.sidebar .sidebar-content .sidebar-btn * {
|
||||
color: #fff;
|
||||
}
|
||||
.sidebar .sidebar-content .sidebar-btn:hover * {
|
||||
color: #fff;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ template(name="boardChangeColorPopup")
|
|||
span.background-box(class="board-color-{{this}}")
|
||||
span {{this}}
|
||||
if isSelected
|
||||
span.checkmark-no-grey ✅
|
||||
| ✅
|
||||
|
||||
template(name="boardChangeBackgroundImagePopup")
|
||||
form
|
||||
|
|
@ -575,8 +575,7 @@ template(name="boardMenuPopup")
|
|||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-open-rules-view(title="{{_ 'rules'}}")
|
||||
span.emoji-icon
|
||||
| ✨
|
||||
| ✨
|
||||
| {{_ 'rules'}}
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
|
|
@ -588,6 +587,10 @@ template(name="boardMenuPopup")
|
|||
| 📦
|
||||
| {{_ 'archived-items'}}
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-open-migrations
|
||||
| 🔧
|
||||
| {{_ 'migrations'}}
|
||||
li
|
||||
a.js-change-board-color
|
||||
| 🎨
|
||||
|
|
@ -634,8 +637,7 @@ template(name="boardMenuPopup")
|
|||
// | {{_ 'delete-duplicate-lists'}}
|
||||
li
|
||||
a.js-archive-board
|
||||
span.emoji-icon
|
||||
| ➡️📦
|
||||
| ➡️📦
|
||||
| {{_ 'archive-board'}}
|
||||
|
||||
template(name="exportBoard")
|
||||
|
|
@ -807,12 +809,6 @@ template(name="changePermissionsPopup")
|
|||
if isNormal
|
||||
| ✅
|
||||
span.sub-name {{_ 'normal-desc'}}
|
||||
li
|
||||
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-normal-assigned-only{{/if}}")
|
||||
| {{_ 'normal-assigned-only'}}
|
||||
if isNormalAssignedOnly
|
||||
| ✅
|
||||
span.sub-name {{_ 'normal-assigned-only-desc'}}
|
||||
li
|
||||
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-no-comments{{/if}}")
|
||||
| {{_ 'no-comments'}}
|
||||
|
|
@ -825,24 +821,6 @@ template(name="changePermissionsPopup")
|
|||
if isCommentOnly
|
||||
| ✅
|
||||
span.sub-name {{_ 'comment-only-desc'}}
|
||||
li
|
||||
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-comment-assigned-only{{/if}}")
|
||||
| {{_ 'comment-assigned-only'}}
|
||||
if isCommentAssignedOnly
|
||||
| ✅
|
||||
span.sub-name {{_ 'comment-assigned-only-desc'}}
|
||||
li
|
||||
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-read-only{{/if}}")
|
||||
| {{_ 'read-only'}}
|
||||
if isReadOnly
|
||||
| ✅
|
||||
span.sub-name {{_ 'read-only-desc'}}
|
||||
li
|
||||
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-read-assigned-only{{/if}}")
|
||||
| {{_ 'read-assigned-only'}}
|
||||
if isReadAssignedOnly
|
||||
| ✅
|
||||
span.sub-name {{_ 'read-assigned-only-desc'}}
|
||||
li
|
||||
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-worker{{/if}}")
|
||||
| {{_ 'worker'}}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const viewTitles = {
|
|||
multiselection: 'multi-selection',
|
||||
customFields: 'custom-fields',
|
||||
archives: 'archives',
|
||||
migrations: 'migrations',
|
||||
};
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
|
|
@ -238,20 +239,8 @@ Template.memberPopup.helpers({
|
|||
const commentOnly = currentBoard.hasCommentOnly(this.userId);
|
||||
const noComments = currentBoard.hasNoComments(this.userId);
|
||||
const worker = currentBoard.hasWorker(this.userId);
|
||||
const normalAssignedOnly = currentBoard.hasNormalAssignedOnly(this.userId);
|
||||
const commentAssignedOnly = currentBoard.hasCommentAssignedOnly(this.userId);
|
||||
const readOnly = currentBoard.hasReadOnly(this.userId);
|
||||
const readAssignedOnly = currentBoard.hasReadAssignedOnly(this.userId);
|
||||
if (readAssignedOnly) {
|
||||
return TAPi18n.__('read-assigned-only');
|
||||
} else if (readOnly) {
|
||||
return TAPi18n.__('read-only');
|
||||
} else if (commentAssignedOnly) {
|
||||
return TAPi18n.__('comment-assigned-only');
|
||||
} else if (commentOnly) {
|
||||
if (commentOnly) {
|
||||
return TAPi18n.__('comment-only');
|
||||
} else if (normalAssignedOnly) {
|
||||
return TAPi18n.__('normal-assigned-only');
|
||||
} else if (noComments) {
|
||||
return TAPi18n.__('no-comments');
|
||||
} else if (worker) {
|
||||
|
|
@ -283,6 +272,10 @@ Template.boardMenuPopup.events({
|
|||
Sidebar.setView('archives');
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-open-migrations'() {
|
||||
Sidebar.setView('migrations');
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-change-board-color': Popup.open('boardChangeColor'),
|
||||
'click .js-change-background-image': Popup.open('boardChangeBackgroundImage'),
|
||||
'click .js-board-info-on-my-boards': Popup.open('boardInfoOnMyBoards'),
|
||||
|
|
@ -401,7 +394,6 @@ Template.memberPopup.events({
|
|||
FlowRouter.go('home');
|
||||
});
|
||||
}),
|
||||
|
||||
});
|
||||
|
||||
Template.removeMemberPopup.helpers({
|
||||
|
|
@ -1933,7 +1925,7 @@ Template.removeBoardTeamPopup.helpers({
|
|||
});
|
||||
|
||||
Template.changePermissionsPopup.events({
|
||||
'click .js-set-admin, click .js-set-normal, click .js-set-normal-assigned-only, click .js-set-no-comments, click .js-set-comment-only, click .js-set-comment-assigned-only, click .js-set-read-only, click .js-set-read-assigned-only, click .js-set-worker'(
|
||||
'click .js-set-admin, click .js-set-normal, click .js-set-no-comments, click .js-set-comment-only, click .js-set-worker'(
|
||||
event,
|
||||
) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
|
|
@ -1942,14 +1934,6 @@ Template.changePermissionsPopup.events({
|
|||
const isCommentOnly = $(event.currentTarget).hasClass(
|
||||
'js-set-comment-only',
|
||||
);
|
||||
const isNormalAssignedOnly = $(event.currentTarget).hasClass(
|
||||
'js-set-normal-assigned-only',
|
||||
);
|
||||
const isCommentAssignedOnly = $(event.currentTarget).hasClass(
|
||||
'js-set-comment-assigned-only',
|
||||
);
|
||||
const isReadOnly = $(event.currentTarget).hasClass('js-set-read-only');
|
||||
const isReadAssignedOnly = $(event.currentTarget).hasClass('js-set-read-assigned-only');
|
||||
const isNoComments = $(event.currentTarget).hasClass('js-set-no-comments');
|
||||
const isWorker = $(event.currentTarget).hasClass('js-set-worker');
|
||||
currentBoard.setMemberPermission(
|
||||
|
|
@ -1958,10 +1942,6 @@ Template.changePermissionsPopup.events({
|
|||
isNoComments,
|
||||
isCommentOnly,
|
||||
isWorker,
|
||||
isNormalAssignedOnly,
|
||||
isCommentAssignedOnly,
|
||||
isReadOnly,
|
||||
isReadAssignedOnly,
|
||||
);
|
||||
Popup.back(1);
|
||||
},
|
||||
|
|
@ -1979,22 +1959,10 @@ Template.changePermissionsPopup.helpers({
|
|||
!currentBoard.hasAdmin(this.userId) &&
|
||||
!currentBoard.hasNoComments(this.userId) &&
|
||||
!currentBoard.hasCommentOnly(this.userId) &&
|
||||
!currentBoard.hasNormalAssignedOnly(this.userId) &&
|
||||
!currentBoard.hasCommentAssignedOnly(this.userId) &&
|
||||
!currentBoard.hasReadOnly(this.userId) &&
|
||||
!currentBoard.hasReadAssignedOnly(this.userId) &&
|
||||
!currentBoard.hasWorker(this.userId)
|
||||
);
|
||||
},
|
||||
|
||||
isNormalAssignedOnly() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return (
|
||||
!currentBoard.hasAdmin(this.userId) &&
|
||||
currentBoard.hasNormalAssignedOnly(this.userId)
|
||||
);
|
||||
},
|
||||
|
||||
isNoComments() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return (
|
||||
|
|
@ -2011,30 +1979,6 @@ Template.changePermissionsPopup.helpers({
|
|||
);
|
||||
},
|
||||
|
||||
isCommentAssignedOnly() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return (
|
||||
!currentBoard.hasAdmin(this.userId) &&
|
||||
currentBoard.hasCommentAssignedOnly(this.userId)
|
||||
);
|
||||
},
|
||||
|
||||
isReadOnly() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return (
|
||||
!currentBoard.hasAdmin(this.userId) &&
|
||||
currentBoard.hasReadOnly(this.userId)
|
||||
);
|
||||
},
|
||||
|
||||
isReadAssignedOnly() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return (
|
||||
!currentBoard.hasAdmin(this.userId) &&
|
||||
currentBoard.hasReadAssignedOnly(this.userId)
|
||||
);
|
||||
},
|
||||
|
||||
isWorker() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ template(name="filterSidebar")
|
|||
else
|
||||
span.quiet {{_ "label-default" (_ (concat "color-" color))}}
|
||||
if Filter.labelIds.isSelected _id
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
hr
|
||||
h3
|
||||
| 👥
|
||||
|
|
@ -68,7 +68,7 @@ template(name="filterSidebar")
|
|||
span.sidebar-list-item-description
|
||||
| {{_ 'filter-no-assignee'}}
|
||||
if Filter.assignees.isSelected undefined
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
each currentBoard.activeMembers
|
||||
with getUser userId
|
||||
li(class="{{#if Filter.assignees.isSelected _id}}active{{/if}}")
|
||||
|
|
@ -90,37 +90,37 @@ template(name="filterSidebar")
|
|||
span.sidebar-list-item-description
|
||||
| {{_ 'filter-no-due-date' }}
|
||||
if Filter.dueAt.isSelected 'noDate'
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
li(class="{{#if Filter.dueAt.isSelected 'past'}}active{{/if}}")
|
||||
a.name.js-toggle-overdue-filter
|
||||
span.sidebar-list-item-description
|
||||
| {{_ 'filter-overdue' }}
|
||||
if Filter.dueAt.isSelected 'past'
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
li(class="{{#if Filter.dueAt.isSelected 'today'}}active{{/if}}")
|
||||
a.name.js-toggle-due-today-filter
|
||||
span.sidebar-list-item-description
|
||||
| {{_ 'filter-due-today' }}
|
||||
if Filter.dueAt.isSelected 'today'
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
li(class="{{#if Filter.dueAt.isSelected 'tomorrow'}}active{{/if}}")
|
||||
a.name.js-toggle-due-tomorrow-filter
|
||||
span.sidebar-list-item-description
|
||||
| {{_ 'filter-due-tomorrow' }}
|
||||
if Filter.dueAt.isSelected 'tomorrow'
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
li(class="{{#if Filter.dueAt.isSelected 'thisweek'}}active{{/if}}")
|
||||
a.name.js-toggle-due-this-week-filter
|
||||
span.sidebar-list-item-description
|
||||
| {{_ 'filter-due-this-week' }}
|
||||
if Filter.dueAt.isSelected 'thisweek'
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
li(class="{{#if Filter.dueAt.isSelected 'nextweek'}}active{{/if}}")
|
||||
a.name.js-toggle-due-next-week-filter
|
||||
span.sidebar-list-item-description
|
||||
| {{_ 'filter-due-next-week' }}
|
||||
if Filter.dueAt.isSelected 'nextweek'
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
hr
|
||||
h3
|
||||
| 📋
|
||||
|
|
@ -138,7 +138,7 @@ template(name="filterSidebar")
|
|||
span.sidebar-list-item-description
|
||||
| {{ name }}
|
||||
if Filter.customFields.isSelected _id
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
hr
|
||||
h3
|
||||
| {{_ 'other-filters-label'}}
|
||||
|
|
@ -148,14 +148,14 @@ template(name="filterSidebar")
|
|||
span.sidebar-list-item-description
|
||||
| {{_ 'filter-show-archive'}}
|
||||
if Filter.archive.isSelected _id
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
ul.sidebar-list
|
||||
li(class="{{#if Filter.hideEmpty.isSelected _id}}active{{/if}}")
|
||||
a.name.js-toggle-hideEmpty-filter
|
||||
span.sidebar-list-item-description
|
||||
| {{_ 'filter-hide-empty'}}
|
||||
if Filter.hideEmpty.isSelected _id
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
hr
|
||||
h3 {{_ 'advanced-filter-label'}}
|
||||
input.js-field-advanced-filter(type="text")
|
||||
|
|
@ -206,12 +206,6 @@ template(name="multiselectionSidebar")
|
|||
| ⋯
|
||||
if currentUser.isBoardAdmin
|
||||
hr
|
||||
a.sidebar-btn.js-selection-color
|
||||
| 🎨
|
||||
span {{_ 'selection-color'}}
|
||||
a.sidebar-btn.js-copy-selection
|
||||
| 📋
|
||||
span {{_ 'copy-selection'}}
|
||||
a.sidebar-btn.js-move-selection
|
||||
| 📤
|
||||
span {{_ 'move-selection'}}
|
||||
|
|
@ -230,76 +224,4 @@ template(name="disambiguateMultiMemberPopup")
|
|||
button.wide.js-assign-member {{_ 'assign-member'}}
|
||||
|
||||
template(name="moveSelectionPopup")
|
||||
h3 {{_ 'moveSelectionPopup-title'}}
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above-move" style="display: inline")
|
||||
label(for="position-above-move") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below-move" style="display: inline")
|
||||
label(for="position-below-move") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
||||
template(name="copySelectionPopup")
|
||||
h3 {{_ 'copySelectionPopup-title'}}
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above-copy" style="display: inline")
|
||||
label(for="position-above-copy") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below-copy" style="display: inline")
|
||||
label(for="position-below-copy") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
||||
template(name="setSelectionColorPopup")
|
||||
h3 {{_ 'setSelectionColorPopup-title'}}
|
||||
form.edit-label
|
||||
.palette-colors: each colors
|
||||
unless $eq color 'white'
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
| ✅
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
+boardLists
|
||||
|
|
|
|||
|
|
@ -162,8 +162,6 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
},
|
||||
'click .js-move-selection': Popup.open('moveSelection'),
|
||||
'click .js-copy-selection': Popup.open('copySelection'),
|
||||
'click .js-selection-color': Popup.open('setSelectionColor'),
|
||||
'click .js-archive-selection'() {
|
||||
mutateSelectedCards('archive');
|
||||
EscapeActions.executeUpTo('multiselection');
|
||||
|
|
@ -204,267 +202,10 @@ Template.disambiguateMultiMemberPopup.events({
|
|||
},
|
||||
});
|
||||
|
||||
Template.moveSelectionPopup.onCreated(function() {
|
||||
this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
|
||||
this.selectedSwimlaneId = new ReactiveVar('');
|
||||
this.selectedListId = new ReactiveVar('');
|
||||
this.selectedCardId = new ReactiveVar('');
|
||||
this.position = new ReactiveVar('above');
|
||||
|
||||
this.getBoardData = function(boardId) {
|
||||
const self = this;
|
||||
Meteor.subscribe('board', boardId, false, {
|
||||
onReady() {
|
||||
const sameBoardId = self.selectedBoardId.get() === boardId;
|
||||
self.selectedBoardId.set(boardId);
|
||||
|
||||
if (!sameBoardId) {
|
||||
self.setFirstSwimlaneId();
|
||||
self.setFirstListId();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
this.setFirstSwimlaneId = function() {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
const swimlaneId = board.swimlanes()[0]._id;
|
||||
this.selectedSwimlaneId.set(swimlaneId);
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
this.setFirstListId = function() {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
const listId = board.lists()[0]._id;
|
||||
this.selectedListId.set(listId);
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
this.getBoardData(Session.get('currentBoard'));
|
||||
this.setFirstSwimlaneId();
|
||||
this.setFirstListId();
|
||||
});
|
||||
|
||||
Template.moveSelectionPopup.helpers({
|
||||
boards() {
|
||||
return ReactiveCache.getBoards(
|
||||
{
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
_id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() },
|
||||
},
|
||||
{
|
||||
sort: { sort: 1 },
|
||||
},
|
||||
);
|
||||
},
|
||||
swimlanes() {
|
||||
const board = ReactiveCache.getBoard(Template.instance().selectedBoardId.get());
|
||||
return board ? board.swimlanes() : [];
|
||||
},
|
||||
lists() {
|
||||
const board = ReactiveCache.getBoard(Template.instance().selectedBoardId.get());
|
||||
return board ? board.lists() : [];
|
||||
},
|
||||
cards() {
|
||||
const instance = Template.instance();
|
||||
const list = ReactiveCache.getList(instance.selectedListId.get());
|
||||
if (!list) return [];
|
||||
return list.cards(instance.selectedSwimlaneId.get()).sort((a, b) => a.sort - b.sort);
|
||||
},
|
||||
isDialogOptionBoardId(boardId) {
|
||||
return Template.instance().selectedBoardId.get() === boardId;
|
||||
},
|
||||
isDialogOptionSwimlaneId(swimlaneId) {
|
||||
return Template.instance().selectedSwimlaneId.get() === swimlaneId;
|
||||
},
|
||||
isDialogOptionListId(listId) {
|
||||
return Template.instance().selectedListId.get() === listId;
|
||||
},
|
||||
});
|
||||
|
||||
Template.moveSelectionPopup.events({
|
||||
'change .js-select-boards'(event) {
|
||||
const boardId = $(event.currentTarget).val();
|
||||
Template.instance().getBoardData(boardId);
|
||||
},
|
||||
'change .js-select-swimlanes'(event) {
|
||||
Template.instance().selectedSwimlaneId.set($(event.currentTarget).val());
|
||||
},
|
||||
'change .js-select-lists'(event) {
|
||||
Template.instance().selectedListId.set($(event.currentTarget).val());
|
||||
},
|
||||
'change .js-select-cards'(event) {
|
||||
Template.instance().selectedCardId.set($(event.currentTarget).val());
|
||||
},
|
||||
'change input[name="position"]'(event) {
|
||||
Template.instance().position.set($(event.currentTarget).val());
|
||||
},
|
||||
'click .js-done'() {
|
||||
const instance = Template.instance();
|
||||
const boardId = instance.selectedBoardId.get();
|
||||
const swimlaneId = instance.selectedSwimlaneId.get();
|
||||
const listId = instance.selectedListId.get();
|
||||
const cardId = instance.selectedCardId.get();
|
||||
const position = instance.position.get();
|
||||
|
||||
// Calculate sortIndex
|
||||
let sortIndex = 0;
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
if (position === 'above') {
|
||||
sortIndex = targetCard.sort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetCard.sort + 0.5;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no card selected, move to end
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
const cards = board.cards({ swimlaneId, listId }).sort('sort');
|
||||
if (cards.length > 0) {
|
||||
sortIndex = cards[cards.length - 1].sort + 1;
|
||||
}
|
||||
}
|
||||
|
||||
mutateSelectedCards('move', boardId, swimlaneId, listId, sortIndex);
|
||||
EscapeActions.executeUpTo('multiselection');
|
||||
},
|
||||
});
|
||||
|
||||
Template.copySelectionPopup.onCreated(function() {
|
||||
this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
|
||||
this.selectedSwimlaneId = new ReactiveVar('');
|
||||
this.selectedListId = new ReactiveVar('');
|
||||
this.selectedCardId = new ReactiveVar('');
|
||||
this.position = new ReactiveVar('above');
|
||||
|
||||
this.getBoardData = function(boardId) {
|
||||
const self = this;
|
||||
Meteor.subscribe('board', boardId, false, {
|
||||
onReady() {
|
||||
const sameBoardId = self.selectedBoardId.get() === boardId;
|
||||
self.selectedBoardId.set(boardId);
|
||||
|
||||
if (!sameBoardId) {
|
||||
self.setFirstSwimlaneId();
|
||||
self.setFirstListId();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
this.setFirstSwimlaneId = function() {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
const swimlaneId = board.swimlanes()[0]._id;
|
||||
this.selectedSwimlaneId.set(swimlaneId);
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
this.setFirstListId = function() {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
const listId = board.lists()[0]._id;
|
||||
this.selectedListId.set(listId);
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
this.getBoardData(Session.get('currentBoard'));
|
||||
this.setFirstSwimlaneId();
|
||||
this.setFirstListId();
|
||||
});
|
||||
|
||||
Template.copySelectionPopup.helpers({
|
||||
boards() {
|
||||
return ReactiveCache.getBoards(
|
||||
{
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
_id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() },
|
||||
},
|
||||
{
|
||||
sort: { sort: 1 },
|
||||
},
|
||||
);
|
||||
},
|
||||
swimlanes() {
|
||||
const board = ReactiveCache.getBoard(Template.instance().selectedBoardId.get());
|
||||
return board ? board.swimlanes() : [];
|
||||
},
|
||||
lists() {
|
||||
const board = ReactiveCache.getBoard(Template.instance().selectedBoardId.get());
|
||||
return board ? board.lists() : [];
|
||||
},
|
||||
cards() {
|
||||
const instance = Template.instance();
|
||||
const list = ReactiveCache.getList(instance.selectedListId.get());
|
||||
if (!list) return [];
|
||||
return list.cards(instance.selectedSwimlaneId.get()).sort((a, b) => a.sort - b.sort);
|
||||
},
|
||||
isDialogOptionBoardId(boardId) {
|
||||
return Template.instance().selectedBoardId.get() === boardId;
|
||||
},
|
||||
isDialogOptionSwimlaneId(swimlaneId) {
|
||||
return Template.instance().selectedSwimlaneId.get() === swimlaneId;
|
||||
},
|
||||
isDialogOptionListId(listId) {
|
||||
return Template.instance().selectedListId.get() === listId;
|
||||
},
|
||||
});
|
||||
|
||||
Template.copySelectionPopup.events({
|
||||
'change .js-select-boards'(event) {
|
||||
const boardId = $(event.currentTarget).val();
|
||||
Template.instance().getBoardData(boardId);
|
||||
},
|
||||
'change .js-select-swimlanes'(event) {
|
||||
Template.instance().selectedSwimlaneId.set($(event.currentTarget).val());
|
||||
},
|
||||
'change .js-select-lists'(event) {
|
||||
Template.instance().selectedListId.set($(event.currentTarget).val());
|
||||
},
|
||||
'change .js-select-cards'(event) {
|
||||
Template.instance().selectedCardId.set($(event.currentTarget).val());
|
||||
},
|
||||
'change input[name="position"]'(event) {
|
||||
Template.instance().position.set($(event.currentTarget).val());
|
||||
},
|
||||
'click .js-done'() {
|
||||
const instance = Template.instance();
|
||||
const boardId = instance.selectedBoardId.get();
|
||||
const swimlaneId = instance.selectedSwimlaneId.get();
|
||||
const listId = instance.selectedListId.get();
|
||||
const cardId = instance.selectedCardId.get();
|
||||
const position = instance.position.get();
|
||||
|
||||
mutateSelectedCards((card) => {
|
||||
const newCard = card.copy(boardId, swimlaneId, listId);
|
||||
if (newCard) {
|
||||
let sortIndex = 0;
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
if (position === 'above') {
|
||||
sortIndex = targetCard.sort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetCard.sort + 0.5;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// To end
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
const cards = board.cards({ swimlaneId, listId }).sort('sort');
|
||||
if (cards.length > 0) {
|
||||
sortIndex = cards[cards.length - 1].sort + 1;
|
||||
}
|
||||
}
|
||||
newCard.setSort(sortIndex);
|
||||
}
|
||||
});
|
||||
'click .js-select-list'() {
|
||||
// Move the minicard to the end of the target list
|
||||
mutateSelectedCards('moveToEndOfList', { listId: this._id });
|
||||
EscapeActions.executeUpTo('multiselection');
|
||||
},
|
||||
});
|
||||
|
|
|
|||
109
client/components/sidebar/sidebarMigrations.jade
Normal file
109
client/components/sidebar/sidebarMigrations.jade
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
template(name='migrationsSidebar')
|
||||
if currentUser.isBoardAdmin
|
||||
.sidebar-migrations
|
||||
h3
|
||||
| 🔧
|
||||
| {{_ 'migrations'}}
|
||||
p.quiet {{_ 'migrations-description'}}
|
||||
|
||||
.migrations-list
|
||||
h4 {{_ 'board-migrations'}}
|
||||
.migration-item
|
||||
a.js-run-migration(data-migration="comprehensive")
|
||||
.migration-name
|
||||
| {{_ 'comprehensive-board-migration'}}
|
||||
.migration-status
|
||||
if comprehensiveMigrationNeeded
|
||||
span.badge.badge-warning {{_ 'migration-needed'}}
|
||||
else
|
||||
span.badge.badge-success {{_ 'migration-complete'}}
|
||||
|
||||
.migration-item
|
||||
a.js-run-migration(data-migration="fixMissingLists")
|
||||
.migration-name
|
||||
| {{_ 'fix-missing-lists-migration'}}
|
||||
.migration-status
|
||||
if fixMissingListsNeeded
|
||||
span.badge.badge-warning {{_ 'migration-needed'}}
|
||||
else
|
||||
span.badge.badge-success {{_ 'migration-complete'}}
|
||||
|
||||
.migration-item
|
||||
a.js-run-migration(data-migration="deleteDuplicateEmptyLists")
|
||||
.migration-name
|
||||
| {{_ 'delete-duplicate-empty-lists-migration'}}
|
||||
.migration-status
|
||||
if deleteDuplicateEmptyListsNeeded
|
||||
span.badge.badge-warning {{_ 'migration-needed'}}
|
||||
else
|
||||
span.badge.badge-success {{_ 'migration-complete'}}
|
||||
|
||||
.migration-item
|
||||
a.js-run-migration(data-migration="restoreLostCards")
|
||||
.migration-name
|
||||
| {{_ 'restore-lost-cards-migration'}}
|
||||
.migration-status
|
||||
if restoreLostCardsNeeded
|
||||
span.badge.badge-warning {{_ 'migration-needed'}}
|
||||
else
|
||||
span.badge.badge-success {{_ 'migration-complete'}}
|
||||
|
||||
.migration-item
|
||||
a.js-run-migration(data-migration="restoreAllArchived")
|
||||
.migration-name
|
||||
| {{_ 'restore-all-archived-migration'}}
|
||||
.migration-status
|
||||
if restoreAllArchivedNeeded
|
||||
span.badge.badge-warning {{_ 'migration-needed'}}
|
||||
else
|
||||
span.badge.badge-success {{_ 'migration-complete'}}
|
||||
|
||||
.migration-item
|
||||
a.js-run-migration(data-migration="fixAvatarUrls")
|
||||
.migration-name
|
||||
| {{_ 'fix-avatar-urls-migration'}}
|
||||
.migration-status
|
||||
if fixAvatarUrlsNeeded
|
||||
span.badge.badge-warning {{_ 'migration-needed'}}
|
||||
else
|
||||
span.badge.badge-success {{_ 'migration-complete'}}
|
||||
|
||||
.migration-item
|
||||
a.js-run-migration(data-migration="fixAllFileUrls")
|
||||
.migration-name
|
||||
| {{_ 'fix-all-file-urls-migration'}}
|
||||
.migration-status
|
||||
if fixAllFileUrlsNeeded
|
||||
span.badge.badge-warning {{_ 'migration-needed'}}
|
||||
else
|
||||
span.badge.badge-success {{_ 'migration-complete'}}
|
||||
else
|
||||
p.quiet {{_ 'migrations-admin-only'}}
|
||||
|
||||
template(name='runComprehensiveMigrationPopup')
|
||||
p {{_ 'run-comprehensive-migration-confirm'}}
|
||||
button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}}
|
||||
|
||||
template(name='runFixMissingListsMigrationPopup')
|
||||
p {{_ 'run-fix-missing-lists-migration-confirm'}}
|
||||
button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}}
|
||||
|
||||
template(name='runDeleteDuplicateEmptyListsMigrationPopup')
|
||||
p {{_ 'run-delete-duplicate-empty-lists-migration-confirm'}}
|
||||
button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}}
|
||||
|
||||
template(name='runRestoreLostCardsMigrationPopup')
|
||||
p {{_ 'run-restore-lost-cards-migration-confirm'}}
|
||||
button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}}
|
||||
|
||||
template(name='runRestoreAllArchivedMigrationPopup')
|
||||
p {{_ 'run-restore-all-archived-migration-confirm'}}
|
||||
button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}}
|
||||
|
||||
template(name='runFixAvatarUrlsMigrationPopup')
|
||||
p {{_ 'run-fix-avatar-urls-migration-confirm'}}
|
||||
button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}}
|
||||
|
||||
template(name='runFixAllFileUrlsMigrationPopup')
|
||||
p {{_ 'run-fix-all-file-urls-migration-confirm'}}
|
||||
button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}}
|
||||
341
client/components/sidebar/sidebarMigrations.js
Normal file
341
client/components/sidebar/sidebarMigrations.js
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { migrationProgressManager } from '/client/components/migrationProgress';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.migrationStatuses = new ReactiveVar({});
|
||||
this.loadMigrationStatuses();
|
||||
},
|
||||
|
||||
loadMigrationStatuses() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
if (!boardId) return;
|
||||
|
||||
// Check comprehensive migration
|
||||
Meteor.call('comprehensiveBoardMigration.needsMigration', boardId, (err, res) => {
|
||||
if (!err) {
|
||||
const statuses = this.migrationStatuses.get();
|
||||
statuses.comprehensive = res;
|
||||
this.migrationStatuses.set(statuses);
|
||||
}
|
||||
});
|
||||
|
||||
// Check fix missing lists migration
|
||||
Meteor.call('fixMissingListsMigration.needsMigration', boardId, (err, res) => {
|
||||
if (!err) {
|
||||
const statuses = this.migrationStatuses.get();
|
||||
statuses.fixMissingLists = res;
|
||||
this.migrationStatuses.set(statuses);
|
||||
}
|
||||
});
|
||||
|
||||
// Check delete duplicate empty lists migration
|
||||
Meteor.call('deleteDuplicateEmptyLists.needsMigration', boardId, (err, res) => {
|
||||
if (!err) {
|
||||
const statuses = this.migrationStatuses.get();
|
||||
statuses.deleteDuplicateEmptyLists = res;
|
||||
this.migrationStatuses.set(statuses);
|
||||
}
|
||||
});
|
||||
|
||||
// Check restore lost cards migration
|
||||
Meteor.call('restoreLostCards.needsMigration', boardId, (err, res) => {
|
||||
if (!err) {
|
||||
const statuses = this.migrationStatuses.get();
|
||||
statuses.restoreLostCards = res;
|
||||
this.migrationStatuses.set(statuses);
|
||||
}
|
||||
});
|
||||
|
||||
// Check restore all archived migration
|
||||
Meteor.call('restoreAllArchived.needsMigration', boardId, (err, res) => {
|
||||
if (!err) {
|
||||
const statuses = this.migrationStatuses.get();
|
||||
statuses.restoreAllArchived = res;
|
||||
this.migrationStatuses.set(statuses);
|
||||
}
|
||||
});
|
||||
|
||||
// Check fix avatar URLs migration (board-specific)
|
||||
Meteor.call('fixAvatarUrls.needsMigration', boardId, (err, res) => {
|
||||
if (!err) {
|
||||
const statuses = this.migrationStatuses.get();
|
||||
statuses.fixAvatarUrls = res;
|
||||
this.migrationStatuses.set(statuses);
|
||||
}
|
||||
});
|
||||
|
||||
// Check fix all file URLs migration (board-specific)
|
||||
Meteor.call('fixAllFileUrls.needsMigration', boardId, (err, res) => {
|
||||
if (!err) {
|
||||
const statuses = this.migrationStatuses.get();
|
||||
statuses.fixAllFileUrls = res;
|
||||
this.migrationStatuses.set(statuses);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
comprehensiveMigrationNeeded() {
|
||||
return this.migrationStatuses.get().comprehensive === true;
|
||||
},
|
||||
|
||||
fixMissingListsNeeded() {
|
||||
return this.migrationStatuses.get().fixMissingLists === true;
|
||||
},
|
||||
|
||||
deleteDuplicateEmptyListsNeeded() {
|
||||
return this.migrationStatuses.get().deleteDuplicateEmptyLists === true;
|
||||
},
|
||||
|
||||
restoreLostCardsNeeded() {
|
||||
return this.migrationStatuses.get().restoreLostCards === true;
|
||||
},
|
||||
|
||||
restoreAllArchivedNeeded() {
|
||||
return this.migrationStatuses.get().restoreAllArchived === true;
|
||||
},
|
||||
|
||||
fixAvatarUrlsNeeded() {
|
||||
return this.migrationStatuses.get().fixAvatarUrls === true;
|
||||
},
|
||||
|
||||
fixAllFileUrlsNeeded() {
|
||||
return this.migrationStatuses.get().fixAllFileUrls === true;
|
||||
},
|
||||
|
||||
// Simulate migration progress updates using the global progress popup
|
||||
async simulateMigrationProgress(progressSteps) {
|
||||
const totalSteps = progressSteps.length;
|
||||
for (let i = 0; i < progressSteps.length; i++) {
|
||||
const step = progressSteps[i];
|
||||
const overall = Math.round(((i + 1) / totalSteps) * 100);
|
||||
|
||||
// Start step
|
||||
migrationProgressManager.updateProgress({
|
||||
overallProgress: overall,
|
||||
currentStep: i + 1,
|
||||
totalSteps,
|
||||
stepName: step.step,
|
||||
stepProgress: 0,
|
||||
stepStatus: `Starting ${step.name}...`,
|
||||
stepDetails: null,
|
||||
boardId: Session.get('currentBoard'),
|
||||
});
|
||||
|
||||
const stepDuration = step.duration;
|
||||
const updateInterval = 100;
|
||||
const totalUpdates = Math.max(1, Math.floor(stepDuration / updateInterval));
|
||||
for (let j = 0; j < totalUpdates; j++) {
|
||||
const per = Math.round(((j + 1) / totalUpdates) * 100);
|
||||
migrationProgressManager.updateProgress({
|
||||
overallProgress: overall,
|
||||
currentStep: i + 1,
|
||||
totalSteps,
|
||||
stepName: step.step,
|
||||
stepProgress: per,
|
||||
stepStatus: `Processing ${step.name}...`,
|
||||
stepDetails: { progress: `${per}%` },
|
||||
boardId: Session.get('currentBoard'),
|
||||
});
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((r) => setTimeout(r, updateInterval));
|
||||
}
|
||||
|
||||
// Complete step
|
||||
migrationProgressManager.updateProgress({
|
||||
overallProgress: overall,
|
||||
currentStep: i + 1,
|
||||
totalSteps,
|
||||
stepName: step.step,
|
||||
stepProgress: 100,
|
||||
stepStatus: `${step.name} completed`,
|
||||
stepDetails: { status: 'completed' },
|
||||
boardId: Session.get('currentBoard'),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
runMigration(migrationType) {
|
||||
const boardId = Session.get('currentBoard');
|
||||
|
||||
let methodName;
|
||||
let methodArgs = [];
|
||||
|
||||
switch (migrationType) {
|
||||
case 'comprehensive':
|
||||
methodName = 'comprehensiveBoardMigration.execute';
|
||||
methodArgs = [boardId];
|
||||
break;
|
||||
|
||||
case 'fixMissingLists':
|
||||
methodName = 'fixMissingListsMigration.execute';
|
||||
methodArgs = [boardId];
|
||||
break;
|
||||
|
||||
case 'deleteDuplicateEmptyLists':
|
||||
methodName = 'deleteDuplicateEmptyLists.execute';
|
||||
methodArgs = [boardId];
|
||||
break;
|
||||
|
||||
case 'restoreLostCards':
|
||||
methodName = 'restoreLostCards.execute';
|
||||
methodArgs = [boardId];
|
||||
break;
|
||||
|
||||
case 'restoreAllArchived':
|
||||
methodName = 'restoreAllArchived.execute';
|
||||
methodArgs = [boardId];
|
||||
break;
|
||||
|
||||
case 'fixAvatarUrls':
|
||||
methodName = 'fixAvatarUrls.execute';
|
||||
methodArgs = [boardId];
|
||||
break;
|
||||
|
||||
case 'fixAllFileUrls':
|
||||
methodName = 'fixAllFileUrls.execute';
|
||||
methodArgs = [boardId];
|
||||
break;
|
||||
}
|
||||
|
||||
if (methodName) {
|
||||
// Define simulated steps per migration type
|
||||
const stepsByType = {
|
||||
comprehensive: [
|
||||
{ step: 'analyze_board_structure', name: 'Analyze Board Structure', duration: 800 },
|
||||
{ step: 'fix_orphaned_cards', name: 'Fix Orphaned Cards', duration: 1200 },
|
||||
{ step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 1000 },
|
||||
{ step: 'ensure_per_swimlane_lists', name: 'Ensure Per-Swimlane Lists', duration: 800 },
|
||||
{ step: 'validate_migration', name: 'Validate Migration', duration: 800 },
|
||||
{ step: 'fix_avatar_urls', name: 'Fix Avatar URLs', duration: 600 },
|
||||
{ step: 'fix_attachment_urls', name: 'Fix Attachment URLs', duration: 600 },
|
||||
],
|
||||
fixMissingLists: [
|
||||
{ step: 'analyze_lists', name: 'Analyze Lists', duration: 600 },
|
||||
{ step: 'create_missing_lists', name: 'Create Missing Lists', duration: 900 },
|
||||
{ step: 'update_cards', name: 'Update Cards', duration: 900 },
|
||||
{ step: 'finalize', name: 'Finalize', duration: 400 },
|
||||
],
|
||||
deleteDuplicateEmptyLists: [
|
||||
{ step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 700 },
|
||||
{ step: 'delete_duplicate_empty_lists', name: 'Delete Duplicate Empty Lists', duration: 800 },
|
||||
],
|
||||
restoreLostCards: [
|
||||
{ step: 'ensure_lost_cards_swimlane', name: 'Ensure Lost Cards Swimlane', duration: 600 },
|
||||
{ step: 'restore_lists', name: 'Restore Lists', duration: 800 },
|
||||
{ step: 'restore_cards', name: 'Restore Cards', duration: 1000 },
|
||||
],
|
||||
restoreAllArchived: [
|
||||
{ step: 'restore_swimlanes', name: 'Restore Swimlanes', duration: 800 },
|
||||
{ step: 'restore_lists', name: 'Restore Lists', duration: 900 },
|
||||
{ step: 'restore_cards', name: 'Restore Cards', duration: 1000 },
|
||||
{ step: 'fix_missing_ids', name: 'Fix Missing IDs', duration: 600 },
|
||||
],
|
||||
fixAvatarUrls: [
|
||||
{ step: 'scan_users', name: 'Checking board member avatars', duration: 500 },
|
||||
{ step: 'fix_urls', name: 'Fixing avatar URLs', duration: 900 },
|
||||
],
|
||||
fixAllFileUrls: [
|
||||
{ step: 'scan_files', name: 'Checking board file attachments', duration: 600 },
|
||||
{ step: 'fix_urls', name: 'Fixing file URLs', duration: 1000 },
|
||||
],
|
||||
};
|
||||
|
||||
const steps = stepsByType[migrationType] || [
|
||||
{ step: 'running', name: 'Running Migration', duration: 1000 },
|
||||
];
|
||||
|
||||
// Kick off popup and simulated progress
|
||||
migrationProgressManager.startMigration();
|
||||
const progressPromise = this.simulateMigrationProgress(steps);
|
||||
|
||||
// Start migration call
|
||||
const callPromise = new Promise((resolve, reject) => {
|
||||
Meteor.call(methodName, ...methodArgs, (err, result) => {
|
||||
if (err) return reject(err);
|
||||
return resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
Promise.allSettled([callPromise, progressPromise]).then(([callRes]) => {
|
||||
if (callRes.status === 'rejected') {
|
||||
migrationProgressManager.failMigration(callRes.reason);
|
||||
} else {
|
||||
const result = callRes.value;
|
||||
// Summarize result details in the popup
|
||||
let summary = {};
|
||||
if (result && result.results) {
|
||||
// Comprehensive returns {success, results}
|
||||
const r = result.results;
|
||||
summary = {
|
||||
totalCardsProcessed: r.totalCardsProcessed,
|
||||
totalListsProcessed: r.totalListsProcessed,
|
||||
totalListsCreated: r.totalListsCreated,
|
||||
};
|
||||
} else if (result && result.changes) {
|
||||
// Many migrations return a changes string array
|
||||
summary = { changes: result.changes.join(' | ') };
|
||||
} else if (result && typeof result === 'object') {
|
||||
summary = result;
|
||||
}
|
||||
|
||||
migrationProgressManager.updateProgress({
|
||||
overallProgress: 100,
|
||||
currentStep: steps.length,
|
||||
totalSteps: steps.length,
|
||||
stepName: 'completed',
|
||||
stepProgress: 100,
|
||||
stepStatus: 'Migration completed',
|
||||
stepDetails: summary,
|
||||
boardId: Session.get('currentBoard'),
|
||||
});
|
||||
|
||||
migrationProgressManager.completeMigration();
|
||||
|
||||
// Refresh status badges slightly after
|
||||
Meteor.setTimeout(() => {
|
||||
this.loadMigrationStatuses();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
const self = this; // Capture component reference
|
||||
|
||||
return [
|
||||
{
|
||||
'click .js-run-migration[data-migration="comprehensive"]': Popup.afterConfirm('runComprehensiveMigration', function() {
|
||||
self.runMigration('comprehensive');
|
||||
Popup.back();
|
||||
}),
|
||||
'click .js-run-migration[data-migration="fixMissingLists"]': Popup.afterConfirm('runFixMissingListsMigration', function() {
|
||||
self.runMigration('fixMissingLists');
|
||||
Popup.back();
|
||||
}),
|
||||
'click .js-run-migration[data-migration="deleteDuplicateEmptyLists"]': Popup.afterConfirm('runDeleteDuplicateEmptyListsMigration', function() {
|
||||
self.runMigration('deleteDuplicateEmptyLists');
|
||||
Popup.back();
|
||||
}),
|
||||
'click .js-run-migration[data-migration="restoreLostCards"]': Popup.afterConfirm('runRestoreLostCardsMigration', function() {
|
||||
self.runMigration('restoreLostCards');
|
||||
Popup.back();
|
||||
}),
|
||||
'click .js-run-migration[data-migration="restoreAllArchived"]': Popup.afterConfirm('runRestoreAllArchivedMigration', function() {
|
||||
self.runMigration('restoreAllArchived');
|
||||
Popup.back();
|
||||
}),
|
||||
'click .js-run-migration[data-migration="fixAvatarUrls"]': Popup.afterConfirm('runFixAvatarUrlsMigration', function() {
|
||||
self.runMigration('fixAvatarUrls');
|
||||
Popup.back();
|
||||
}),
|
||||
'click .js-run-migration[data-migration="fixAllFileUrls"]': Popup.afterConfirm('runFixAllFileUrlsMigration', function() {
|
||||
self.runMigration('fixAllFileUrls');
|
||||
Popup.back();
|
||||
}),
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('migrationsSidebar');
|
||||
|
|
@ -26,21 +26,25 @@ template(name="swimlaneFixedHeader")
|
|||
if currentUser
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isWorker
|
||||
a.swimlane-collapse-indicator.js-collapse-swimlane.swimlane-header-collapse(title="{{_ 'collapse'}}")
|
||||
if collapseSwimlane
|
||||
| ▶
|
||||
else
|
||||
| 🔽
|
||||
a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
|
||||
| ➕
|
||||
a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
|
||||
| ☰
|
||||
//// TODO: Collapse Swimlane: make button working, etc.
|
||||
//unless collapsed
|
||||
// a.js-collapse-swimlane(title="{{_ 'collapse'}}")
|
||||
// i.fa.fa-arrow-down.swimlane-header-collapse-down
|
||||
// ⬆️.swimlane-header-collapse-up
|
||||
//if collapsed
|
||||
// a.js-collapse-swimlane(title="{{_ 'uncollapse'}}")
|
||||
// ⬆️.swimlane-header-collapse-up
|
||||
// i.fa.fa-arrow-down.swimlane-header-collapse-down
|
||||
unless isTouchScreen
|
||||
a.swimlane-header-handle.handle.js-swimlane-header-handle
|
||||
| ↕️
|
||||
if isTouchScreen
|
||||
a.swimlane-header-miniscreen-handle.handle.js-swimlane-header-handle
|
||||
| ↕️
|
||||
a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
|
||||
| ☰
|
||||
|
||||
template(name="editSwimlaneTitleForm")
|
||||
.list-composer
|
||||
|
|
|
|||
|
|
@ -20,14 +20,13 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
collapsed(check = undefined) {
|
||||
const swimlane = Template.currentData();
|
||||
const status = Utils.getSwimlaneCollapseState(swimlane);
|
||||
const status = swimlane.isCollapsed();
|
||||
if (check === undefined) {
|
||||
// just check
|
||||
return status;
|
||||
} else {
|
||||
const next = typeof check === 'boolean' ? check : !status;
|
||||
Utils.setSwimlaneCollapseState(swimlane, next);
|
||||
return next;
|
||||
swimlane.collapse(!status);
|
||||
return !status;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -50,10 +49,6 @@ Template.swimlaneFixedHeader.helpers({
|
|||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
collapseSwimlane() {
|
||||
const swimlane = Template.currentData();
|
||||
return Utils.getSwimlaneCollapseState(swimlane);
|
||||
},
|
||||
isTitleDefault(title) {
|
||||
// https://github.com/wekan/wekan/issues/4763
|
||||
// https://github.com/wekan/wekan/issues/4742
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue