mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 23:40:13 +01:00
Compare commits
199 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
614cb44b55 | ||
|
|
0ce8e8b74d | ||
|
|
4ea53af76e | ||
|
|
016f17d663 | ||
|
|
07f69950a7 | ||
|
|
cec625607d | ||
|
|
a290c7b34b | ||
|
|
5b77ac1b44 | ||
|
|
41c635afb5 | ||
|
|
adbf729cb2 | ||
|
|
88ea716d63 | ||
|
|
003a07ebce | ||
|
|
d3c237bc66 | ||
|
|
bac0fa81fc | ||
|
|
a42915614a | ||
|
|
5ff9bf331f | ||
|
|
36d7b0f8a7 | ||
|
|
67c8a98f20 | ||
|
|
a81a603031 | ||
|
|
e30ce78053 | ||
|
|
3d70de94c6 | ||
|
|
70975c2944 | ||
|
|
960e2126b4 | ||
|
|
3db1305e58 | ||
|
|
f16780b5e3 | ||
|
|
37a3065f3c | ||
|
|
7ff1649d89 | ||
|
|
a39ae31b45 | ||
|
|
6302a48221 | ||
|
|
c277bee9d2 | ||
|
|
c5f5ce126d | ||
|
|
0004ae716b | ||
|
|
7f53dfac3c | ||
|
|
18003900c2 | ||
|
|
fe104791b5 | ||
|
|
6244657ca5 | ||
|
|
46866dac85 | ||
|
|
c829c073cf | ||
|
|
0772ca4036 | ||
|
|
581733d605 | ||
|
|
b02af27ac3 | ||
|
|
20af0a2ef5 | ||
|
|
c58ab5b07d | ||
|
|
e5e711c938 | ||
|
|
42594abe4e | ||
|
|
0afbdc95b4 | ||
|
|
16a74bb748 | ||
|
|
8711b476be | ||
|
|
df9fba4765 | ||
|
|
7d27139aa9 | ||
|
|
e4638d5fbc | ||
|
|
bc5854dd29 | ||
|
|
ba49d4d140 | ||
|
|
71b7dcffb5 | ||
|
|
7713e613b4 | ||
|
|
91a0aa7387 | ||
|
|
fbd6b920ef | ||
|
|
1b25d1d572 | ||
|
|
e93e72234c | ||
|
|
15d9b0ae3a | ||
|
|
550d87ac6c | ||
|
|
f8e576e890 | ||
|
|
fb8ef4d978 | ||
|
|
5127e87898 | ||
|
|
3f2d4444e4 | ||
|
|
9c7badb0eb | ||
|
|
9d9f77a731 | ||
|
|
c400ce74b1 | ||
|
|
c2e20ee4a3 | ||
|
|
ccd9034339 | ||
|
|
0a1a075f31 | ||
|
|
4aaeec9515 | ||
|
|
ea310d7508 | ||
|
|
0a2e6a0c38 | ||
|
|
f26d582018 | ||
|
|
e9a727301d | ||
|
|
d64d2f9c42 | ||
|
|
5c0d122e84 | ||
|
|
5079c853a7 | ||
|
|
b039ba12a2 | ||
|
|
3323ac6ac1 | ||
|
|
3204311ac1 | ||
|
|
0fc2ad97cd | ||
|
|
30620d0ca4 | ||
|
|
bccc22c5fe | ||
|
|
ecf2418347 | ||
|
|
0c99cb3103 | ||
|
|
034dc08269 | ||
|
|
d1a51b42f6 | ||
|
|
92bfbb2d0c | ||
|
|
91b846e2cd | ||
|
|
7fe7fb4c15 | ||
|
|
0cebd8aa4d | ||
|
|
8662c96d1c | ||
|
|
0cbc9402f3 | ||
|
|
940df02456 | ||
|
|
b4b598f542 | ||
|
|
ef19c35b5a | ||
|
|
fc98120269 | ||
|
|
b8a3d6deaf | ||
|
|
45537ede87 | ||
|
|
29a9c5bc7b | ||
|
|
7ca81285b1 | ||
|
|
49a865cdbf | ||
|
|
a0c30c35ed | ||
|
|
de20424885 | ||
|
|
f7e09ae89c | ||
|
|
c6d4600683 | ||
|
|
bd1837ee36 | ||
|
|
544b24ceb1 | ||
|
|
0825374183 | ||
|
|
b053fb8e61 | ||
|
|
ae11e80bde | ||
|
|
8e296231ba | ||
|
|
49891eff36 | ||
|
|
58df525b49 | ||
|
|
1761f43afa | ||
|
|
37d7d938c5 | ||
|
|
b7ca2310b2 | ||
|
|
c562b3969a | ||
|
|
d1d553e8d7 | ||
|
|
b6e7b258e0 | ||
|
|
c7bbe47221 | ||
|
|
347fa9e5cd | ||
|
|
07ce151508 | ||
|
|
665c9b5e52 | ||
|
|
9399a0c545 | ||
|
|
a540b12895 | ||
|
|
e29d9dcd17 | ||
|
|
1aa0d84977 | ||
|
|
7f31d7c812 | ||
|
|
4987a95d8e | ||
|
|
ef7771febb | ||
|
|
12cba0e148 | ||
|
|
c3a4052227 | ||
|
|
82f048ccef | ||
|
|
7a585a3dfb | ||
|
|
8d3b53f51d | ||
|
|
d73e006935 | ||
|
|
9536e60bd1 | ||
|
|
678ca978a3 | ||
|
|
39420877fd | ||
|
|
6ea03cfba3 | ||
|
|
9214b56aea | ||
|
|
699b4c464f | ||
|
|
9fa54a3148 | ||
|
|
f2019b1059 | ||
|
|
714bbd0fb0 | ||
|
|
80777b4663 | ||
|
|
9473c1fe41 | ||
|
|
98f141d62f | ||
|
|
85dd213b14 | ||
|
|
3cf00911f7 | ||
|
|
bddaad8346 | ||
|
|
5df4efd7ba | ||
|
|
59df6aad05 | ||
|
|
c4af4d03ac | ||
|
|
62679819d9 | ||
|
|
46d46e313c | ||
|
|
27e9d3ce47 | ||
|
|
b6b0c5fe6d | ||
|
|
87b934a955 | ||
|
|
8cc6e9b812 | ||
|
|
b7da17ff31 | ||
|
|
2dd3916f7e | ||
|
|
516552cce6 | ||
|
|
2d44881619 | ||
|
|
0acbf30b03 | ||
|
|
e61f6b1c89 | ||
|
|
973a49526f | ||
|
|
e1902d58c1 | ||
|
|
1e53125499 | ||
|
|
91fb7d9e70 | ||
|
|
eb6b42c4c9 | ||
|
|
679d210667 | ||
|
|
1e6252de7f | ||
|
|
48b645ee1e | ||
|
|
951d2e4937 | ||
|
|
1658883b78 | ||
|
|
3514335247 | ||
|
|
55bec31a3f | ||
|
|
0d36abee4e | ||
|
|
caa6e615ff | ||
|
|
cd3576b995 | ||
|
|
324f3f7794 | ||
|
|
3257110673 | ||
|
|
66b444e2b0 | ||
|
|
23860b1ee8 | ||
|
|
101048339b | ||
|
|
dc78e3b7a0 | ||
|
|
d965faa317 | ||
|
|
5d2bfab0f5 | ||
|
|
841a6eaf8c | ||
|
|
db59bb4aa4 | ||
|
|
61f7099106 | ||
|
|
ef828bdd38 | ||
|
|
1134b45b05 | ||
|
|
b06daff4c7 | ||
|
|
cea414b589 |
267 changed files with 26535 additions and 4576 deletions
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: wekan
|
||||||
custom: ['https://wekan.fi/commercial-support/']
|
custom: ['https://wekan.fi/commercial-support/']
|
||||||
|
|
|
||||||
2
.github/workflows/depsreview.yaml
vendored
2
.github/workflows/depsreview.yaml
vendored
|
|
@ -9,6 +9,6 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: 'Checkout Repository'
|
- name: 'Checkout Repository'
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
- name: 'Dependency Review'
|
- name: 'Dependency Review'
|
||||||
uses: actions/dependency-review-action@v4
|
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:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# Login against a Docker registry except on PR
|
# Login against a Docker registry except on PR
|
||||||
# https://github.com/docker/login-action
|
# https://github.com/docker/login-action
|
||||||
|
|
@ -48,7 +48,7 @@ jobs:
|
||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
- name: Extract Docker metadata
|
- name: Extract Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
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
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- name: Build the Docker image
|
- name: Build the Docker image
|
||||||
run: docker build . --file Dockerfile --tag wekan:$(date +%s)
|
run: docker build . --file Dockerfile --tag wekan:$(date +%s)
|
||||||
|
|
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
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
|
# runs-on: ubuntu-latest
|
||||||
# steps:
|
# steps:
|
||||||
# - name: checkout
|
# - name: checkout
|
||||||
# uses: actions/checkout@v5
|
# uses: actions/checkout@v6
|
||||||
#
|
#
|
||||||
# - name: setup node
|
# - name: setup node
|
||||||
# uses: actions/setup-node@v1
|
# uses: actions/setup-node@v1
|
||||||
|
|
@ -42,7 +42,7 @@ jobs:
|
||||||
# needs: [lintcode]
|
# needs: [lintcode]
|
||||||
# steps:
|
# steps:
|
||||||
# - name: checkout
|
# - name: checkout
|
||||||
# uses: actions/checkout@v5
|
# uses: actions/checkout@v6
|
||||||
#
|
#
|
||||||
# - name: setup node
|
# - name: setup node
|
||||||
# uses: actions/setup-node@v1
|
# uses: actions/setup-node@v1
|
||||||
|
|
@ -65,7 +65,7 @@ jobs:
|
||||||
# needs: [lintcode,lintstyle]
|
# needs: [lintcode,lintstyle]
|
||||||
# steps:
|
# steps:
|
||||||
# - name: checkout
|
# - name: checkout
|
||||||
# uses: actions/checkout@v5
|
# uses: actions/checkout@v6
|
||||||
#
|
#
|
||||||
# - name: setup node
|
# - name: setup node
|
||||||
# uses: actions/setup-node@v1
|
# uses: actions/setup-node@v1
|
||||||
|
|
@ -90,12 +90,12 @@ jobs:
|
||||||
|
|
||||||
# CHECKOUTS
|
# CHECKOUTS
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# CACHING
|
# CACHING
|
||||||
- name: Install Meteor
|
- name: Install Meteor
|
||||||
id: cache-meteor-install
|
id: cache-meteor-install
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.meteor
|
path: ~/.meteor
|
||||||
key: v1-meteor-${{ hashFiles('.meteor/versions') }}
|
key: v1-meteor-${{ hashFiles('.meteor/versions') }}
|
||||||
|
|
@ -104,7 +104,7 @@ jobs:
|
||||||
|
|
||||||
- name: Cache NPM dependencies
|
- name: Cache NPM dependencies
|
||||||
id: cache-meteor-npm
|
id: cache-meteor-npm
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: v1-npm-${{ hashFiles('package-lock.json') }}
|
key: v1-npm-${{ hashFiles('package-lock.json') }}
|
||||||
|
|
@ -113,7 +113,7 @@ jobs:
|
||||||
|
|
||||||
- name: Cache Meteor build
|
- name: Cache Meteor build
|
||||||
id: cache-meteor-build
|
id: cache-meteor-build
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
.meteor/local/resolver-result-cache.json
|
.meteor/local/resolver-result-cache.json
|
||||||
|
|
@ -136,7 +136,7 @@ jobs:
|
||||||
run: sh ./test-wekan.sh -cv
|
run: sh ./test-wekan.sh -cv
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: coverage-folder
|
name: coverage-folder
|
||||||
path: .coverage/
|
path: .coverage/
|
||||||
|
|
@ -147,10 +147,10 @@ jobs:
|
||||||
needs: [tests]
|
needs: [tests]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Download coverage
|
- name: Download coverage
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: coverage-folder
|
name: coverage-folder
|
||||||
path: .coverage/
|
path: .coverage/
|
||||||
|
|
|
||||||
237
CHANGELOG.md
237
CHANGELOG.md
|
|
@ -19,6 +19,243 @@ Fixing other platforms In Progress.
|
||||||
|
|
||||||
[Upgrade WeKan](https://wekan.fi/upgrade/)
|
[Upgrade WeKan](https://wekan.fi/upgrade/)
|
||||||
|
|
||||||
|
WeKan 8.00-8.06 had wrong raw database directory setting /var/snap/wekan/common/wekan and some cards were not visible.
|
||||||
|
Those are fixed at WeKan 8.07 where database directory is back to /var/snap/wekan/common and all cards are visible.
|
||||||
|
|
||||||
|
# Upcoming WeKan ® release
|
||||||
|
|
||||||
|
This release 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.
|
||||||
|
|
||||||
|
and fixes the following bugs:
|
||||||
|
|
||||||
|
- [Fix Broken Strikethroughs in Markdown to HTML conversion](https://github.com/wekan/wekan/pull/6009).
|
||||||
|
Thanks to brlin-tw.
|
||||||
|
|
||||||
|
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||||
|
|
||||||
|
# v8.17 2025-11-06 WeKan ® release
|
||||||
|
|
||||||
|
This release adds the following new feature:
|
||||||
|
|
||||||
|
- [Feature: Workspaces, at All Boards page](https://github.com/wekan/wekan/commit/0afbdc95b49537e06b4f9cf98f51a669ef249384).
|
||||||
|
Thanks to xet7.
|
||||||
|
|
||||||
|
and fixes the following bugs:
|
||||||
|
|
||||||
|
- [Fix 8.16: Switching Board View fails with 403 error](https://github.com/wekan/wekan/commit/550d87ac6cb3ec946600616485afdbd242983ab4).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Moved migrations from opening board to right sidebar / Migrations](https://github.com/wekan/wekan/commit/1b25d1d5720d4f486a10d2acce37e315cf9b6057).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix 8.16 Lists with no items are deleted every time when board is opened. Moved migrations to right sidebar](https://github.com/wekan/wekan/commit/7713e613b431e44dc13cee72e7a1e5f031473fa6).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Remove old translations and code not in use anymore](https://github.com/wekan/wekan/commit/ba49d4d140bc0d4cfb5a96db9ab077bc85db58f1).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fixed sidebar migrations to be per-board, not global. Clarified translations](https://github.com/wekan/wekan/commit/e4638d5fbcbe004ac393462331805cac3ba25097).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix star board](https://github.com/wekan/wekan/commit/8711b476be30496b96b845529b5717bb6e685c27).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix Card emoji issues](https://github.com/wekan/wekan/commit/e5e711c938edcca23c974c3eec97296898bcf24e).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Try to fix Edit Custom Fields button not working. Removed duplicate option from Boards Settings](https://github.com/wekan/wekan/commit/20af0a2ef55b11e7205845859ee92a929616ce91).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix Regression - calendar popup to set due date has gone](https://github.com/wekan/wekan/commit/581733d605b7e0494e72229c45947cff134f6dd6).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Remove not working Bookmark menu option](https://github.com/wekan/wekan/commit/c829c073cf822e48b7cd84bbfb79d42867412517).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix Workspaces at All Boards to have correct count of remaining etc, while starred also at Starred/Favorites](https://github.com/wekan/wekan/commit/6244657ca53a54646ec01e702851a51d89bd0d55).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix Worker Permissions does not allow for cards to be moved. - v8.15. Removed buttons Worker should not use](https://github.com/wekan/wekan/commit/18003900c2d497c129793d1653d4d9872a2f19da).
|
||||||
|
Thanks to xet7.
|
||||||
|
|
||||||
|
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||||
|
|
||||||
|
# v8.16 2025-11-02 WeKan ® release
|
||||||
|
|
||||||
|
This release fixes 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.
|
||||||
|
- [Fix SECURITY ISSUE 2: Access to boards of any Orgs/Teams, and avatar permissions](https://github.com/wekan/wekan/commit/f26d58201855e861bab1cd1fda4d62c664efdb81).
|
||||||
|
Thanks to Siam Thanat Hack (STH) and xet7.
|
||||||
|
- [Fix SECURITY ISSUE 3: Unauthenticated (or any) user can update board sort](https://github.com/wekan/wekan/commit/ea310d7508b344512e5de0dfbc9bdfd38145c5c5).
|
||||||
|
Thanks to Siam Thanat Hack (STH) and xet7.
|
||||||
|
- [Fix SECURITY ISSUE 4: Members can forge others’ votes (Low). Bonus: Similar fixes to planning poker too done by xet7](https://github.com/wekan/wekan/commit/0a1a075f3153e71d9a858576f1c68d2925230d9c).
|
||||||
|
Thanks to Siam Thanat Hack (STH) and xet7.
|
||||||
|
- [Fix SECURITY ISSUE 5: Attachment API uses bearer value as userId and DoS (Low)](https://github.com/wekan/wekan/commit/ccd90343394f433b287733ad0a33c08e0a71f53c).
|
||||||
|
Thanks to Siam Thanat Hack (STH) and xet7.
|
||||||
|
|
||||||
|
and adds the following new features:
|
||||||
|
|
||||||
|
- [List menu / More / Delete duplicate lists that do not have any cards](https://github.com/wekan/wekan/commit/91b846e2cdee9154b045d11b4b4c1a7ae1d79016).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Disabled migrations that happen when opening board. Defaulting to per-swimlane lists and drag drop list to same or different swimlane](https://github.com/wekan/wekan/commit/034dc08269520ca31c780cce64e0150969e9228e).
|
||||||
|
Thanks to xet7.
|
||||||
|
|
||||||
|
and fixes the following bugs:
|
||||||
|
|
||||||
|
- [Fix changing swimlane color to not reload webpage](https://github.com/wekan/wekan/commit/ecf2418347cae4329deb292b534f68eb099d3f90).
|
||||||
|
Thanks to xet7.
|
||||||
|
|
||||||
|
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||||
|
|
||||||
|
# v8.15 2025-10-23 WeKan ® release
|
||||||
|
|
||||||
|
This release fixes the following bugs:
|
||||||
|
|
||||||
|
- Fix drag lists did not work
|
||||||
|
[Part 1](https://github.com/wekan/wekan/commit/8662c96d1c8d4fa76ce7b31eb06678ad59c3ebe1),
|
||||||
|
[Part 2](https://github.com/wekan/wekan/commit/0cebd8aa4dbe0bf2418b814716744ab806b671c2).
|
||||||
|
Thanks to xet7.
|
||||||
|
|
||||||
|
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||||
|
|
||||||
|
# v8.14 2025-10-23 WeKan ® release
|
||||||
|
|
||||||
|
This release fixes the following bugs:
|
||||||
|
|
||||||
|
- [Fix board reloading page every second](https://github.com/wekan/wekan/commit/b4b598f542d0cefc5f2d5d6c7286f0a312cf6a55).
|
||||||
|
Thanks to xet7.
|
||||||
|
|
||||||
|
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||||
|
|
||||||
|
# v8.12 2025-10-23 WeKan ® release
|
||||||
|
|
||||||
|
This release fixes the following bugs:
|
||||||
|
|
||||||
|
- [Fix Regression - unable to view cards by due date v8.11](https://github.com/wekan/wekan/commit/ae11e80bde79d9ad412d185f20e5a7f802685260).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix Regression - unable to rearrange tasks within a checklist - v8.11](https://github.com/wekan/wekan/commit/544b24ceb1687e5b568d8c7b74403a5a2e3f6bc6).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix unable to add members to board](https://github.com/wekan/wekan/commit/c6d46006837a29fb311e444f94fa65f236e23bc7).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Removed not needed | at left side of minicard badges](https://github.com/wekan/wekan/commit/a0c30c35ed57113df041ef1020d3e9e5449f35e4).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix opened card Date Format to be used at dates popups](https://github.com/wekan/wekan/commit/7ca81285b14d1ec60d6e7e9c191d1194950f18c8).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix UI issues of Right Sidebar / Subtasks Settings and Card Settings](https://github.com/wekan/wekan/commit/45537ede870eca59ad72cd7ad013a12f60032df4).
|
||||||
|
Thanks to xet7.
|
||||||
|
|
||||||
|
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||||
|
|
||||||
|
# v8.11 2025-10-21 WeKan ® release
|
||||||
|
|
||||||
|
This release fixes the following bugs:
|
||||||
|
|
||||||
|
- [Fix due dates to use colors: red = overdue, amber = due soon, no shade = not due yet](https://github.com/wekan/wekan/commit/1aa0d849775fbd0dfc83fa8e4cdca84d22a15042).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix My Due Cards to be sorted by due date, oldest first](https://github.com/wekan/wekan/commit/a540b12895520f398bce10bd244f733d221975d4).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Verify that due background colors are correct also at My Due Cards](https://github.com/wekan/wekan/commit/665c9b5e522e73115a1515ced066037110db84e1).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix Regression - due date taking a while to load all cards v8.06](https://github.com/wekan/wekan/commit/347fa9e5cd89d064ebb8ab544e20a41f52206db6).
|
||||||
|
Thanks to xet7.
|
||||||
|
- Fix duplicated lists.
|
||||||
|
[Part 1](https://github.com/wekan/wekan/commit/b6e7b258e0e8caecafc553dceb5771985992a0f9),
|
||||||
|
[Part 2](https://github.com/wekan/wekan/commit/b7ca2310b2cdec7db204229b2d5b9f95b6da8c7d),
|
||||||
|
[Part 3](https://github.com/wekan/wekan/commit/58df525b4915a99d0f603cc2536fd1fad1d20b29).
|
||||||
|
Thanks to xet7.
|
||||||
|
|
||||||
|
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||||
|
|
||||||
|
# v8.10 2025-10-21 WeKan ® release
|
||||||
|
|
||||||
|
This release fixes the following bugs:
|
||||||
|
|
||||||
|
- [Prevent opened board re-migrating and reloading every 5 seconds](https://github.com/wekan/wekan/commit/4987a95d8e35fc4cd30010fd17722ee94037d7f2).
|
||||||
|
Thanks to xet7.
|
||||||
|
|
||||||
|
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||||
|
|
||||||
|
# v8.09 2025-10-21 WeKan ® release
|
||||||
|
|
||||||
|
This release fixes the following bugs:
|
||||||
|
|
||||||
|
- [Fix Admin Panel / People editing and layout](https://github.com/wekan/wekan/commit/7a585a3dfb080af51f88669ea5928f715779cee4).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix upgrade to 8.08 duplicates lists](https://github.com/wekan/wekan/commit/c3a405222782a4a91eb8725faaa8309f0926dcc4).
|
||||||
|
Thanks to xet7.
|
||||||
|
|
||||||
|
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||||
|
|
||||||
|
# v8.08 2025-10-21 WeKan ® release
|
||||||
|
|
||||||
|
This release fixes the following bugs:
|
||||||
|
|
||||||
|
- [Fix opening board migration of Shared Lists to Per-Swimlane lists to use ReactiveCache correctly without errors](https://github.com/wekan/wekan/commit/9536e60bd1c77c8a22e89d2eb2968e11da3a28cd).
|
||||||
|
Thanks to xet7.
|
||||||
|
|
||||||
|
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||||
|
|
||||||
|
# v8.07 2025-10-20 WeKan ® release
|
||||||
|
|
||||||
|
This release fixes the following bugs:
|
||||||
|
|
||||||
|
- [Fix Snap Candidate WeKan 8.00-8.06 commit ae01ea5 database directory from /var/snap/wekan/common/wekan back to 8.07 /var/snap/wekan/common](https://github.com/wekan/wekan/commit/98f141d62f3b6d4371d024c72eae6688d0f4e516).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [When opening board, add missing lists](https://github.com/wekan/wekan/commit/80777b46638ed15b8194105751499ada4b066d19).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [If Snap Candidate MongoDB raw database files were at SNAP_COMMON/wekan, migrate them back to SNAP_COMMON](https://github.com/wekan/wekan/commit/f2019b1059c8d6f4cd9a46c3db7e004c4928cebb).
|
||||||
|
Thanks to xet7.
|
||||||
|
|
||||||
|
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||||
|
|
||||||
|
# v8.06 2025-10-20 WeKan ® release
|
||||||
|
|
||||||
|
This release adds the following new features:
|
||||||
|
|
||||||
|
- [At Public Board, drag resize list width and swimlane height. For logged in users, fix adding labels](https://github.com/wekan/wekan/commit/351433524708e9a7ccb4795d9ca31a78904943ea).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [When opening board, migrate from Shared Lists to Per-Swimlane Lists](https://github.com/wekan/wekan/commit/1e6252de7f26f3af14a99fb63b5dac27ba0576f3).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Added Date Format setting to Opened Card](https://github.com/wekan/wekan/commit/2dd3916f7ee3df10bd88643cf2c796cb166b3044).
|
||||||
|
Thanks to xet7.
|
||||||
|
|
||||||
|
and fixes the following bugs:
|
||||||
|
|
||||||
|
- [Fix add and drag drop attachments to minicards and card](https://github.com/wekan/wekan/commit/b06daff4c7e63453643459f7d8798fde97e3200c).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix starred, archive and clone icons](https://github.com/wekan/wekan/pull/5953).
|
||||||
|
Thanks to helioguardabaxo.
|
||||||
|
- Fix Due dates to be color coded and have unicode icons.
|
||||||
|
[Part 1](https://github.com/wekan/wekan/commit/d965faa3174dc81636106e6f81435b2750b0625f),
|
||||||
|
[Part 2](https://github.com/wekan/wekan/commit/101048339bdd1e45f876aeb1aa5ec32ceda28139).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix unable to see My Due Cards](https://github.com/wekan/wekan/commit/66b444e2b0c9b2ed5f98cd1ff0cd9222b2d0c624).
|
||||||
|
Thanks to xet7.
|
||||||
|
- Fix drag drop lists.
|
||||||
|
[Part 1](https://github.com/wekan/wekan/commit/324f3f7794aace800022a24deb5fd5fb36ebd384),
|
||||||
|
[Part 2](https://github.com/wekan/wekan/commit/ff516ec696ef499f11b04b30053eeb9d3f96d8d1).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Removed extra pipe characters](https://github.com/wekan/wekan/commit/caa6e615ff3c3681bf2b470a625eb39c6009b825).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix syntax error at migrations](https://github.com/wekan/wekan/commit/eb6b42c4c9f99894fd93e62c9b3fceda3429c96c).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix opened card attachments button text to be at tooltip, not at opened card](https://github.com/wekan/wekan/commit/1e53125499ef563ca3c65f786ac3525e5f50274c).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix Broken Hyperlinks in Markdown to HTML conversion](https://github.com/wekan/wekan/commit/973a49526fdf22c143468d3d9db64269b1defa7d).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix migrations](https://github.com/wekan/wekan/commit/0acbf30b0346f49c0ee8f5161fb00b4eca8e1a0c).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix card popup to use HTML date, not anymore JQuery date](https://github.com/wekan/wekan/commit/2d44881619d78e8ef4c5060d17e9035f5babd778).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix Bug: Scale of Minicard icons is linked to horizontal screensize](https://github.com/wekan/wekan/commit/b6b0c5fe6d7dbd37926c662f96f2e3653cabd867).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Fix Bug Member settings drops to the second line and overlaps when many boards are starred as favourites](https://github.com/wekan/wekan/commit/46d46e313cbb8d9c3e4a976ec27b5141c266050f).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Some mobile view fixes](https://github.com/wekan/wekan/commit/c4af4d03acc02f3e54e91f2a65bce2f88742b1a6).
|
||||||
|
Thanks to xet7.
|
||||||
|
- [Have all iPhone use mobile view by default, while still having possibility to use mobile/desktop switch button for desktop mode](https://github.com/wekan/wekan/commit/5df4efd7ba06e618e454f068df05885306283bb1).
|
||||||
|
Thanks to xet7.
|
||||||
|
|
||||||
|
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||||
|
|
||||||
# v8.05 2025-10-17 WeKan ® release
|
# v8.05 2025-10-17 WeKan ® release
|
||||||
|
|
||||||
This release fixes the following bugs:
|
This release fixes the following bugs:
|
||||||
|
|
|
||||||
|
|
@ -249,9 +249,9 @@ cd /home/wekan/app
|
||||||
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
||||||
#rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
|
#rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
|
||||||
#mv /home/wekan/app_build/bundle /build
|
#mv /home/wekan/app_build/bundle /build
|
||||||
wget "https://github.com/wekan/wekan/releases/download/v8.05/wekan-8.05-amd64.zip"
|
wget "https://github.com/wekan/wekan/releases/download/v8.17/wekan-8.17-amd64.zip"
|
||||||
unzip wekan-8.05-amd64.zip
|
unzip wekan-8.17-amd64.zip
|
||||||
rm wekan-8.05-amd64.zip
|
rm wekan-8.17-amd64.zip
|
||||||
mv /home/wekan/app/bundle /build
|
mv /home/wekan/app/bundle /build
|
||||||
|
|
||||||
# Put back the original tar
|
# Put back the original tar
|
||||||
|
|
|
||||||
87
SECURITY.md
87
SECURITY.md
|
|
@ -1,12 +1,20 @@
|
||||||
About money, see [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
||||||
|
|
||||||
Security is very important to us. If you discover any issue regarding security, please disclose
|
## Responsible Security Disclosure
|
||||||
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.
|
|
||||||
|
|
||||||
We thank you with a place at our hall of fame page, that is at https://wekan.fi/hall-of-fame
|
- 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
|
||||||
|
|
||||||
## How should reports be formatted?
|
## How should reports be formatted?
|
||||||
|
|
||||||
|
|
@ -26,7 +34,7 @@ CWSS (optional): %cwss
|
||||||
|
|
||||||
Anyone who reports a unique security issue in scope and does not disclose it to
|
Anyone who reports a unique security issue in scope and does not disclose it to
|
||||||
a third party before we have patched and updated may be upon their approval
|
a third party before we have patched and updated may be upon their approval
|
||||||
added to the Wekan Hall of Fame.
|
added to the WeKan Hall of Fame https://wekan.fi/hall-of-fame/
|
||||||
|
|
||||||
## Which domains are in scope?
|
## Which domains are in scope?
|
||||||
|
|
||||||
|
|
@ -63,11 +71,6 @@ and by by companies that have 30k users.
|
||||||
- If you are thinking about TLS MITM, look at https://github.com/caddyserver/caddy/issues/2530
|
- If you are thinking about TLS MITM, look at https://github.com/caddyserver/caddy/issues/2530
|
||||||
- Let's Encrypt TLS requires publicly accessible webserver, that Let's Encrypt TLS validation servers check.
|
- Let's Encrypt TLS requires publicly accessible webserver, that Let's Encrypt TLS validation servers check.
|
||||||
- If firewall limits to only allowed IP addresses, you may need non-Let's Encrypt TLS cert.
|
- If firewall limits to only allowed IP addresses, you may need non-Let's Encrypt TLS cert.
|
||||||
- For On Premise:
|
|
||||||
- https://caddyserver.com/docs/automatic-https#local-https
|
|
||||||
- https://github.com/wekan/wekan/wiki/Caddy-Webserver-Config
|
|
||||||
- https://github.com/wekan/wekan/wiki/Azure
|
|
||||||
- https://github.com/wekan/wekan/wiki/Traefik-and-self-signed-SSL-certs
|
|
||||||
|
|
||||||
## XSS
|
## XSS
|
||||||
|
|
||||||
|
|
@ -172,6 +175,57 @@ Meteor.startup(() => {
|
||||||
- https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
|
- https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
|
||||||
- https://wekan.github.io/hall-of-fame/filebleed/
|
- https://wekan.github.io/hall-of-fame/filebleed/
|
||||||
|
|
||||||
|
### Attachments: Forced download to prevent stored XSS
|
||||||
|
|
||||||
|
- To prevent browser-side execution of uploaded content under the app origin, all attachment downloads are served with safe headers:
|
||||||
|
- `Content-Type: application/octet-stream`
|
||||||
|
- `Content-Disposition: attachment`
|
||||||
|
- `X-Content-Type-Options: nosniff`
|
||||||
|
- A restrictive `Content-Security-Policy` with `sandbox`
|
||||||
|
- This means attachments are downloaded instead of rendered inline by default. This mitigates HTML/JS/SVG based stored XSS vectors.
|
||||||
|
- Avatars and inline images remain supported but SVG uploads are blocked and never rendered inline.
|
||||||
|
|
||||||
|
## Users: Client update restrictions
|
||||||
|
|
||||||
|
- Client-side updates to user documents are limited to safe fields only:
|
||||||
|
- `username`
|
||||||
|
- `profile.*`
|
||||||
|
- Sensitive fields are blocked from any client updates and can only be modified by server methods with authorization:
|
||||||
|
- `orgs`, `teams`, `roles`, `isAdmin`, `createdThroughApi`, `loginDisabled`, `authenticationMethod`, `services.*`, `emails.*`, `sessionData.*`
|
||||||
|
- Attempts to update forbidden fields from the client are denied.
|
||||||
|
- Admin operations like managing org/team membership or toggling flags must use server methods that check permissions.
|
||||||
|
|
||||||
|
## Voting: integrity and authorization
|
||||||
|
|
||||||
|
- Client updates to card `vote` fields are blocked to prevent forged votes and inconsistent policy enforcement.
|
||||||
|
- Voting is performed via a server method that enforces:
|
||||||
|
- Authentication and board membership, or an explicit per-card flag allowing non-members to vote.
|
||||||
|
- Only the caller's own userId is added/removed from `vote.positive`/`vote.negative`.
|
||||||
|
- This prevents members from fabricating other users' votes and ensures non-members cannot vote unless explicitly allowed.
|
||||||
|
|
||||||
|
## Planning Poker: integrity and authorization
|
||||||
|
|
||||||
|
- Client updates to card `poker` fields are blocked. All poker actions go through server methods that enforce:
|
||||||
|
- Authentication and board membership for configuration and results.
|
||||||
|
- For casting a poker vote, either board membership or an explicit per-card flag allowing non-members to participate.
|
||||||
|
- Only the caller's own userId is added/removed from the selected estimation bucket (e.g., one, two, five, etc.).
|
||||||
|
- Methods cover setting/unsetting poker question/end, casting votes, replaying, and setting final estimation.
|
||||||
|
|
||||||
|
## Attachment API: authentication and DoS prevention
|
||||||
|
|
||||||
|
- The attachment API (`/api/attachment/*`) requires proper authentication using `X-User-Id` and `X-Auth-Token` headers.
|
||||||
|
- Authentication validates tokens by hashing with `Accounts._hashLoginToken` and matching against stored login tokens, preventing identity spoofing.
|
||||||
|
- Request handlers implement:
|
||||||
|
- 30-second timeout to prevent hanging connections.
|
||||||
|
- Request body size limits (50MB for uploads, 10MB for metadata operations).
|
||||||
|
- Proper error handling and guaranteed response completion.
|
||||||
|
- Request error event handlers to clean up failed connections.
|
||||||
|
- This prevents:
|
||||||
|
- DoS attacks via concurrent unauthenticated or malformed requests.
|
||||||
|
- Identity spoofing by using arbitrary bearer tokens or user IDs.
|
||||||
|
- Resource exhaustion from hanging connections or excessive payloads.
|
||||||
|
- Access control: all attachment operations verify board membership before allowing access.
|
||||||
|
|
||||||
## Brute force login protection
|
## Brute force login protection
|
||||||
|
|
||||||
- https://github.com/wekan/wekan/commit/23e5e1e3bd081699ce39ce5887db7e612616014d
|
- https://github.com/wekan/wekan/commit/23e5e1e3bd081699ce39ce5887db7e612616014d
|
||||||
|
|
@ -218,9 +272,4 @@ Typical already known or "no impact" bugs such as:
|
||||||
- Email spoofing, SPF, DMARC & DKIM. Wekan does not include email server.
|
- Email spoofing, SPF, DMARC & DKIM. Wekan does not include email server.
|
||||||
|
|
||||||
Wekan is Open Source with MIT license, and free to use also for commercial use.
|
Wekan is Open Source with MIT license, and free to use also for commercial use.
|
||||||
We welcome all fixes to improve security by email to security@wekan.team
|
We welcome all fixes to improve security by email to security@wekan.fi
|
||||||
|
|
||||||
## 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
|
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||||
appVersion: "v8.05.0"
|
appVersion: "v8.17.0"
|
||||||
files:
|
files:
|
||||||
userUploads:
|
userUploads:
|
||||||
- README.md
|
- README.md
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,50 @@ import '/client/components/migrationProgress';
|
||||||
|
|
||||||
// Import cron settings
|
// Import cron settings
|
||||||
import '/client/components/settings/cronSettings';
|
import '/client/components/settings/cronSettings';
|
||||||
|
|
||||||
|
// Mirror Meteor login token into a cookie for server-side file route auth
|
||||||
|
// This enables cookie-based auth for /cdn/storage/* without leaking ROOT_URL
|
||||||
|
// Token already lives in localStorage; cookie adds same-origin send-on-request semantics
|
||||||
|
Meteor.startup(() => {
|
||||||
|
const COOKIE_NAME = 'meteor_login_token';
|
||||||
|
const cookieAttrs = () => {
|
||||||
|
const attrs = ['Path=/', 'SameSite=Lax'];
|
||||||
|
try {
|
||||||
|
if (window.location && window.location.protocol === 'https:') {
|
||||||
|
attrs.push('Secure');
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return attrs.join('; ');
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCookie = (name, value) => {
|
||||||
|
if (!value) return;
|
||||||
|
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; ${cookieAttrs()}`;
|
||||||
|
};
|
||||||
|
const clearCookie = (name) => {
|
||||||
|
document.cookie = `${encodeURIComponent(name)}=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; ${cookieAttrs()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncCookie = () => {
|
||||||
|
try {
|
||||||
|
const token = Accounts && typeof Accounts._storedLoginToken === 'function' ? Accounts._storedLoginToken() : null;
|
||||||
|
if (token) setCookie(COOKIE_NAME, token); else clearCookie(COOKIE_NAME);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial sync on startup
|
||||||
|
syncCookie();
|
||||||
|
|
||||||
|
// Keep cookie in sync on login/logout
|
||||||
|
if (Accounts && typeof Accounts.onLogin === 'function') Accounts.onLogin(syncCookie);
|
||||||
|
if (Accounts && typeof Accounts.onLogout === 'function') Accounts.onLogout(syncCookie);
|
||||||
|
|
||||||
|
// Sync across tabs/windows when localStorage changes
|
||||||
|
window.addEventListener('storage', (ev) => {
|
||||||
|
if (ev && typeof ev.key === 'string' && ev.key.indexOf('Meteor.loginToken') !== -1) {
|
||||||
|
syncCookie();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -269,56 +269,71 @@
|
||||||
}
|
}
|
||||||
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
|
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
|
||||||
.board-wrapper.mobile-view {
|
.board-wrapper.mobile-view {
|
||||||
width: 100% !important;
|
width: 100vw !important;
|
||||||
min-width: 100% !important;
|
max-width: 100vw !important;
|
||||||
|
min-width: 100vw !important;
|
||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
right: 0 !important;
|
right: 0 !important;
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.board-wrapper.mobile-view .board-canvas {
|
.board-wrapper.mobile-view .board-canvas {
|
||||||
width: 100% !important;
|
width: 100vw !important;
|
||||||
min-width: 100% !important;
|
max-width: 100vw !important;
|
||||||
|
min-width: 100vw !important;
|
||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
right: 0 !important;
|
right: 0 !important;
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.board-wrapper.mobile-view .board-canvas.mobile-view .swimlane {
|
.board-wrapper.mobile-view .board-canvas.mobile-view .swimlane {
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
display: flex;
|
display: block !important;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden !important;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
width: 100%;
|
width: 100vw !important;
|
||||||
min-width: 100%;
|
max-width: 100vw !important;
|
||||||
|
min-width: 100vw !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px),
|
||||||
|
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||||
.board-wrapper {
|
.board-wrapper {
|
||||||
width: 100% !important;
|
width: 100vw !important;
|
||||||
min-width: 100% !important;
|
max-width: 100vw !important;
|
||||||
|
min-width: 100vw !important;
|
||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
right: 0 !important;
|
right: 0 !important;
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.board-wrapper .board-canvas {
|
.board-wrapper .board-canvas {
|
||||||
width: 100% !important;
|
width: 100vw !important;
|
||||||
min-width: 100% !important;
|
max-width: 100vw !important;
|
||||||
|
min-width: 100vw !important;
|
||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
right: 0 !important;
|
right: 0 !important;
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.board-wrapper .board-canvas .swimlane {
|
.board-wrapper .board-canvas .swimlane {
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
display: flex;
|
display: block !important;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden !important;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
width: 100%;
|
width: 100vw !important;
|
||||||
min-width: 100%;
|
max-width: 100vw !important;
|
||||||
|
min-width: 100vw !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.calendar-event-green {
|
.calendar-event-green {
|
||||||
|
|
@ -496,3 +511,10 @@
|
||||||
font-size: 25px;
|
font-size: 25px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Global file drag over state for board canvas */
|
||||||
|
.board-canvas.file-drag-over {
|
||||||
|
background-color: rgba(0, 123, 255, 0.05) !important;
|
||||||
|
border: 2px dashed #007bff !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import dragscroll from '@wekanteam/dragscroll';
|
||||||
import { boardConverter } from '/client/lib/boardConverter';
|
import { boardConverter } from '/client/lib/boardConverter';
|
||||||
import { migrationManager } from '/client/lib/migrationManager';
|
import { migrationManager } from '/client/lib/migrationManager';
|
||||||
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
|
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
|
||||||
|
import { migrationProgressManager } from '/client/components/migrationProgress';
|
||||||
import Swimlanes from '/models/swimlanes';
|
import Swimlanes from '/models/swimlanes';
|
||||||
import Lists from '/models/lists';
|
import Lists from '/models/lists';
|
||||||
|
|
||||||
|
|
@ -17,6 +18,8 @@ BlazeComponent.extendComponent({
|
||||||
this.isConverting = new ReactiveVar(false);
|
this.isConverting = new ReactiveVar(false);
|
||||||
this.isMigrating = new ReactiveVar(false);
|
this.isMigrating = new ReactiveVar(false);
|
||||||
this._swimlaneCreated = new Set(); // Track boards where we've created swimlanes
|
this._swimlaneCreated = new Set(); // Track boards where we've created swimlanes
|
||||||
|
this._boardProcessed = false; // Track if board has been processed
|
||||||
|
this._lastProcessedBoardId = null; // Track last processed board ID
|
||||||
|
|
||||||
// The pattern we use to manually handle data loading is described here:
|
// The pattern we use to manually handle data loading is described here:
|
||||||
// https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
|
// https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
|
||||||
|
|
@ -28,21 +31,33 @@ BlazeComponent.extendComponent({
|
||||||
|
|
||||||
const handle = subManager.subscribe('board', currentBoardId, false);
|
const handle = subManager.subscribe('board', currentBoardId, false);
|
||||||
|
|
||||||
Tracker.nonreactive(() => {
|
// Use a separate autorun for subscription ready state to avoid reactive loops
|
||||||
Tracker.autorun(() => {
|
this.subscriptionReadyAutorun = Tracker.autorun(() => {
|
||||||
if (handle.ready()) {
|
if (handle.ready()) {
|
||||||
|
// Only run conversion/migration logic once per board
|
||||||
|
if (!this._boardProcessed || this._lastProcessedBoardId !== currentBoardId) {
|
||||||
|
this._boardProcessed = true;
|
||||||
|
this._lastProcessedBoardId = currentBoardId;
|
||||||
|
|
||||||
// Ensure default swimlane exists (only once per board)
|
// Ensure default swimlane exists (only once per board)
|
||||||
this.ensureDefaultSwimlane(currentBoardId);
|
this.ensureDefaultSwimlane(currentBoardId);
|
||||||
// Check if board needs conversion
|
// Check if board needs conversion
|
||||||
this.checkAndConvertBoard(currentBoardId);
|
this.checkAndConvertBoard(currentBoardId);
|
||||||
} else {
|
|
||||||
this.isBoardReady.set(false);
|
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
this.isBoardReady.set(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onDestroyed() {
|
||||||
|
// Clean up the subscription ready autorun to prevent memory leaks
|
||||||
|
if (this.subscriptionReadyAutorun) {
|
||||||
|
this.subscriptionReadyAutorun.stop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
ensureDefaultSwimlane(boardId) {
|
ensureDefaultSwimlane(boardId) {
|
||||||
// Only create swimlane once per board
|
// Only create swimlane once per board
|
||||||
if (this._swimlaneCreated.has(boardId)) {
|
if (this._swimlaneCreated.has(boardId)) {
|
||||||
|
|
@ -56,10 +71,17 @@ BlazeComponent.extendComponent({
|
||||||
const swimlanes = board.swimlanes();
|
const swimlanes = board.swimlanes();
|
||||||
|
|
||||||
if (swimlanes.length === 0) {
|
if (swimlanes.length === 0) {
|
||||||
const swimlaneId = Swimlanes.insert({
|
// Check if any swimlane exists in the database to avoid race conditions
|
||||||
title: 'Default',
|
const existingSwimlanes = ReactiveCache.getSwimlanes({ boardId });
|
||||||
boardId: boardId,
|
if (existingSwimlanes.length === 0) {
|
||||||
});
|
const swimlaneId = Swimlanes.insert({
|
||||||
|
title: 'Default',
|
||||||
|
boardId: boardId,
|
||||||
|
});
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log(`Created default swimlane ${swimlaneId} for board ${boardId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
this._swimlaneCreated.add(boardId);
|
this._swimlaneCreated.add(boardId);
|
||||||
} else {
|
} else {
|
||||||
this._swimlaneCreated.add(boardId);
|
this._swimlaneCreated.add(boardId);
|
||||||
|
|
@ -77,43 +99,10 @@ BlazeComponent.extendComponent({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if board needs migration based on migration version
|
// Automatic migration disabled - migrations must be run manually from sidebar
|
||||||
const needsMigration = !board.migrationVersion || board.migrationVersion < 1;
|
// Board admins can run migrations from the sidebar Migrations menu
|
||||||
|
this.isBoardReady.set(true);
|
||||||
if (needsMigration) {
|
|
||||||
// Start background migration for old boards
|
|
||||||
this.isMigrating.set(true);
|
|
||||||
await this.startBackgroundMigration(boardId);
|
|
||||||
this.isMigrating.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if board needs conversion (for old structure)
|
|
||||||
if (boardConverter.isBoardConverted(boardId)) {
|
|
||||||
if (process.env.DEBUG === 'true') {
|
|
||||||
console.log(`Board ${boardId} has already been converted, skipping conversion`);
|
|
||||||
}
|
|
||||||
this.isBoardReady.set(true);
|
|
||||||
} else {
|
|
||||||
const needsConversion = boardConverter.needsConversion(boardId);
|
|
||||||
|
|
||||||
if (needsConversion) {
|
|
||||||
this.isConverting.set(true);
|
|
||||||
const success = await boardConverter.convertBoard(boardId);
|
|
||||||
this.isConverting.set(false);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
this.isBoardReady.set(true);
|
|
||||||
} else {
|
|
||||||
console.error('Board conversion failed, setting ready to true anyway');
|
|
||||||
this.isBoardReady.set(true); // Still show board even if conversion failed
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.isBoardReady.set(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start attachment migration in background if needed
|
|
||||||
this.startAttachmentMigrationIfNeeded(boardId);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during board conversion check:', error);
|
console.error('Error during board conversion check:', error);
|
||||||
this.isConverting.set(false);
|
this.isConverting.set(false);
|
||||||
|
|
@ -122,6 +111,136 @@ BlazeComponent.extendComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
async startBackgroundMigration(boardId) {
|
||||||
try {
|
try {
|
||||||
// Start background migration using the cron system
|
// Start background migration using the cron system
|
||||||
|
|
@ -139,6 +258,227 @@ BlazeComponent.extendComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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) {
|
async startAttachmentMigrationIfNeeded(boardId) {
|
||||||
try {
|
try {
|
||||||
// Check if board has already been migrated
|
// Check if board has already been migrated
|
||||||
|
|
@ -204,39 +544,50 @@ BlazeComponent.extendComponent({
|
||||||
this._isDragging = false;
|
this._isDragging = false;
|
||||||
// Used to set the overlay
|
// Used to set the overlay
|
||||||
this.mouseHasEnterCardDetails = false;
|
this.mouseHasEnterCardDetails = false;
|
||||||
|
this._sortFieldsFixed = new Set(); // Track which boards have had sort fields fixed
|
||||||
|
|
||||||
// fix swimlanes sort field if there are null values
|
// fix swimlanes sort field if there are null values
|
||||||
const currentBoardData = Utils.getCurrentBoard();
|
const currentBoardData = Utils.getCurrentBoard();
|
||||||
if (currentBoardData && Swimlanes) {
|
if (currentBoardData && Swimlanes) {
|
||||||
const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
|
const boardId = currentBoardData._id;
|
||||||
if (nullSortSwimlanes.length > 0) {
|
// Only fix sort fields once per board to prevent reactive loops
|
||||||
const swimlanes = currentBoardData.swimlanes();
|
if (!this._sortFieldsFixed.has(`swimlanes-${boardId}`)) {
|
||||||
let count = 0;
|
const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
|
||||||
swimlanes.forEach(s => {
|
if (nullSortSwimlanes.length > 0) {
|
||||||
Swimlanes.update(s._id, {
|
const swimlanes = currentBoardData.swimlanes();
|
||||||
$set: {
|
let count = 0;
|
||||||
sort: count,
|
swimlanes.forEach(s => {
|
||||||
},
|
Swimlanes.update(s._id, {
|
||||||
|
$set: {
|
||||||
|
sort: count,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
count += 1;
|
||||||
});
|
});
|
||||||
count += 1;
|
}
|
||||||
});
|
this._sortFieldsFixed.add(`swimlanes-${boardId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix lists sort field if there are null values
|
// fix lists sort field if there are null values
|
||||||
if (currentBoardData && Lists) {
|
if (currentBoardData && Lists) {
|
||||||
const nullSortLists = currentBoardData.nullSortLists();
|
const boardId = currentBoardData._id;
|
||||||
if (nullSortLists.length > 0) {
|
// Only fix sort fields once per board to prevent reactive loops
|
||||||
const lists = currentBoardData.lists();
|
if (!this._sortFieldsFixed.has(`lists-${boardId}`)) {
|
||||||
let count = 0;
|
const nullSortLists = currentBoardData.nullSortLists();
|
||||||
lists.forEach(l => {
|
if (nullSortLists.length > 0) {
|
||||||
Lists.update(l._id, {
|
const lists = currentBoardData.lists();
|
||||||
$set: {
|
let count = 0;
|
||||||
sort: count,
|
lists.forEach(l => {
|
||||||
},
|
Lists.update(l._id, {
|
||||||
|
$set: {
|
||||||
|
sort: count,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
count += 1;
|
||||||
});
|
});
|
||||||
count += 1;
|
}
|
||||||
});
|
this._sortFieldsFixed.add(`lists-${boardId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -550,22 +901,20 @@ BlazeComponent.extendComponent({
|
||||||
// Always reset dragscroll on view switch
|
// Always reset dragscroll on view switch
|
||||||
dragscroll.reset();
|
dragscroll.reset();
|
||||||
|
|
||||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
if ($swimlanesDom.data('uiSortable') || $swimlanesDom.data('sortable')) {
|
||||||
$swimlanesDom.sortable({
|
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||||
handle: '.js-swimlane-header-handle',
|
$swimlanesDom.sortable('option', 'handle', '.js-swimlane-header-handle');
|
||||||
});
|
} else {
|
||||||
} else {
|
$swimlanesDom.sortable('option', 'handle', '.swimlane-header');
|
||||||
$swimlanesDom.sortable({
|
}
|
||||||
handle: '.swimlane-header',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable drag-dropping if the current user is not a board member
|
// Disable drag-dropping if the current user is not a board member
|
||||||
$swimlanesDom.sortable(
|
$swimlanesDom.sortable(
|
||||||
'option',
|
'option',
|
||||||
'disabled',
|
'disabled',
|
||||||
!ReactiveCache.getCurrentUser()?.isBoardAdmin(),
|
!ReactiveCache.getCurrentUser()?.isBoardAdmin(),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// If there is no data in the board (ie, no lists) we autofocus the list
|
// If there is no data in the board (ie, no lists) we autofocus the list
|
||||||
|
|
@ -721,6 +1070,31 @@ BlazeComponent.extendComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'click .js-empty-board-add-swimlane': Popup.open('swimlaneAdd'),
|
'click .js-empty-board-add-swimlane': Popup.open('swimlaneAdd'),
|
||||||
|
// Global drag and drop file upload handlers for better visual feedback
|
||||||
|
'dragover .board-canvas'(event) {
|
||||||
|
const dataTransfer = event.originalEvent.dataTransfer;
|
||||||
|
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||||
|
event.preventDefault();
|
||||||
|
// Add visual indicator that files can be dropped
|
||||||
|
$('.board-canvas').addClass('file-drag-over');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'dragleave .board-canvas'(event) {
|
||||||
|
const dataTransfer = event.originalEvent.dataTransfer;
|
||||||
|
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||||
|
// Only remove class if we're leaving the board canvas entirely
|
||||||
|
if (!event.currentTarget.contains(event.relatedTarget)) {
|
||||||
|
$('.board-canvas').removeClass('file-drag-over');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'drop .board-canvas'(event) {
|
||||||
|
const dataTransfer = event.originalEvent.dataTransfer;
|
||||||
|
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||||
|
event.preventDefault();
|
||||||
|
$('.board-canvas').removeClass('file-drag-over');
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
@ -997,9 +1371,13 @@ BlazeComponent.extendComponent({
|
||||||
const firstSwimlane = currentBoard.swimlanes()[0];
|
const firstSwimlane = currentBoard.swimlanes()[0];
|
||||||
Meteor.call('createCardWithDueDate', currentBoard._id, firstList._id, myTitle, startDate.toDate(), firstSwimlane._id, function(error, result) {
|
Meteor.call('createCardWithDueDate', currentBoard._id, firstList._id, myTitle, startDate.toDate(), firstSwimlane._id, function(error, result) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log(error);
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("Card Created", result);
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log("Card Created", result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
closeModal();
|
closeModal();
|
||||||
|
|
|
||||||
|
|
@ -505,73 +505,73 @@
|
||||||
flex-wrap: nowrap !important;
|
flex-wrap: nowrap !important;
|
||||||
align-items: stretch !important;
|
align-items: stretch !important;
|
||||||
justify-content: flex-start !important;
|
justify-content: flex-start !important;
|
||||||
width: 100% !important;
|
width: 100vw !important;
|
||||||
max-width: 100% !important;
|
max-width: 100vw !important;
|
||||||
min-width: 100% !important;
|
min-width: 100vw !important;
|
||||||
overflow-x: hidden !important;
|
overflow-x: hidden !important;
|
||||||
overflow-y: auto !important;
|
overflow-y: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-mode .swimlane {
|
.mobile-mode .swimlane {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
width: 100% !important;
|
width: 100vw !important;
|
||||||
max-width: 100% !important;
|
max-width: 100vw !important;
|
||||||
min-width: 100% !important;
|
min-width: 100vw !important;
|
||||||
margin: 0 0 2rem 0 !important;
|
margin: 0 0 2rem 0 !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
float: none !important;
|
float: none !important;
|
||||||
clear: both !important;
|
clear: both !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-mode .swimlane .swimlane-header {
|
.mobile-mode .swimlane .swimlane-header {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
width: 100% !important;
|
width: 100vw !important;
|
||||||
max-width: 100% !important;
|
max-width: 100vw !important;
|
||||||
min-width: 100% !important;
|
min-width: 100vw !important;
|
||||||
margin: 0 0 1rem 0 !important;
|
margin: 0 0 1rem 0 !important;
|
||||||
padding: 1rem !important;
|
padding: 1rem !important;
|
||||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||||
font-weight: bold !important;
|
font-weight: bold !important;
|
||||||
border-bottom: 2px solid #ccc !important;
|
border-bottom: 2px solid #ccc !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-mode .swimlane .lists {
|
.mobile-mode .swimlane .lists {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
width: 100% !important;
|
width: 100vw !important;
|
||||||
max-width: 100% !important;
|
max-width: 100vw !important;
|
||||||
min-width: 100% !important;
|
min-width: 100vw !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
flex-direction: column !important;
|
flex-direction: column !important;
|
||||||
flex-wrap: nowrap !important;
|
flex-wrap: nowrap !important;
|
||||||
align-items: stretch !important;
|
align-items: stretch !important;
|
||||||
justify-content: flex-start !important;
|
justify-content: flex-start !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-mode .list {
|
.mobile-mode .list {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
width: 100% !important;
|
width: 100vw !important;
|
||||||
max-width: 100% !important;
|
max-width: 100vw !important;
|
||||||
min-width: 100% !important;
|
min-width: 100vw !important;
|
||||||
margin: 0 0 2rem 0 !important;
|
margin: 0 0 2rem 0 !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
float: none !important;
|
float: none !important;
|
||||||
clear: both !important;
|
clear: both !important;
|
||||||
border-left: none !important;
|
border-left: none !important;
|
||||||
border-right: none !important;
|
border-right: none !important;
|
||||||
border-top: none !important;
|
border-top: none !important;
|
||||||
border-bottom: 2px solid #ccc !important;
|
border-bottom: 2px solid #ccc !important;
|
||||||
flex: none !important;
|
flex: none !important;
|
||||||
flex-basis: auto !important;
|
flex-basis: auto !important;
|
||||||
flex-grow: 0 !important;
|
flex-grow: 0 !important;
|
||||||
flex-shrink: 0 !important;
|
flex-shrink: 0 !important;
|
||||||
position: static !important;
|
position: static !important;
|
||||||
left: auto !important;
|
left: auto !important;
|
||||||
right: auto !important;
|
right: auto !important;
|
||||||
top: auto !important;
|
top: auto !important;
|
||||||
bottom: auto !important;
|
bottom: auto !important;
|
||||||
transform: none !important;
|
transform: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-mode .list:first-child {
|
.mobile-mode .list:first-child {
|
||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
|
|
@ -667,9 +667,9 @@
|
||||||
flex-wrap: nowrap !important;
|
flex-wrap: nowrap !important;
|
||||||
align-items: stretch !important;
|
align-items: stretch !important;
|
||||||
justify-content: flex-start !important;
|
justify-content: flex-start !important;
|
||||||
width: 100% !important;
|
width: 100vw !important;
|
||||||
max-width: 100% !important;
|
max-width: 100vw !important;
|
||||||
min-width: 100% !important;
|
min-width: 100vw !important;
|
||||||
overflow-x: hidden !important;
|
overflow-x: hidden !important;
|
||||||
overflow-y: auto !important;
|
overflow-y: auto !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,6 @@ template(name="boardHeaderBar")
|
||||||
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
|
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
|
||||||
| ✏️
|
| ✏️
|
||||||
|
|
||||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
|
||||||
title="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}" aria-label="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}")
|
|
||||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
|
||||||
if showStarCounter
|
|
||||||
span
|
|
||||||
= currentBoard.stars
|
|
||||||
|
|
||||||
a.board-header-btn(
|
a.board-header-btn(
|
||||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||||
title="{{_ currentBoard.permission}}")
|
title="{{_ currentBoard.permission}}")
|
||||||
|
|
@ -38,6 +31,13 @@ template(name="boardHeaderBar")
|
||||||
if $eq watchLevel "muted"
|
if $eq watchLevel "muted"
|
||||||
| 🔕
|
| 🔕
|
||||||
span {{_ watchLevel}}
|
span {{_ watchLevel}}
|
||||||
|
a.board-header-btn.js-star-board(title="{{_ 'star-board'}}")
|
||||||
|
if isStarred
|
||||||
|
| ⭐
|
||||||
|
else
|
||||||
|
| ☆
|
||||||
|
if showStarCounter
|
||||||
|
span.board-star-counter {{currentBoard.stars}}
|
||||||
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
||||||
| {{sortCardsIcon}}
|
| {{sortCardsIcon}}
|
||||||
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
|
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
|
||||||
|
|
@ -61,10 +61,6 @@ template(name="boardHeaderBar")
|
||||||
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
|
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
|
||||||
| ✏️
|
| ✏️
|
||||||
|
|
||||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
|
||||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
|
||||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
|
||||||
|
|
||||||
a.board-header-btn(
|
a.board-header-btn(
|
||||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||||
title="{{_ currentBoard.permission}}")
|
title="{{_ currentBoard.permission}}")
|
||||||
|
|
@ -78,6 +74,11 @@ template(name="boardHeaderBar")
|
||||||
| 🔔
|
| 🔔
|
||||||
if $eq watchLevel "muted"
|
if $eq watchLevel "muted"
|
||||||
| 🔕
|
| 🔕
|
||||||
|
a.board-header-btn.js-star-board(title="{{_ 'star-board'}}")
|
||||||
|
if isStarred
|
||||||
|
| ⭐
|
||||||
|
else
|
||||||
|
| ☆
|
||||||
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
||||||
| {{sortCardsIcon}}
|
| {{sortCardsIcon}}
|
||||||
if isSortActive
|
if isSortActive
|
||||||
|
|
@ -237,6 +238,65 @@ template(name="createBoard")
|
||||||
| /
|
| /
|
||||||
a.js-board-template {{_ 'template'}}
|
a.js-board-template {{_ 'template'}}
|
||||||
|
|
||||||
|
template(name="createBoardPopup")
|
||||||
|
form
|
||||||
|
label
|
||||||
|
| {{_ 'title'}}
|
||||||
|
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
|
||||||
|
if visibilityMenuIsOpen.get
|
||||||
|
+boardVisibilityList
|
||||||
|
else
|
||||||
|
p.quiet
|
||||||
|
if $eq visibility.get 'public'
|
||||||
|
span 🌐
|
||||||
|
= " "
|
||||||
|
| {{{_ 'board-public-info'}}}
|
||||||
|
else
|
||||||
|
span 🔒
|
||||||
|
= " "
|
||||||
|
| {{{_ 'board-private-info'}}}
|
||||||
|
a.js-change-visibility {{_ 'change'}}.
|
||||||
|
a.flex.js-toggle-add-template-container
|
||||||
|
.materialCheckBox#add-template-container
|
||||||
|
span {{_ 'add-template-container'}}
|
||||||
|
input.primary.wide(type="submit" value="{{_ 'create'}}")
|
||||||
|
span.quiet
|
||||||
|
| {{_ 'or'}}
|
||||||
|
a.js-import-board {{_ 'import'}}
|
||||||
|
span.quiet
|
||||||
|
| /
|
||||||
|
a.js-board-template {{_ 'template'}}
|
||||||
|
|
||||||
|
// New popup for Template Container creation; shares the same form content
|
||||||
|
template(name="createTemplateContainerPopup")
|
||||||
|
form
|
||||||
|
label
|
||||||
|
| {{_ 'title'}}
|
||||||
|
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
|
||||||
|
if visibilityMenuIsOpen.get
|
||||||
|
+boardVisibilityList
|
||||||
|
else
|
||||||
|
p.quiet
|
||||||
|
if $eq visibility.get 'public'
|
||||||
|
span 🌐
|
||||||
|
= " "
|
||||||
|
| {{{_ 'board-public-info'}}}
|
||||||
|
else
|
||||||
|
span 🔒
|
||||||
|
= " "
|
||||||
|
| {{{_ 'board-private-info'}}}
|
||||||
|
a.js-change-visibility {{_ 'change'}}.
|
||||||
|
a.flex.js-toggle-add-template-container
|
||||||
|
.materialCheckBox#add-template-container
|
||||||
|
span {{_ 'add-template-container'}}
|
||||||
|
input.primary.wide(type="submit" value="{{_ 'create'}}")
|
||||||
|
span.quiet
|
||||||
|
| {{_ 'or'}}
|
||||||
|
a.js-import-board {{_ 'import'}}
|
||||||
|
span.quiet
|
||||||
|
| /
|
||||||
|
a.js-board-template {{_ 'template'}}
|
||||||
|
|
||||||
//template(name="listsortPopup")
|
//template(name="listsortPopup")
|
||||||
// h2
|
// h2
|
||||||
// | {{_ 'list-sort-by'}}
|
// | {{_ 'list-sort-by'}}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,10 @@ BlazeComponent.extendComponent({
|
||||||
{
|
{
|
||||||
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
|
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
|
||||||
'click .js-star-board'() {
|
'click .js-star-board'() {
|
||||||
ReactiveCache.getCurrentUser().toggleBoardStar(Session.get('currentBoard'));
|
const boardId = Session.get('currentBoard');
|
||||||
|
if (boardId) {
|
||||||
|
Meteor.call('toggleBoardStar', boardId);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'click .js-open-board-menu': Popup.open('boardMenu'),
|
'click .js-open-board-menu': Popup.open('boardMenu'),
|
||||||
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
|
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
|
||||||
|
|
@ -82,18 +85,26 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
'click .js-toggle-board-view': Popup.open('boardChangeView'),
|
'click .js-toggle-board-view': Popup.open('boardChangeView'),
|
||||||
'click .js-toggle-sidebar'() {
|
'click .js-toggle-sidebar'() {
|
||||||
console.log('Hamburger menu clicked');
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('Hamburger menu clicked');
|
||||||
|
}
|
||||||
// Use the same approach as keyboard shortcuts
|
// Use the same approach as keyboard shortcuts
|
||||||
if (typeof Sidebar !== 'undefined' && Sidebar && typeof Sidebar.toggle === 'function') {
|
if (typeof Sidebar !== 'undefined' && Sidebar && typeof Sidebar.toggle === 'function') {
|
||||||
console.log('Using Sidebar.toggle()');
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('Using Sidebar.toggle()');
|
||||||
|
}
|
||||||
Sidebar.toggle();
|
Sidebar.toggle();
|
||||||
} else {
|
} else {
|
||||||
console.warn('Sidebar not available, trying alternative approach');
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.warn('Sidebar not available, trying alternative approach');
|
||||||
|
}
|
||||||
// Try to trigger the sidebar through the global Blaze helper
|
// Try to trigger the sidebar through the global Blaze helper
|
||||||
if (typeof Blaze !== 'undefined' && Blaze._globalHelpers && Blaze._globalHelpers.Sidebar) {
|
if (typeof Blaze !== 'undefined' && Blaze._globalHelpers && Blaze._globalHelpers.Sidebar) {
|
||||||
const sidebar = Blaze._globalHelpers.Sidebar();
|
const sidebar = Blaze._globalHelpers.Sidebar();
|
||||||
if (sidebar && typeof sidebar.toggle === 'function') {
|
if (sidebar && typeof sidebar.toggle === 'function') {
|
||||||
console.log('Using Blaze helper Sidebar.toggle()');
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('Using Blaze helper Sidebar.toggle()');
|
||||||
|
}
|
||||||
sidebar.toggle();
|
sidebar.toggle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -155,6 +166,7 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
}).register('boardHeaderBar');
|
}).register('boardHeaderBar');
|
||||||
|
|
||||||
Template.boardHeaderBar.helpers({
|
Template.boardHeaderBar.helpers({
|
||||||
|
|
@ -282,6 +294,15 @@ const CreateBoard = BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Assign to space if one was selected
|
||||||
|
const spaceId = Session.get('createBoardInWorkspace');
|
||||||
|
if (spaceId) {
|
||||||
|
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
|
||||||
|
if (err) console.error('Error assigning board to space:', err);
|
||||||
|
});
|
||||||
|
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||||
|
}
|
||||||
|
|
||||||
Utils.goBoardId(this.boardId.get());
|
Utils.goBoardId(this.boardId.get());
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -300,6 +321,15 @@ const CreateBoard = BlazeComponent.extendComponent({
|
||||||
boardId: this.boardId.get(),
|
boardId: this.boardId.get(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Assign to space if one was selected
|
||||||
|
const spaceId = Session.get('createBoardInWorkspace');
|
||||||
|
if (spaceId) {
|
||||||
|
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
|
||||||
|
if (err) console.error('Error assigning board to space:', err);
|
||||||
|
});
|
||||||
|
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||||
|
}
|
||||||
|
|
||||||
Utils.goBoardId(this.boardId.get());
|
Utils.goBoardId(this.boardId.get());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -321,6 +351,13 @@ const CreateBoard = BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
}).register('createBoardPopup');
|
}).register('createBoardPopup');
|
||||||
|
|
||||||
|
(class CreateTemplateContainerPopup extends CreateBoard {
|
||||||
|
onRendered() {
|
||||||
|
// Always pre-check the template container checkbox for this popup
|
||||||
|
$('#add-template-container').addClass('is-checked');
|
||||||
|
}
|
||||||
|
}).register('createTemplateContainerPopup');
|
||||||
|
|
||||||
(class HeaderBarCreateBoard extends CreateBoard {
|
(class HeaderBarCreateBoard extends CreateBoard {
|
||||||
onSubmit(event) {
|
onSubmit(event) {
|
||||||
super.onSubmit(event);
|
super.onSubmit(event);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,273 @@
|
||||||
padding: 1vh 0;
|
padding: 1vh 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Two-column layout for All Boards */
|
||||||
|
.boards-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 260px 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-left-menu {
|
||||||
|
border-right: 1px solid #e0e0e0;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-left-menu ul.menu {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-left-menu .menu-item {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
.boards-left-menu .menu-item a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.boards-left-menu .menu-item .menu-label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.boards-left-menu .menu-item .menu-count {
|
||||||
|
background: #ddd;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.boards-left-menu .menu-item.active a,
|
||||||
|
.boards-left-menu .menu-item a:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
.boards-left-menu .menu-item.active .menu-count {
|
||||||
|
background: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drag-over state for menu items (for dropping boards on Remaining) */
|
||||||
|
.boards-left-menu .menu-item a.drag-over {
|
||||||
|
background: #d0e8ff;
|
||||||
|
border: 2px dashed #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspaces-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.workspaces-header .js-add-space {
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-tree {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-node {
|
||||||
|
margin: 2px 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-node-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-node.dragging > .workspace-node-content {
|
||||||
|
opacity: 0.5;
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-node.drag-over > .workspace-node-content {
|
||||||
|
background: #d0e8ff;
|
||||||
|
border: 2px dashed #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-drag-handle {
|
||||||
|
cursor: grab;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 4px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-drag-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-node .js-select-space {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-node .workspace-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-node .workspace-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-node .workspace-count {
|
||||||
|
background: #ddd;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-node .js-edit-space,
|
||||||
|
.workspace-node .js-add-subspace {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-node .js-edit-space:hover,
|
||||||
|
.workspace-node .js-add-subspace:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-node.active > .workspace-node-content .js-select-space,
|
||||||
|
.workspace-node > .workspace-node-content:hover .js-select-space {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-node.active .workspace-count {
|
||||||
|
background: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-right-grid {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-path-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-path-header .path-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-path-header .multiselection-hint {
|
||||||
|
background: #FFF3CD;
|
||||||
|
color: #856404;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: normal;
|
||||||
|
border: 1px solid #FFE69C;
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.7; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-path-header .path-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-path-header .path-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-path-header .path-text {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-path-header .board-header-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-path-header .board-header-btn:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-path-header .board-header-btn.emphasis {
|
||||||
|
background: #2196F3;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #2196F3;
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.5);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-path-header .board-header-btn.emphasis:hover {
|
||||||
|
background: #1976D2;
|
||||||
|
box-shadow: 0 3px 12px rgba(33, 150, 243, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-path-header .board-header-btn-close {
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: #f44336;
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-left: 10px; /* Extra space between MultiSelection toggle and Remove Filter */
|
||||||
|
}
|
||||||
|
|
||||||
|
.boards-path-header .board-header-btn-close:hover {
|
||||||
|
background: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
.zoom-controls {
|
.zoom-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -103,26 +370,38 @@
|
||||||
transform: rotate(4deg);
|
transform: rotate(4deg);
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
.board-list li.starred .fa-star,
|
.board-list li.starred .is-star-active,
|
||||||
.board-list li.starred .fa-star-o {
|
.board-list li.starred .is-not-star-active {
|
||||||
|
opacity: 1;
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
/* Show star icon on hover even for non-starred boards */
|
||||||
|
.board-list li:hover .is-star-active,
|
||||||
|
.board-list li:hover .is-not-star-active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.board-list .board-list-item {
|
.board-list .board-list-item {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #999;
|
background-color: inherit; /* Inherit board color from parent li.js-board */
|
||||||
color: #f6f6f6;
|
color: #f6f6f6;
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
border-radius: 3px;
|
border-radius: 0; /* No border-radius - parent .js-board has it */
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 8px;
|
padding: 36px 8px 32px 8px; /* Top padding for drag handle, bottom for checkbox */
|
||||||
margin: 8px;
|
margin: 0; /* No margin - moved to parent .js-board */
|
||||||
position: relative;
|
position: relative;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.board-list .board-list-item > .js-open-board {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
.board-list .board-list-item.template-container {
|
.board-list .board-list-item.template-container {
|
||||||
border: 4px solid #fff;
|
border: 4px solid #fff;
|
||||||
}
|
}
|
||||||
|
|
@ -150,13 +429,20 @@
|
||||||
.board-list .js-add-board .label {
|
.board-list .js-add-board .label {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
line-height: 56px;
|
line-height: 56px;
|
||||||
|
min-height: 100px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #999; /* Darker background for better text contrast */
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 36px 8px 32px 8px;
|
||||||
}
|
}
|
||||||
.board-list .js-add-board :hover {
|
.board-list .js-add-board .label:hover {
|
||||||
background-color: #939393;
|
background-color: #808080; /* Even darker on hover */
|
||||||
}
|
}
|
||||||
.board-list .fa-star,
|
.board-list .is-star-active,
|
||||||
.board-list .fa-star-o {
|
.board-list .is-not-star-active {
|
||||||
bottom: 0;
|
top: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
|
|
@ -164,7 +450,6 @@
|
||||||
padding: 9px 9px;
|
padding: 9px 9px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
|
||||||
transition-duration: 0.15s;
|
transition-duration: 0.15s;
|
||||||
transition-property: color, font-size, background;
|
transition-property: color, font-size, background;
|
||||||
}
|
}
|
||||||
|
|
@ -212,32 +497,121 @@
|
||||||
transition-duration: 0.15s;
|
transition-duration: 0.15s;
|
||||||
transition-property: color, font-size, background;
|
transition-property: color, font-size, background;
|
||||||
}
|
}
|
||||||
.board-list li:hover a:hover .fa-star,
|
.board-list li:hover a:hover .is-star-active,
|
||||||
.board-list li:hover a:hover .fa-clone,
|
.board-list li:hover a:hover .fa-clone,
|
||||||
.board-list li:hover a:hover .fa-archive,
|
.board-list li:hover a:hover .fa-archive,
|
||||||
.board-list li:hover a:hover .fa-star-o {
|
.board-list li:hover a:hover .is-not-star-active {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.board-list li:hover a .fa-star,
|
.board-list li:hover a .is-star-active,
|
||||||
.board-list li:hover a .fa-clone,
|
.board-list li:hover a .fa-clone,
|
||||||
.board-list li:hover a .fa-archive,
|
.board-list li:hover a .fa-archive,
|
||||||
.board-list li:hover a .fa-star-o {
|
.board-list li:hover a .is-not-star-active {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
.board-list li:hover a .fa-star:hover,
|
.board-list li:hover a .is-star-active:hover,
|
||||||
.board-list li:hover a .fa-clone:hover,
|
.board-list li:hover a .fa-clone:hover,
|
||||||
.board-list li:hover a .fa-archive:hover,
|
.board-list li:hover a .fa-archive:hover,
|
||||||
.board-list li:hover a .fa-star-o:hover {
|
.board-list li:hover a .is-not-star-active:hover {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.board-list li:hover a .fa-star.is-star-active,
|
.board-list li:hover a .is-star-active,
|
||||||
.board-list li:hover a .fa-clone.is-star-active,
|
.board-list li:hover a .fa-clone,
|
||||||
.board-list li:hover a .fa-archive.is-star-active,
|
.board-list li:hover a .fa-archive,
|
||||||
.board-list li:hover a .fa-star-o.is-star-active {
|
.board-list li:hover a .is-not-star-active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Board drag handle - always visible and positioned at top */
|
||||||
|
.board-list .board-handle {
|
||||||
|
position: absolute;
|
||||||
|
padding: 4px 6px;
|
||||||
|
top: 4px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(0,0,0,0.4);
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
cursor: grab;
|
||||||
|
opacity: 1;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-list .board-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-list .board-handle:hover {
|
||||||
|
background: rgba(255, 255, 0, 0.8) !important;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Multiselection checkbox on board items */
|
||||||
|
.board-list .board-list-item .multi-selection-checkbox {
|
||||||
|
position: absolute !important;
|
||||||
|
bottom: 4px !important;
|
||||||
|
left: 4px !important;
|
||||||
|
top: auto !important;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 11;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||||
|
transform: none !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-list .board-list-item .multi-selection-checkbox:hover {
|
||||||
|
background: rgba(0,0,0,0.7);
|
||||||
|
transform: scale(1.15) !important;
|
||||||
|
box-shadow: 0 3px 6px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-list .board-list-item .multi-selection-checkbox.is-checked {
|
||||||
|
background: #2196F3;
|
||||||
|
border-color: #2196F3;
|
||||||
|
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.6);
|
||||||
|
width: 24px !important;
|
||||||
|
height: 24px !important;
|
||||||
|
top: auto !important;
|
||||||
|
left: 4px !important;
|
||||||
|
transform: none !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-list .board-list-item .multi-selection-checkbox.is-checked::after {
|
||||||
|
content: '✓';
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-list.is-multiselection-active .js-board.is-checked {
|
||||||
|
outline: 4px solid #2196F3;
|
||||||
|
outline-offset: -4px;
|
||||||
|
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Visual hint when multiselection is active */
|
||||||
|
.board-list.is-multiselection-active .board-list-item {
|
||||||
|
border: 2px dashed rgba(33, 150, 243, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.board-backgrounds-list .board-background-select {
|
.board-backgrounds-list .board-background-select {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -361,6 +735,18 @@
|
||||||
min-height: 100vh; /* Force content to be tall enough to scroll */
|
min-height: 100vh; /* Force content to be tall enough to scroll */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide archive and clone board buttons in mobile view */
|
||||||
|
.board-list.mobile-view .js-archive-board,
|
||||||
|
.board-list.mobile-view .js-clone-board {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Change board drag handle to up-down arrow in mobile view */
|
||||||
|
.board-list.mobile-view .board-handle.fa-arrows::before {
|
||||||
|
content: "↕️" !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
.board-list.mobile-view::after {
|
.board-list.mobile-view::after {
|
||||||
content: '';
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -371,7 +757,8 @@
|
||||||
screen and (max-device-width: 800px),
|
screen and (max-device-width: 800px),
|
||||||
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
|
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
|
||||||
screen and (max-width: 800px) and (orientation: portrait),
|
screen and (max-width: 800px) and (orientation: portrait),
|
||||||
screen and (max-width: 800px) and (orientation: landscape) {
|
screen and (max-width: 800px) and (orientation: landscape),
|
||||||
|
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||||
.board-list {
|
.board-list {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -457,7 +844,8 @@
|
||||||
screen and (max-device-width: 800px),
|
screen and (max-device-width: 800px),
|
||||||
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
|
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
|
||||||
screen and (max-width: 800px) and (orientation: portrait),
|
screen and (max-width: 800px) and (orientation: portrait),
|
||||||
screen and (max-width: 800px) and (orientation: landscape) {
|
screen and (max-width: 800px) and (orientation: landscape),
|
||||||
|
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||||
.wrapper {
|
.wrapper {
|
||||||
font-size: 2em !important; /* 2x bigger base font size for All Boards page */
|
font-size: 2em !important; /* 2x bigger base font size for All Boards page */
|
||||||
}
|
}
|
||||||
|
|
@ -725,9 +1113,62 @@
|
||||||
#resetBtn {
|
#resetBtn {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#resetBtn.filter-reset-btn {
|
||||||
|
background: #f44336;
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#resetBtn.filter-reset-btn:hover {
|
||||||
|
background: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
#resetBtn.filter-reset-btn .reset-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.js-board {
|
.js-board {
|
||||||
display: block;
|
display: block;
|
||||||
|
background-color: #999; /* Default gray background if no color class is applied */
|
||||||
|
border-radius: 3px; /* Rounded corners for board items */
|
||||||
|
overflow: hidden; /* Ensure children respect rounded corners */
|
||||||
|
margin: 8px; /* Space between board items */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Reset background for add-board button */
|
||||||
|
.js-add-board {
|
||||||
|
background-color: transparent !important;
|
||||||
|
margin: 8px !important; /* Keep margin for add-board */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply board colors to li.js-board parent instead of just the link */
|
||||||
|
.board-list .board-color-nephritis { background-color: #27ae60; }
|
||||||
|
.board-list .board-color-pomegranate { background-color: #c0392b; }
|
||||||
|
.board-list .board-color-belize { background-color: #2980b9; }
|
||||||
|
.board-list .board-color-wisteria { background-color: #8e44ad; }
|
||||||
|
.board-list .board-color-midnight { background-color: #2c3e50; }
|
||||||
|
.board-list .board-color-pumpkin { background-color: #e67e22; }
|
||||||
|
.board-list .board-color-moderatepink { background-color: #cd5a91; }
|
||||||
|
.board-list .board-color-strongcyan { background-color: #00aecc; }
|
||||||
|
.board-list .board-color-limegreen { background-color: #4bbf6b; }
|
||||||
|
.board-list .board-color-dark { background-color: #2c3e51; }
|
||||||
|
.board-list .board-color-relax { background-color: #27ae61; }
|
||||||
|
.board-list .board-color-corteza { background-color: #568ba2; }
|
||||||
|
.board-list .board-color-clearblue { background-color: #3498db; }
|
||||||
|
.board-list .board-color-natural { background-color: #596557; }
|
||||||
|
.board-list .board-color-modern { background-color: #2a80b8; }
|
||||||
|
.board-list .board-color-moderndark { background-color: #2a2a2a; }
|
||||||
|
.board-list .board-color-exodark { background-color: #222; }
|
||||||
|
|
||||||
.minicard-members {
|
.minicard-members {
|
||||||
padding: 6px 0 6px 8px;
|
padding: 6px 0 6px 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -757,7 +1198,8 @@
|
||||||
screen and (max-device-width: 800px),
|
screen and (max-device-width: 800px),
|
||||||
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
|
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
|
||||||
screen and (max-width: 800px) and (orientation: portrait),
|
screen and (max-width: 800px) and (orientation: portrait),
|
||||||
screen and (max-width: 800px) and (orientation: landscape) {
|
screen and (max-width: 800px) and (orientation: landscape),
|
||||||
|
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||||
.wrapper {
|
.wrapper {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
@ -824,5 +1266,17 @@
|
||||||
#content {
|
#content {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide archive and clone board buttons in mobile view */
|
||||||
|
.board-list .js-archive-board,
|
||||||
|
.board-list .js-clone-board {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Change board drag handle to up-down arrow in mobile view */
|
||||||
|
.board-list .board-handle.fa-arrows::before {
|
||||||
|
content: "↕️" !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,148 +2,160 @@ template(name="boardList")
|
||||||
.wrapper
|
.wrapper
|
||||||
.board-list-header
|
.board-list-header
|
||||||
|
|
||||||
ul.AllBoardTeamsOrgs
|
.boards-layout
|
||||||
li.AllBoardTeams
|
// Left menu
|
||||||
if userHasTeams
|
.boards-left-menu
|
||||||
select.js-AllBoardTeams#jsAllBoardTeams("multiple")
|
ul.menu
|
||||||
option(value="-1") {{_ 'teams'}} :
|
li(class="menu-item {{#if isSelectedMenu 'starred'}}active{{/if}}")
|
||||||
each teamsDatas
|
a.js-select-menu(data-type="starred")
|
||||||
option(value="{{teamId}}") {{_ teamDisplayName}}
|
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 📋 {{_ '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 📂 {{_ 'allboards.remaining'}}
|
||||||
|
span.menu-count {{menuItemCount 'remaining'}}
|
||||||
|
.workspaces-header
|
||||||
|
span 🗂️ {{_ 'allboards.workspaces'}}
|
||||||
|
a.js-add-workspace(title="{{_ 'allboards.add-workspace'}}") +
|
||||||
|
// Workspaces tree
|
||||||
|
+workspaceTree(nodes=workspacesTree selectedWorkspaceId=selectedWorkspaceId)
|
||||||
|
|
||||||
li.AllBoardOrgs
|
// Existing filter by orgs/teams (kept)
|
||||||
if userHasOrgs
|
ul.AllBoardTeamsOrgs
|
||||||
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
|
li.AllBoardTeams
|
||||||
option(value="-1") {{_ 'organizations'}} :
|
if userHasTeams
|
||||||
each orgsDatas
|
select.js-AllBoardTeams#jsAllBoardTeams("multiple")
|
||||||
option(value="{{orgId}}") {{orgDisplayName}}
|
option(value="-1") {{_ 'teams'}} :
|
||||||
|
each teamsDatas
|
||||||
|
option(value="{{teamId}}") {{_ teamDisplayName}}
|
||||||
|
|
||||||
//li.AllBoardTemplates
|
li.AllBoardOrgs
|
||||||
// if userHasTemplates
|
if userHasOrgs
|
||||||
// select.js-AllBoardTemplates#jsAllBoardTemplates("multiple")
|
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
|
||||||
// option(value="-1") {{_ 'templates'}} :
|
option(value="-1") {{_ 'organizations'}} :
|
||||||
// each templatesDatas
|
each orgsDatas
|
||||||
// option(value="{{templateId}}") {{_ templateDisplayName}}
|
option(value="{{orgId}}") {{orgDisplayName}}
|
||||||
|
|
||||||
li.AllBoardBtns
|
li.AllBoardBtns
|
||||||
div.AllBoardButtonsContainer
|
div.AllBoardButtonsContainer
|
||||||
if userHasOrgsOrTeams
|
if userHasOrgsOrTeams
|
||||||
i.fa.fa-filter
|
span 🔍
|
||||||
input#filterBtn(type="button" value="{{_ 'filter'}}")
|
input#filterBtn(type="button" value="{{_ 'filter'}}")
|
||||||
input#resetBtn(type="button" value="{{_ 'filter-clear'}}")
|
button#resetBtn.filter-reset-btn
|
||||||
|
span.reset-icon ❌
|
||||||
|
span {{_ 'filter-clear'}}
|
||||||
|
|
||||||
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
// Right boards grid
|
||||||
li.js-add-board
|
.boards-right-grid
|
||||||
a.board-list-item.label(title="{{_ 'add-board'}}")
|
.boards-path-header
|
||||||
| {{_ 'add-board'}}
|
.path-left
|
||||||
each boards
|
span.path-icon {{currentMenuPath.icon}}
|
||||||
li(class="{{_id}}" class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
|
span.path-text {{currentMenuPath.text}}
|
||||||
if isInvited
|
if BoardMultiSelection.isActive
|
||||||
.board-list-item
|
span.multiselection-hint 📌 {{_ 'multi-selection-active'}}
|
||||||
span.details
|
.path-right
|
||||||
span.board-list-item-name= title
|
if canModifyBoards
|
||||||
i.fa.js-star-board(
|
if hasBoardsSelected
|
||||||
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
|
button.js-archive-selected-boards.board-header-btn
|
||||||
title="{{_ 'star-board-title'}}")
|
span 📦
|
||||||
p.board-list-item-desc {{_ 'just-invited'}}
|
span {{_ 'archive-board'}}
|
||||||
button.js-accept-invite.primary {{_ 'accept'}}
|
button.js-duplicate-selected-boards.board-header-btn
|
||||||
button.js-decline-invite {{_ 'decline'}}
|
span 📋
|
||||||
else
|
span {{_ 'duplicate-board'}}
|
||||||
if $eq type "template-container"
|
a.board-header-btn.js-multiselection-activate(
|
||||||
a.js-open-board.template-container.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
|
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
|
||||||
span.details
|
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
|
||||||
span.board-list-item-name(title="{{_ 'template-container'}}")
|
| ☑️
|
||||||
+viewer
|
if BoardMultiSelection.isActive
|
||||||
= title
|
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||||
i.fa.js-star-board(
|
| ✖
|
||||||
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
|
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
|
||||||
title="{{_ 'star-board-title'}}")
|
li.js-add-board
|
||||||
p.board-list-item-desc
|
if isSelectedMenu 'templates'
|
||||||
+viewer
|
a.board-list-item.label(title="{{_ 'add-template-container'}}")
|
||||||
= description
|
| ➕ {{_ 'add-template-container'}}
|
||||||
if hasSpentTimeCards
|
|
||||||
i.fa.js-has-spenttime-cards(
|
|
||||||
class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
|
|
||||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
|
||||||
if isTouchScreenOrShowDesktopDragHandles
|
|
||||||
i.fa.board-handle(
|
|
||||||
class="fa-arrows"
|
|
||||||
title="{{_ 'drag-board'}}")
|
|
||||||
else
|
|
||||||
if isSandstorm
|
|
||||||
i.fa.js-clone-board(
|
|
||||||
class="fa-clone"
|
|
||||||
title="{{_ 'duplicate-board'}}")
|
|
||||||
i.fa.js-archive-board(
|
|
||||||
class="fa-archive"
|
|
||||||
title="{{_ 'archive-board'}}")
|
|
||||||
else if isAdministrable
|
|
||||||
i.fa.js-clone-board(
|
|
||||||
class="fa-clone"
|
|
||||||
title="{{_ 'duplicate-board'}}")
|
|
||||||
i.fa.js-archive-board(
|
|
||||||
class="fa-archive"
|
|
||||||
title="{{_ 'archive-board'}}")
|
|
||||||
else if currentUser.isAdmin
|
|
||||||
i.fa.js-clone-board(
|
|
||||||
class="fa-clone"
|
|
||||||
title="{{_ 'duplicate-board'}}")
|
|
||||||
i.fa.js-archive-board(
|
|
||||||
class="fa-archive"
|
|
||||||
title="{{_ 'archive-board'}}")
|
|
||||||
else
|
else
|
||||||
a.js-open-board.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
|
a.board-list-item.label(title="{{_ 'add-board'}}")
|
||||||
span.details
|
| ➕ {{_ 'add-board'}}
|
||||||
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
|
each boards
|
||||||
+viewer
|
li.js-board(class="{{_id}} {{#if isStarred}}starred{{/if}} {{colorClass}} {{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}", draggable="true")
|
||||||
= title
|
if isInvited
|
||||||
unless currentSetting.hideBoardMemberList
|
.board-list-item
|
||||||
if allowsBoardMemberList
|
if BoardMultiSelection.isActive
|
||||||
.minicard-members
|
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||||
each member in boardMembers _id
|
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||||
a.name
|
span.details
|
||||||
+userAvatar(userId=member noRemove=true)
|
span.board-list-item-name= title
|
||||||
unless currentSetting.hideCardCounterList
|
span.js-star-board(
|
||||||
if allowsCardCounterList
|
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||||
.minicard-lists.flex.flex-wrap
|
title="{{_ 'star-board-title'}}")
|
||||||
each list in boardLists _id
|
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||||
.item
|
p.board-list-item-desc {{_ 'just-invited'}}
|
||||||
| {{ list }}
|
button.js-accept-invite.primary {{_ 'accept'}}
|
||||||
i.fa.js-star-board(
|
button.js-decline-invite {{_ 'decline'}}
|
||||||
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
|
else
|
||||||
title="{{_ 'star-board-title'}}")
|
if $eq type "template-container"
|
||||||
p.board-list-item-desc
|
.template-container.board-list-item
|
||||||
+viewer
|
if BoardMultiSelection.isActive
|
||||||
= description
|
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||||
if hasSpentTimeCards
|
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||||
i.fa.js-has-spenttime-cards(
|
span.board-handle(title="{{_ 'drag-board'}}") ↕️
|
||||||
class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
|
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
span.details
|
||||||
if isTouchScreenOrShowDesktopDragHandles
|
span.board-list-item-name(title="{{_ 'template-container'}}")
|
||||||
i.fa.board-handle(
|
+viewer
|
||||||
class="fa-arrows"
|
= title
|
||||||
title="{{_ 'drag-board'}}")
|
p.board-list-item-desc
|
||||||
else
|
+viewer
|
||||||
if isSandstorm
|
= description
|
||||||
i.fa.js-clone-board(
|
if hasSpentTimeCards
|
||||||
class="fa-clone"
|
span.js-has-spenttime-cards(
|
||||||
title="{{_ 'duplicate-board'}}")
|
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
|
||||||
i.fa.js-archive-board(
|
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||||
class="fa-archive"
|
| ⏱️
|
||||||
title="{{_ 'archive-board'}}")
|
span.js-star-board(
|
||||||
else if isAdministrable
|
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||||
i.fa.js-clone-board(
|
title="{{_ 'star-board-title'}}")
|
||||||
class="fa-clone"
|
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||||
title="{{_ 'duplicate-board'}}")
|
else
|
||||||
i.fa.js-archive-board(
|
.board-list-item
|
||||||
class="fa-archive"
|
if BoardMultiSelection.isActive
|
||||||
title="{{_ 'archive-board'}}")
|
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||||
else if currentUser.isAdmin
|
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||||
i.fa.js-clone-board(
|
span.board-handle(title="{{_ 'drag-board'}}") ↕️
|
||||||
class="fa-clone"
|
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||||
title="{{_ 'duplicate-board'}}")
|
span.details
|
||||||
i.fa.js-archive-board(
|
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
|
||||||
class="fa-archive"
|
+viewer
|
||||||
title="{{_ 'archive-board'}}")
|
= title
|
||||||
|
unless currentSetting.hideBoardMemberList
|
||||||
|
if allowsBoardMemberList
|
||||||
|
.minicard-members
|
||||||
|
each member in boardMembers _id
|
||||||
|
a.name
|
||||||
|
+userAvatar(userId=member noRemove=true)
|
||||||
|
unless currentSetting.hideCardCounterList
|
||||||
|
if allowsCardCounterList
|
||||||
|
.minicard-lists.flex.flex-wrap
|
||||||
|
each list in boardLists _id
|
||||||
|
.item
|
||||||
|
| {{ list }}
|
||||||
|
p.board-list-item-desc
|
||||||
|
+viewer
|
||||||
|
= description
|
||||||
|
if hasSpentTimeCards
|
||||||
|
span.js-has-spenttime-cards(
|
||||||
|
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
|
||||||
|
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||||
|
| ⏱️
|
||||||
|
a.js-star-board(
|
||||||
|
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||||
|
title="{{_ 'star-board-title'}}")
|
||||||
|
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||||
|
|
||||||
template(name="boardListHeaderBar")
|
template(name="boardListHeaderBar")
|
||||||
h1 {{_ title }}
|
h1 {{_ title }}
|
||||||
|
|
@ -154,3 +166,25 @@ template(name="boardListHeaderBar")
|
||||||
// a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
|
// a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
|
||||||
// i.fa.fa-clone
|
// i.fa.fa-clone
|
||||||
// span {{_ 'templates'}}
|
// span {{_ 'templates'}}
|
||||||
|
|
||||||
|
// Recursive template for workspaces tree
|
||||||
|
template(name="workspaceTree")
|
||||||
|
if nodes
|
||||||
|
ul.workspace-tree.js-workspace-tree
|
||||||
|
each nodes
|
||||||
|
li.workspace-node(class="{{#if $eq id selectedWorkspaceId}}active{{/if}}" data-workspace-id="{{id}}" draggable="true")
|
||||||
|
.workspace-node-content
|
||||||
|
span.workspace-drag-handle ↕️
|
||||||
|
a.js-select-workspace(data-id="{{id}}")
|
||||||
|
span.workspace-icon
|
||||||
|
if icon
|
||||||
|
+viewer
|
||||||
|
= icon
|
||||||
|
else
|
||||||
|
| 📁
|
||||||
|
span.workspace-name= name
|
||||||
|
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
|
||||||
|
+workspaceTree(nodes=children selectedWorkspaceId=selectedWorkspaceId)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ Template.boardList.helpers({
|
||||||
return Utils.isMiniScreen() && Session.get('currentBoard'); */
|
return Utils.isMiniScreen() && Session.get('currentBoard'); */
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
BoardMultiSelection() {
|
||||||
|
return BoardMultiSelection;
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
Template.boardListHeaderBar.events({
|
Template.boardListHeaderBar.events({
|
||||||
|
|
@ -45,6 +48,9 @@ BlazeComponent.extendComponent({
|
||||||
onCreated() {
|
onCreated() {
|
||||||
Meteor.subscribe('setting');
|
Meteor.subscribe('setting');
|
||||||
Meteor.subscribe('tableVisibilityModeSettings');
|
Meteor.subscribe('tableVisibilityModeSettings');
|
||||||
|
this.selectedMenu = new ReactiveVar('starred');
|
||||||
|
this.selectedWorkspaceIdVar = new ReactiveVar(null);
|
||||||
|
this.workspacesTreeVar = new ReactiveVar([]);
|
||||||
let currUser = ReactiveCache.getCurrentUser();
|
let currUser = ReactiveCache.getCurrentUser();
|
||||||
let userLanguage;
|
let userLanguage;
|
||||||
if (currUser && currUser.profile) {
|
if (currUser && currUser.profile) {
|
||||||
|
|
@ -53,9 +59,72 @@ BlazeComponent.extendComponent({
|
||||||
if (userLanguage) {
|
if (userLanguage) {
|
||||||
TAPi18n.setLanguage(userLanguage);
|
TAPi18n.setLanguage(userLanguage);
|
||||||
}
|
}
|
||||||
|
// Load workspaces tree reactively
|
||||||
|
this.autorun(() => {
|
||||||
|
const u = ReactiveCache.getCurrentUser();
|
||||||
|
const tree = (u && u.profile && u.profile.boardWorkspacesTree) || [];
|
||||||
|
this.workspacesTreeVar.set(tree);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
reorderWorkspaces(draggedSpaceId, targetSpaceId) {
|
||||||
|
const tree = this.workspacesTreeVar.get();
|
||||||
|
|
||||||
|
// Helper to remove a space from tree
|
||||||
|
const removeSpace = (nodes, id) => {
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
if (nodes[i].id === id) {
|
||||||
|
const removed = nodes.splice(i, 1)[0];
|
||||||
|
return { tree: nodes, removed };
|
||||||
|
}
|
||||||
|
if (nodes[i].children) {
|
||||||
|
const result = removeSpace(nodes[i].children, id);
|
||||||
|
if (result.removed) {
|
||||||
|
return { tree: nodes, removed: result.removed };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { tree: nodes, removed: null };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to insert a space after target
|
||||||
|
const insertAfter = (nodes, targetId, spaceToInsert) => {
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
if (nodes[i].id === targetId) {
|
||||||
|
nodes.splice(i + 1, 0, spaceToInsert);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (nodes[i].children) {
|
||||||
|
if (insertAfter(nodes[i].children, targetId, spaceToInsert)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clone the tree
|
||||||
|
const newTree = EJSON.clone(tree);
|
||||||
|
|
||||||
|
// Remove the dragged space
|
||||||
|
const { tree: treeAfterRemoval, removed } = removeSpace(newTree, draggedSpaceId);
|
||||||
|
|
||||||
|
if (removed) {
|
||||||
|
// Insert after target
|
||||||
|
insertAfter(treeAfterRemoval, targetSpaceId, removed);
|
||||||
|
|
||||||
|
// Save the new tree
|
||||||
|
Meteor.call('setWorkspacesTree', treeAfterRemoval, (err) => {
|
||||||
|
if (err) console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRendered() {
|
onRendered() {
|
||||||
|
// jQuery sortable is disabled in favor of HTML5 drag-and-drop for space management
|
||||||
|
// The old sortable code has been removed to prevent conflicts
|
||||||
|
|
||||||
|
/* OLD SORTABLE CODE - DISABLED
|
||||||
const itemsSelector = '.js-board:not(.placeholder)';
|
const itemsSelector = '.js-board:not(.placeholder)';
|
||||||
|
|
||||||
const $boards = this.$('.js-boards');
|
const $boards = this.$('.js-boards');
|
||||||
|
|
@ -73,27 +142,20 @@ BlazeComponent.extendComponent({
|
||||||
EscapeActions.executeUpTo('popup-close');
|
EscapeActions.executeUpTo('popup-close');
|
||||||
},
|
},
|
||||||
stop(evt, ui) {
|
stop(evt, ui) {
|
||||||
// To attribute the new index number, we need to get the DOM element
|
|
||||||
// of the previous and the following card -- if any.
|
|
||||||
const prevBoardDom = ui.item.prev('.js-board').get(0);
|
const prevBoardDom = ui.item.prev('.js-board').get(0);
|
||||||
const nextBoardBom = ui.item.next('.js-board').get(0);
|
const nextBoardDom = ui.item.next('.js-board').get(0);
|
||||||
const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardBom, 1);
|
const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardDom, 1);
|
||||||
|
|
||||||
const boardDomElement = ui.item.get(0);
|
const boardDomElement = ui.item.get(0);
|
||||||
const board = Blaze.getData(boardDomElement);
|
const board = Blaze.getData(boardDomElement);
|
||||||
// Normally the jquery-ui sortable library moves the dragged DOM element
|
|
||||||
// to its new position, which disrupts Blaze reactive updates mechanism
|
|
||||||
// (especially when we move the last card of a list, or when multiple
|
|
||||||
// users move some cards at the same time). To prevent these UX glitches
|
|
||||||
// we ask sortable to gracefully cancel the move, and to put back the
|
|
||||||
// DOM in its initial state. The card move is then handled reactively by
|
|
||||||
// Blaze with the below query.
|
|
||||||
$boards.sortable('cancel');
|
$boards.sortable('cancel');
|
||||||
board.move(sortIndex.base);
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
if (currentUser && typeof currentUser.setBoardSortIndex === 'function') {
|
||||||
|
currentUser.setBoardSortIndex(board._id, sortIndex.base);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Disable drag-dropping if the current user is not a board member or is comment only
|
|
||||||
this.autorun(() => {
|
this.autorun(() => {
|
||||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||||
$boards.sortable({
|
$boards.sortable({
|
||||||
|
|
@ -101,6 +163,7 @@ BlazeComponent.extendComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
},
|
},
|
||||||
userHasTeams() {
|
userHasTeams() {
|
||||||
if (ReactiveCache.getCurrentUser()?.teams?.length > 0)
|
if (ReactiveCache.getCurrentUser()?.teams?.length > 0)
|
||||||
|
|
@ -132,6 +195,41 @@ BlazeComponent.extendComponent({
|
||||||
const ret = this.userHasOrgs() || this.userHasTeams();
|
const ret = this.userHasOrgs() || this.userHasTeams();
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
|
currentMenuPath() {
|
||||||
|
const sel = this.selectedMenu.get();
|
||||||
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
|
||||||
|
// Helper to find space by id in tree
|
||||||
|
const findSpaceById = (nodes, targetId, path = []) => {
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.id === targetId) {
|
||||||
|
return [...path, node];
|
||||||
|
}
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
const result = findSpaceById(node.children, targetId, [...path, node]);
|
||||||
|
if (result) return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sel === 'starred') {
|
||||||
|
return { icon: '⭐', text: TAPi18n.__('allboards.starred') };
|
||||||
|
} else if (sel === 'templates') {
|
||||||
|
return { icon: '📋', text: TAPi18n.__('allboards.templates') };
|
||||||
|
} else if (sel === 'remaining') {
|
||||||
|
return { icon: '📂', text: TAPi18n.__('allboards.remaining') };
|
||||||
|
} else {
|
||||||
|
// sel is a workspaceId, build path
|
||||||
|
const tree = this.workspacesTreeVar.get();
|
||||||
|
const spacePath = findSpaceById(tree, sel);
|
||||||
|
if (spacePath && spacePath.length > 0) {
|
||||||
|
const pathText = spacePath.map(s => s.name).join(' / ');
|
||||||
|
return { icon: '🗂️', text: `${TAPi18n.__('allboards.workspaces')} / ${pathText}` };
|
||||||
|
}
|
||||||
|
return { icon: '🗂️', text: TAPi18n.__('allboards.workspaces') };
|
||||||
|
}
|
||||||
|
},
|
||||||
boards() {
|
boards() {
|
||||||
let query = {
|
let query = {
|
||||||
// { type: 'board' },
|
// { type: 'board' },
|
||||||
|
|
@ -184,10 +282,33 @@ BlazeComponent.extendComponent({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const ret = ReactiveCache.getBoards(query, {
|
const boards = ReactiveCache.getBoards(query, {});
|
||||||
sort: { sort: 1 /* boards default sorting */ },
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
});
|
let list = boards;
|
||||||
return ret;
|
// Apply left menu filtering
|
||||||
|
const sel = this.selectedMenu.get();
|
||||||
|
const assignments = (currentUser && currentUser.profile && currentUser.profile.boardWorkspaceAssignments) || {};
|
||||||
|
if (sel === 'starred') {
|
||||||
|
list = list.filter(b => currentUser && currentUser.hasStarred(b._id));
|
||||||
|
} else if (sel === 'templates') {
|
||||||
|
list = list.filter(b => b.type === 'template-container');
|
||||||
|
} else if (sel === 'remaining') {
|
||||||
|
// Show boards not in any workspace AND not templates
|
||||||
|
// Keep starred boards visible in Remaining too
|
||||||
|
list = list.filter(b =>
|
||||||
|
!assignments[b._id] &&
|
||||||
|
b.type !== 'template-container'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// assume sel is a workspaceId
|
||||||
|
// Keep starred boards visible in their workspace too
|
||||||
|
list = list.filter(b => assignments[b._id] === sel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentUser && typeof currentUser.sortBoardsForUser === 'function') {
|
||||||
|
return currentUser.sortBoardsForUser(list);
|
||||||
|
}
|
||||||
|
return list.slice().sort((a, b) => (a.title || '').localeCompare(b.title || ''));
|
||||||
},
|
},
|
||||||
boardLists(boardId) {
|
boardLists(boardId) {
|
||||||
/* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
|
/* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
|
||||||
|
|
@ -235,11 +356,65 @@ BlazeComponent.extendComponent({
|
||||||
events() {
|
events() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'click .js-add-board': Popup.open('createBoard'),
|
'click .js-select-menu'(evt) {
|
||||||
'click .js-star-board'(evt) {
|
const type = evt.currentTarget.getAttribute('data-type');
|
||||||
const boardId = this.currentData()._id;
|
this.selectedWorkspaceIdVar.set(null);
|
||||||
ReactiveCache.getCurrentUser().toggleBoardStar(boardId);
|
this.selectedMenu.set(type);
|
||||||
|
},
|
||||||
|
'click .js-select-workspace'(evt) {
|
||||||
|
const id = evt.currentTarget.getAttribute('data-id');
|
||||||
|
this.selectedWorkspaceIdVar.set(id);
|
||||||
|
this.selectedMenu.set(id);
|
||||||
|
},
|
||||||
|
'click .js-add-workspace'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
const name = prompt(TAPi18n.__('allboards.add-workspace-prompt') || 'New Space name');
|
||||||
|
if (name && name.trim()) {
|
||||||
|
Meteor.call('createWorkspace', { parentId: null, name: name.trim() }, (err, res) => {
|
||||||
|
if (err) console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'click .js-add-board'(evt) {
|
||||||
|
// Store the currently selected workspace/menu for board creation
|
||||||
|
const selectedWorkspaceId = this.selectedWorkspaceIdVar.get();
|
||||||
|
const selectedMenu = this.selectedMenu.get();
|
||||||
|
|
||||||
|
if (selectedWorkspaceId) {
|
||||||
|
Session.set('createBoardInWorkspace', selectedWorkspaceId);
|
||||||
|
} else {
|
||||||
|
Session.set('createBoardInWorkspace', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open different popup based on context
|
||||||
|
if (selectedMenu === 'templates') {
|
||||||
|
Popup.open('createTemplateContainer')(evt);
|
||||||
|
} else {
|
||||||
|
Popup.open('createBoard')(evt);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'click .js-star-board'(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
const boardId = this.currentData()._id;
|
||||||
|
if (boardId) {
|
||||||
|
Meteor.call('toggleBoardStar', boardId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// HTML5 DnD from boards to spaces
|
||||||
|
'dragstart .js-board'(evt) {
|
||||||
|
const boardId = this.currentData()._id;
|
||||||
|
|
||||||
|
// Support multi-drag
|
||||||
|
if (BoardMultiSelection.isActive() && BoardMultiSelection.isSelected(boardId)) {
|
||||||
|
const selectedIds = BoardMultiSelection.getSelectedBoardIds();
|
||||||
|
try {
|
||||||
|
evt.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(selectedIds));
|
||||||
|
evt.originalEvent.dataTransfer.setData('application/x-board-multi', 'true');
|
||||||
|
} catch (e) {}
|
||||||
|
} else {
|
||||||
|
try { evt.originalEvent.dataTransfer.setData('text/plain', boardId); } catch (e) {}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'click .js-clone-board'(evt) {
|
'click .js-clone-board'(evt) {
|
||||||
if (confirm(TAPi18n.__('duplicate-board-confirm'))) {
|
if (confirm(TAPi18n.__('duplicate-board-confirm'))) {
|
||||||
|
|
@ -290,6 +465,58 @@ BlazeComponent.extendComponent({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
'click .js-multiselection-activate'(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
if (BoardMultiSelection.isActive()) {
|
||||||
|
BoardMultiSelection.disable();
|
||||||
|
} else {
|
||||||
|
BoardMultiSelection.activate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'click .js-multiselection-reset'(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
BoardMultiSelection.disable();
|
||||||
|
},
|
||||||
|
'click .js-toggle-board-multi-selection'(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
const boardId = this.currentData()._id;
|
||||||
|
BoardMultiSelection.toogle(boardId);
|
||||||
|
},
|
||||||
|
'click .js-archive-selected-boards'(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
const selectedBoards = BoardMultiSelection.getSelectedBoardIds();
|
||||||
|
if (selectedBoards.length > 0 && confirm(TAPi18n.__('archive-board-confirm'))) {
|
||||||
|
selectedBoards.forEach(boardId => {
|
||||||
|
Meteor.call('archiveBoard', boardId);
|
||||||
|
});
|
||||||
|
BoardMultiSelection.reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'click .js-duplicate-selected-boards'(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
const selectedBoards = BoardMultiSelection.getSelectedBoardIds();
|
||||||
|
if (selectedBoards.length > 0 && confirm(TAPi18n.__('duplicate-board-confirm'))) {
|
||||||
|
selectedBoards.forEach(boardId => {
|
||||||
|
const board = ReactiveCache.getBoard(boardId);
|
||||||
|
if (board) {
|
||||||
|
Meteor.call(
|
||||||
|
'copyBoard',
|
||||||
|
boardId,
|
||||||
|
{
|
||||||
|
sort: ReactiveCache.getBoards({ archived: false }).length,
|
||||||
|
type: 'board',
|
||||||
|
title: board.title,
|
||||||
|
},
|
||||||
|
(err, res) => {
|
||||||
|
if (err) console.error(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
BoardMultiSelection.reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
'click #resetBtn'(event) {
|
'click #resetBtn'(event) {
|
||||||
let allBoards = document.getElementsByClassName("js-board");
|
let allBoards = document.getElementsByClassName("js-board");
|
||||||
let currBoard;
|
let currBoard;
|
||||||
|
|
@ -356,7 +583,260 @@ BlazeComponent.extendComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'click .js-edit-workspace'(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
const workspaceId = evt.currentTarget.getAttribute('data-id');
|
||||||
|
|
||||||
|
// Find the space in the tree
|
||||||
|
const findSpace = (nodes, id) => {
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.id === id) return node;
|
||||||
|
if (node.children) {
|
||||||
|
const found = findSpace(node.children, id);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tree = this.workspacesTreeVar.get();
|
||||||
|
const space = findSpace(tree, workspaceId);
|
||||||
|
|
||||||
|
if (space) {
|
||||||
|
const newName = prompt(TAPi18n.__('allboards.edit-workspace-name') || 'Space name:', space.name);
|
||||||
|
const newIcon = prompt(TAPi18n.__('allboards.edit-workspace-icon') || 'Space icon (markdown):', space.icon || '📁');
|
||||||
|
|
||||||
|
if (newName !== null && newName.trim()) {
|
||||||
|
// Update space in tree
|
||||||
|
const updateSpaceInTree = (nodes, id, updates) => {
|
||||||
|
return nodes.map(node => {
|
||||||
|
if (node.id === id) {
|
||||||
|
return { ...node, ...updates };
|
||||||
|
}
|
||||||
|
if (node.children) {
|
||||||
|
return { ...node, children: updateSpaceInTree(node.children, id, updates) };
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedTree = updateSpaceInTree(tree, workspaceId, {
|
||||||
|
name: newName.trim(),
|
||||||
|
icon: newIcon || '📁'
|
||||||
|
});
|
||||||
|
|
||||||
|
Meteor.call('setWorkspacesTree', updatedTree, (err) => {
|
||||||
|
if (err) console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'click .js-add-subworkspace'(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
const parentId = evt.currentTarget.getAttribute('data-id');
|
||||||
|
const name = prompt(TAPi18n.__('allboards.add-subworkspace-prompt') || 'Subspace name:');
|
||||||
|
|
||||||
|
if (name && name.trim()) {
|
||||||
|
Meteor.call('createWorkspace', { parentId, name: name.trim() }, (err) => {
|
||||||
|
if (err) console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'dragstart .workspace-node'(evt) {
|
||||||
|
const workspaceId = evt.currentTarget.getAttribute('data-workspace-id');
|
||||||
|
evt.originalEvent.dataTransfer.effectAllowed = 'move';
|
||||||
|
evt.originalEvent.dataTransfer.setData('application/x-workspace-id', workspaceId);
|
||||||
|
|
||||||
|
// Create a better drag image
|
||||||
|
const dragImage = evt.currentTarget.cloneNode(true);
|
||||||
|
dragImage.style.position = 'absolute';
|
||||||
|
dragImage.style.top = '-9999px';
|
||||||
|
dragImage.style.opacity = '0.8';
|
||||||
|
document.body.appendChild(dragImage);
|
||||||
|
evt.originalEvent.dataTransfer.setDragImage(dragImage, 0, 0);
|
||||||
|
setTimeout(() => document.body.removeChild(dragImage), 0);
|
||||||
|
|
||||||
|
evt.currentTarget.classList.add('dragging');
|
||||||
|
},
|
||||||
|
'dragend .workspace-node'(evt) {
|
||||||
|
evt.currentTarget.classList.remove('dragging');
|
||||||
|
document.querySelectorAll('.workspace-node').forEach(el => {
|
||||||
|
el.classList.remove('drag-over');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'dragover .workspace-node'(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
const draggingEl = document.querySelector('.workspace-node.dragging');
|
||||||
|
const targetEl = evt.currentTarget;
|
||||||
|
|
||||||
|
// Allow dropping boards on any space
|
||||||
|
// Or allow dropping spaces on other spaces (but not on itself or descendants)
|
||||||
|
if (!draggingEl || (targetEl !== draggingEl && !draggingEl.contains(targetEl))) {
|
||||||
|
evt.originalEvent.dataTransfer.dropEffect = 'move';
|
||||||
|
targetEl.classList.add('drag-over');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'dragleave .workspace-node'(evt) {
|
||||||
|
evt.currentTarget.classList.remove('drag-over');
|
||||||
|
},
|
||||||
|
'drop .workspace-node'(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
const targetEl = evt.currentTarget;
|
||||||
|
targetEl.classList.remove('drag-over');
|
||||||
|
|
||||||
|
// Check what's being dropped - board or workspace
|
||||||
|
const draggedWorkspaceId = evt.originalEvent.dataTransfer.getData('application/x-workspace-id');
|
||||||
|
const isMultiBoard = evt.originalEvent.dataTransfer.getData('application/x-board-multi');
|
||||||
|
const boardData = evt.originalEvent.dataTransfer.getData('text/plain');
|
||||||
|
|
||||||
|
if (draggedWorkspaceId && !boardData) {
|
||||||
|
// This is a workspace reorder operation
|
||||||
|
const targetWorkspaceId = targetEl.getAttribute('data-workspace-id');
|
||||||
|
|
||||||
|
if (draggedWorkspaceId !== targetWorkspaceId) {
|
||||||
|
this.reorderWorkspaces(draggedWorkspaceId, targetWorkspaceId);
|
||||||
|
}
|
||||||
|
} else if (boardData) {
|
||||||
|
// This is a board assignment operation
|
||||||
|
// Get the workspace ID directly from the dropped workspace-node's data-workspace-id attribute
|
||||||
|
const workspaceId = targetEl.getAttribute('data-workspace-id');
|
||||||
|
|
||||||
|
if (workspaceId) {
|
||||||
|
if (isMultiBoard) {
|
||||||
|
// Multi-board drag
|
||||||
|
try {
|
||||||
|
const boardIds = JSON.parse(boardData);
|
||||||
|
boardIds.forEach(boardId => {
|
||||||
|
Meteor.call('assignBoardToWorkspace', boardId, workspaceId);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Error parsing multi-board data
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single board drag
|
||||||
|
Meteor.call('assignBoardToWorkspace', boardData, workspaceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'dragover .js-select-menu'(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
const menuType = evt.currentTarget.getAttribute('data-type');
|
||||||
|
// Only allow drop on "remaining" menu to unassign boards from spaces
|
||||||
|
if (menuType === 'remaining') {
|
||||||
|
evt.originalEvent.dataTransfer.dropEffect = 'move';
|
||||||
|
evt.currentTarget.classList.add('drag-over');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'dragleave .js-select-menu'(evt) {
|
||||||
|
evt.currentTarget.classList.remove('drag-over');
|
||||||
|
},
|
||||||
|
'drop .js-select-menu'(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
const menuType = evt.currentTarget.getAttribute('data-type');
|
||||||
|
evt.currentTarget.classList.remove('drag-over');
|
||||||
|
|
||||||
|
// Only handle drops on "remaining" menu
|
||||||
|
if (menuType !== 'remaining') return;
|
||||||
|
|
||||||
|
const isMultiBoard = evt.originalEvent.dataTransfer.getData('application/x-board-multi');
|
||||||
|
const boardData = evt.originalEvent.dataTransfer.getData('text/plain');
|
||||||
|
|
||||||
|
if (boardData) {
|
||||||
|
if (isMultiBoard) {
|
||||||
|
// Multi-board drag - unassign all from workspaces
|
||||||
|
try {
|
||||||
|
const boardIds = JSON.parse(boardData);
|
||||||
|
boardIds.forEach(boardId => {
|
||||||
|
Meteor.call('unassignBoardFromWorkspace', boardId);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Error parsing multi-board data
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single board drag - unassign from workspace
|
||||||
|
Meteor.call('unassignBoardFromWorkspace', boardData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
// Helpers for templates
|
||||||
|
workspacesTree() {
|
||||||
|
return this.workspacesTreeVar.get();
|
||||||
|
},
|
||||||
|
selectedWorkspaceId() {
|
||||||
|
return this.selectedWorkspaceIdVar.get();
|
||||||
|
},
|
||||||
|
isSelectedMenu(type) {
|
||||||
|
return this.selectedMenu.get() === type;
|
||||||
|
},
|
||||||
|
isSpaceSelected(id) {
|
||||||
|
return this.selectedWorkspaceIdVar.get() === id;
|
||||||
|
},
|
||||||
|
menuItemCount(type) {
|
||||||
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
const assignments = (currentUser && currentUser.profile && currentUser.profile.boardWorkspaceAssignments) || {};
|
||||||
|
|
||||||
|
// Get all boards for counting
|
||||||
|
let query = {
|
||||||
|
$and: [
|
||||||
|
{ archived: false },
|
||||||
|
{ type: { $in: ['board', 'template-container'] } },
|
||||||
|
{ $or: [{ 'members.userId': Meteor.userId() }] },
|
||||||
|
{ title: { $not: { $regex: /^\^.*\^$/ } } }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const allBoards = ReactiveCache.getBoards(query, {});
|
||||||
|
|
||||||
|
if (type === 'starred') {
|
||||||
|
return allBoards.filter(b => currentUser && currentUser.hasStarred(b._id)).length;
|
||||||
|
} else if (type === 'templates') {
|
||||||
|
return allBoards.filter(b => b.type === 'template-container').length;
|
||||||
|
} else if (type === 'remaining') {
|
||||||
|
// Count boards not in any workspace AND not templates
|
||||||
|
// Include starred boards (they appear in both Starred and Remaining)
|
||||||
|
return allBoards.filter(b =>
|
||||||
|
!assignments[b._id] &&
|
||||||
|
b.type !== 'template-container'
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
workspaceCount(workspaceId) {
|
||||||
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
const assignments = (currentUser && currentUser.profile && currentUser.profile.boardWorkspaceAssignments) || {};
|
||||||
|
|
||||||
|
// Get all boards for counting
|
||||||
|
let query = {
|
||||||
|
$and: [
|
||||||
|
{ archived: false },
|
||||||
|
{ type: { $in: ['board', 'template-container'] } },
|
||||||
|
{ $or: [{ 'members.userId': Meteor.userId() }] },
|
||||||
|
{ title: { $not: { $regex: /^\^.*\^$/ } } }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const allBoards = ReactiveCache.getBoards(query, {});
|
||||||
|
|
||||||
|
// Count boards directly assigned to this space (not including children)
|
||||||
|
return allBoards.filter(b => assignments[b._id] === workspaceId).length;
|
||||||
|
},
|
||||||
|
canModifyBoards() {
|
||||||
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
return currentUser && !currentUser.isCommentOnly();
|
||||||
|
},
|
||||||
|
hasBoardsSelected() {
|
||||||
|
return BoardMultiSelection.count() > 0;
|
||||||
|
},
|
||||||
}).register('boardList');
|
}).register('boardList');
|
||||||
|
|
|
||||||
|
|
@ -86,17 +86,17 @@ template(name="attachmentGallery")
|
||||||
= name
|
= name
|
||||||
span.file-size ({{fileSize size}})
|
span.file-size ({{fileSize size}})
|
||||||
.attachment-actions
|
.attachment-actions
|
||||||
a.js-download(href="{{link}}?download=true", download="{{name}}")
|
a.js-download(href="{{link}}?download=true", download="{{name}}", title="{{_ 'download'}}")
|
||||||
| ⬇️(title="{{_ 'download'}}")
|
| ⬇️
|
||||||
if currentUser.isBoardMember
|
if currentUser.isBoardMember
|
||||||
unless currentUser.isCommentOnly
|
unless currentUser.isCommentOnly
|
||||||
unless currentUser.isWorker
|
unless currentUser.isWorker
|
||||||
a.js-rename
|
a.js-rename(title="{{_ 'rename'}}")
|
||||||
| ✏️(title="{{_ 'rename'}}")
|
| ✏️
|
||||||
a.js-confirm-delete
|
a.js-confirm-delete(title="{{_ 'delete'}}")
|
||||||
| 🗑️(title="{{_ 'delete'}}")
|
| 🗑️
|
||||||
a.js-open-attachment-menu
|
a.js-open-attachment-menu(data-attachment-link="{{link}}", title="{{_ 'attachmentActionsPopup-title'}}")
|
||||||
| ☰(data-attachment-link="{{link}}" title="{{_ 'attachmentActionsPopup-title'}}")
|
| ☰
|
||||||
|
|
||||||
// Migration spinner overlay
|
// Migration spinner overlay
|
||||||
if isAttachmentMigrating _id
|
if isAttachmentMigrating _id
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,7 @@ export function handleFileUpload(card, files) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user can modify the card
|
// Check if user can modify the card
|
||||||
if (!card.canModifyCard()) {
|
if (!Utils.canModifyCard()) {
|
||||||
if (process.env.DEBUG === 'true') {
|
if (process.env.DEBUG === 'true') {
|
||||||
console.warn('User does not have permission to modify this card');
|
console.warn('User does not have permission to modify this card');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { TAPi18n } from '/imports/i18n';
|
import { TAPi18n } from '/imports/i18n';
|
||||||
import { DatePicker } from '/client/lib/datepicker';
|
import { DatePicker } from '/client/lib/datepicker';
|
||||||
|
import { ReactiveCache } from '/imports/reactiveCache';
|
||||||
import {
|
import {
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
formatDate,
|
formatDate,
|
||||||
|
formatDateByUserPreference,
|
||||||
formatTime,
|
formatTime,
|
||||||
getISOWeek,
|
getISOWeek,
|
||||||
isValidDate,
|
isValidDate,
|
||||||
|
|
@ -168,11 +170,18 @@ CardCustomField.register('cardCustomField');
|
||||||
}
|
}
|
||||||
|
|
||||||
showWeekOfYear() {
|
showWeekOfYear() {
|
||||||
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
|
const user = ReactiveCache.getCurrentUser();
|
||||||
|
if (!user) {
|
||||||
|
// For non-logged-in users, week of year is not shown
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return user.isShowWeekOfYear();
|
||||||
}
|
}
|
||||||
|
|
||||||
showDate() {
|
showDate() {
|
||||||
return calendar(this.date.get());
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||||
|
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
showISODate() {
|
showISODate() {
|
||||||
|
|
|
||||||
|
|
@ -8,76 +8,112 @@
|
||||||
.card-date.is-active {
|
.card-date.is-active {
|
||||||
background-color: #b3b3b3;
|
background-color: #b3b3b3;
|
||||||
}
|
}
|
||||||
.card-date.current,
|
/* Date status colors - red = overdue, amber = due soon, no shade = not due */
|
||||||
.card-date.almost-due,
|
.card-date.overdue {
|
||||||
.card-date.due,
|
background-color: #ff4444; /* Red for overdue */
|
||||||
.card-date.long-overdue {
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
.card-date.overdue:hover,
|
||||||
|
.card-date.overdue.is-active {
|
||||||
|
background-color: #cc3333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-date.due-soon {
|
||||||
|
background-color: #ffaa00; /* Amber for due soon */
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.card-date.due-soon:hover,
|
||||||
|
.card-date.due-soon.is-active {
|
||||||
|
background-color: #e69900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-date.not-due {
|
||||||
|
/* No special background - uses default date type colors */
|
||||||
|
}
|
||||||
|
|
||||||
.card-date.current {
|
.card-date.current {
|
||||||
background-color: #5ba639;
|
background-color: #5ba639; /* Green for current/active */
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
.card-date.current:hover,
|
.card-date.current:hover,
|
||||||
.card-date.current.is-active {
|
.card-date.current.is-active {
|
||||||
background-color: #46802c;
|
background-color: #46802c;
|
||||||
}
|
}
|
||||||
.card-date.almost-due {
|
|
||||||
background-color: #edc909;
|
.card-date.completed {
|
||||||
|
background-color: #90ee90; /* Light green for completed */
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
.card-date.almost-due:hover,
|
.card-date.completed:hover,
|
||||||
.card-date.almost-due.is-active {
|
.card-date.completed.is-active {
|
||||||
background-color: #bc9f07;
|
background-color: #7dd87d;
|
||||||
}
|
}
|
||||||
.card-date.due {
|
|
||||||
background-color: #fa3f00;
|
.card-date.completed-early {
|
||||||
|
background-color: #4caf50; /* Green for completed early */
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
.card-date.due:hover,
|
.card-date.completed-early:hover,
|
||||||
.card-date.due.is-active {
|
.card-date.completed-early.is-active {
|
||||||
background-color: #c73200;
|
background-color: #45a049;
|
||||||
}
|
}
|
||||||
.card-date.long-overdue {
|
|
||||||
background-color: #fd5d47;
|
.card-date.completed-late {
|
||||||
|
background-color: #ff9800; /* Orange for completed late */
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
.card-date.long-overdue:hover,
|
.card-date.completed-late:hover,
|
||||||
.card-date.long-overdue.is-active {
|
.card-date.completed-late.is-active {
|
||||||
background-color: #fd3e24;
|
background-color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-date.completed-on-time {
|
||||||
|
background-color: #2196f3; /* Blue for completed on time */
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.card-date.completed-on-time:hover,
|
||||||
|
.card-date.completed-on-time.is-active {
|
||||||
|
background-color: #1976d2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Date type specific colors */
|
/* Date type specific colors */
|
||||||
.card-date.received-date {
|
.card-date.received-date {
|
||||||
background-color: #0079bf; /* Blue for received */
|
background-color: #dbdbdb; /* Light grey for received */
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-date.received-date:hover,
|
.card-date.received-date:hover,
|
||||||
.card-date.received-date.is-active {
|
.card-date.received-date.is-active {
|
||||||
background-color: #005a8b;
|
background-color: #b3b3b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-date.start-date {
|
.card-date.start-date {
|
||||||
background-color: #3cb500; /* Green for start */
|
background-color: #90ee90; /* Light green for start */
|
||||||
|
color: #000; /* Black text for start */
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-date.start-date:hover,
|
.card-date.start-date:hover,
|
||||||
.card-date.start-date.is-active {
|
.card-date.start-date.is-active {
|
||||||
background-color: #2d8f00;
|
background-color: #7dd87d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-date.due-date {
|
.card-date.due-date {
|
||||||
background-color: #ff9f19; /* Orange for due */
|
background-color: #ffd700; /* Yellow for due */
|
||||||
|
color: #000; /* Black text for due */
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-date.due-date:hover,
|
.card-date.due-date:hover,
|
||||||
.card-date.due-date.is-active {
|
.card-date.due-date.is-active {
|
||||||
background-color: #e68a00;
|
background-color: #e6c200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-date.end-date {
|
.card-date.end-date {
|
||||||
background-color: #a632db; /* Purple for end */
|
background-color: #ffb3b3; /* Light red for end */
|
||||||
|
color: #000; /* Black text for end */
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-date.end-date:hover,
|
.card-date.end-date:hover,
|
||||||
.card-date.end-date.is-active {
|
.card-date.end-date.is-active {
|
||||||
background-color: #8a2bb8;
|
background-color: #ff9999;
|
||||||
}
|
}
|
||||||
.card-date.end-date time::before {
|
.card-date.end-date time::before {
|
||||||
content: "🏁"; /* Finish flag - represents end/completion */
|
content: "🏁"; /* Finish flag - represents end/completion */
|
||||||
|
|
@ -94,7 +130,7 @@
|
||||||
|
|
||||||
/* Generic date badge and custom field date */
|
/* Generic date badge and custom field date */
|
||||||
.card-date:not(.received-date):not(.start-date):not(.due-date):not(.end-date) time::before {
|
.card-date:not(.received-date):not(.start-date):not(.due-date):not(.end-date) time::before {
|
||||||
content: "📅"; /* Calendar - represents generic date */
|
/*content: "📅"; // Calendar - represents generic date */
|
||||||
}
|
}
|
||||||
.card-date time::before {
|
.card-date time::before {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,14 @@ template(name="dateCustomField")
|
||||||
|
|
||||||
template(name="minicardReceivedDate")
|
template(name="minicardReceivedDate")
|
||||||
if canModifyCard
|
if canModifyCard
|
||||||
a.js-edit-date.card-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
a.js-edit-date.card-date.received-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||||
time(datetime="{{showISODate}}")
|
time(datetime="{{showISODate}}")
|
||||||
| {{showDate}}
|
| {{showDate}}
|
||||||
if showWeekOfYear
|
if showWeekOfYear
|
||||||
b
|
b
|
||||||
| {{showWeek}}
|
| {{showWeek}}
|
||||||
else
|
else
|
||||||
a.card-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
a.card-date.received-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||||
time(datetime="{{showISODate}}")
|
time(datetime="{{showISODate}}")
|
||||||
| {{showDate}}
|
| {{showDate}}
|
||||||
if showWeekOfYear
|
if showWeekOfYear
|
||||||
|
|
@ -40,14 +40,14 @@ template(name="minicardReceivedDate")
|
||||||
|
|
||||||
template(name="minicardStartDate")
|
template(name="minicardStartDate")
|
||||||
if canModifyCard
|
if canModifyCard
|
||||||
a.js-edit-date.card-date(title="{{_ 'card-start'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
a.js-edit-date.card-date.start-date(title="{{_ 'card-start'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||||
time(datetime="{{showISODate}}")
|
time(datetime="{{showISODate}}")
|
||||||
| {{showDate}}
|
| {{showDate}}
|
||||||
if showWeekOfYear
|
if showWeekOfYear
|
||||||
b
|
b
|
||||||
| {{showWeek}}
|
| {{showWeek}}
|
||||||
else
|
else
|
||||||
a.card-date(title="{{_ 'card-start'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
a.card-date.start-date(title="{{_ 'card-start'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||||
time(datetime="{{showISODate}}")
|
time(datetime="{{showISODate}}")
|
||||||
| {{showDate}}
|
| {{showDate}}
|
||||||
if showWeekOfYear
|
if showWeekOfYear
|
||||||
|
|
@ -56,14 +56,14 @@ template(name="minicardStartDate")
|
||||||
|
|
||||||
template(name="minicardDueDate")
|
template(name="minicardDueDate")
|
||||||
if canModifyCard
|
if canModifyCard
|
||||||
a.js-edit-date.card-date(title="{{_ 'card-due'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
a.js-edit-date.card-date.due-date(title="{{_ 'card-due'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||||
time(datetime="{{showISODate}}")
|
time(datetime="{{showISODate}}")
|
||||||
| {{showDate}}
|
| {{showDate}}
|
||||||
if showWeekOfYear
|
if showWeekOfYear
|
||||||
b
|
b
|
||||||
| {{showWeek}}
|
| {{showWeek}}
|
||||||
else
|
else
|
||||||
a.card-date(title="{{_ 'card-due'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
a.card-date.due-date(title="{{_ 'card-due'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||||
time(datetime="{{showISODate}}")
|
time(datetime="{{showISODate}}")
|
||||||
| {{showDate}}
|
| {{showDate}}
|
||||||
if showWeekOfYear
|
if showWeekOfYear
|
||||||
|
|
@ -72,14 +72,14 @@ template(name="minicardDueDate")
|
||||||
|
|
||||||
template(name="minicardEndDate")
|
template(name="minicardEndDate")
|
||||||
if canModifyCard
|
if canModifyCard
|
||||||
a.js-edit-date.card-date(title="{{_ 'card-end'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
a.js-edit-date.card-date.end-date(title="{{_ 'card-end'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||||
time(datetime="{{showISODate}}")
|
time(datetime="{{showISODate}}")
|
||||||
| {{showDate}}
|
| {{showDate}}
|
||||||
if showWeekOfYear
|
if showWeekOfYear
|
||||||
b
|
b
|
||||||
| {{showWeek}}
|
| {{showWeek}}
|
||||||
else
|
else
|
||||||
a.card-date(title="{{_ 'card-end'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
a.card-date.end-date(title="{{_ 'card-end'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||||
time(datetime="{{showISODate}}")
|
time(datetime="{{showISODate}}")
|
||||||
| {{showDate}}
|
| {{showDate}}
|
||||||
if showWeekOfYear
|
if showWeekOfYear
|
||||||
|
|
@ -93,3 +93,60 @@ template(name="minicardCustomFieldDate")
|
||||||
if showWeekOfYear
|
if showWeekOfYear
|
||||||
b
|
b
|
||||||
| {{showWeek}}
|
| {{showWeek}}
|
||||||
|
|
||||||
|
template(name="editCardReceivedDatePopup")
|
||||||
|
form.edit-card-received-date
|
||||||
|
.datepicker
|
||||||
|
// Date input field (existing)
|
||||||
|
// Insert calendar selector right after date input
|
||||||
|
.calendar-selector
|
||||||
|
label(for="calendar-received") 🗓️
|
||||||
|
input#calendar-received.js-calendar-date(type="date")
|
||||||
|
// Time input field (if present)
|
||||||
|
.clear-date
|
||||||
|
a.js-clear-date {{_ 'clear'}}
|
||||||
|
.datepicker-actions
|
||||||
|
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||||
|
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||||
|
|
||||||
|
template(name="editCardStartDatePopup")
|
||||||
|
form.edit-card-start-date
|
||||||
|
.datepicker
|
||||||
|
// Date input field (existing)
|
||||||
|
.calendar-selector
|
||||||
|
label(for="calendar-start") 🗓️
|
||||||
|
input#calendar-start.js-calendar-date(type="date")
|
||||||
|
// Time input field (if present)
|
||||||
|
.clear-date
|
||||||
|
a.js-clear-date {{_ 'clear'}}
|
||||||
|
.datepicker-actions
|
||||||
|
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||||
|
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||||
|
|
||||||
|
template(name="editCardDueDatePopup")
|
||||||
|
form.edit-card-due-date
|
||||||
|
.datepicker
|
||||||
|
// Date input field (existing)
|
||||||
|
.calendar-selector
|
||||||
|
label(for="calendar-due") 🗓️
|
||||||
|
input#calendar-due.js-calendar-date(type="date")
|
||||||
|
// Time input field (if present)
|
||||||
|
.clear-date
|
||||||
|
a.js-clear-date {{_ 'clear'}}
|
||||||
|
.datepicker-actions
|
||||||
|
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||||
|
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||||
|
|
||||||
|
template(name="editCardEndDatePopup")
|
||||||
|
form.edit-card-end-date
|
||||||
|
.datepicker
|
||||||
|
// Date input field (existing)
|
||||||
|
.calendar-selector
|
||||||
|
label(for="calendar-end") 🗓️
|
||||||
|
input#calendar-end.js-calendar-date(type="date")
|
||||||
|
// Time input field (if present)
|
||||||
|
.clear-date
|
||||||
|
a.js-clear-date {{_ 'clear'}}
|
||||||
|
.datepicker-actions
|
||||||
|
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||||
|
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { DatePicker } from '/client/lib/datepicker';
|
||||||
import {
|
import {
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
formatDate,
|
formatDate,
|
||||||
|
formatDateByUserPreference,
|
||||||
formatTime,
|
formatTime,
|
||||||
getISOWeek,
|
getISOWeek,
|
||||||
isValidDate,
|
isValidDate,
|
||||||
|
|
@ -18,7 +19,8 @@ import {
|
||||||
now,
|
now,
|
||||||
createDate,
|
createDate,
|
||||||
fromNow,
|
fromNow,
|
||||||
calendar
|
calendar,
|
||||||
|
diff
|
||||||
} from '/imports/lib/dateUtils';
|
} from '/imports/lib/dateUtils';
|
||||||
|
|
||||||
// editCardReceivedDatePopup
|
// editCardReceivedDatePopup
|
||||||
|
|
@ -47,12 +49,18 @@ import {
|
||||||
|
|
||||||
onRendered() {
|
onRendered() {
|
||||||
super.onRendered();
|
super.onRendered();
|
||||||
if (moment.isDate(this.card.getReceived())) {
|
// DatePicker base class handles initialization with native HTML inputs
|
||||||
this.$('.js-datepicker').datepicker(
|
const self = this;
|
||||||
'setStartDate',
|
this.$('.js-calendar-date').on('change', function(evt) {
|
||||||
this.card.getReceived(),
|
const currentUser = ReactiveCache.getCurrentUser && ReactiveCache.getCurrentUser();
|
||||||
);
|
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||||
}
|
const value = evt.target.value;
|
||||||
|
if (value) {
|
||||||
|
// Format date according to user preference
|
||||||
|
const formatted = formatDateByUserPreference(new Date(value), dateFormat, true);
|
||||||
|
self._storeDate(new Date(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_storeDate(date) {
|
_storeDate(date) {
|
||||||
|
|
@ -73,9 +81,7 @@ import {
|
||||||
|
|
||||||
onRendered() {
|
onRendered() {
|
||||||
super.onRendered();
|
super.onRendered();
|
||||||
if (moment.isDate(this.card.getStart())) {
|
// DatePicker base class handles initialization with native HTML inputs
|
||||||
this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_storeDate(date) {
|
_storeDate(date) {
|
||||||
|
|
@ -96,9 +102,7 @@ import {
|
||||||
|
|
||||||
onRendered() {
|
onRendered() {
|
||||||
super.onRendered();
|
super.onRendered();
|
||||||
if (moment.isDate(this.card.getStart())) {
|
// DatePicker base class handles initialization with native HTML inputs
|
||||||
this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_storeDate(date) {
|
_storeDate(date) {
|
||||||
|
|
@ -130,11 +134,18 @@ const CardDate = BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
showWeekOfYear() {
|
showWeekOfYear() {
|
||||||
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
|
const user = ReactiveCache.getCurrentUser();
|
||||||
|
if (!user) {
|
||||||
|
// For non-logged-in users, week of year is not shown
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return user.isShowWeekOfYear();
|
||||||
},
|
},
|
||||||
|
|
||||||
showDate() {
|
showDate() {
|
||||||
return calendar(this.date.get());
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||||
|
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
showISODate() {
|
showISODate() {
|
||||||
|
|
@ -157,21 +168,26 @@ class CardReceivedDate extends CardDate {
|
||||||
const endAt = this.data().getEnd();
|
const endAt = this.data().getEnd();
|
||||||
const startAt = this.data().getStart();
|
const startAt = this.data().getStart();
|
||||||
const theDate = this.date.get();
|
const theDate = this.date.get();
|
||||||
// if dueAt, endAt and startAt exist & are > receivedAt, receivedAt doesn't need to be flagged
|
const now = this.now.get();
|
||||||
|
|
||||||
|
// Received date logic: if received date is after start, due, or end dates, it's overdue
|
||||||
if (
|
if (
|
||||||
(startAt && theDate.isAfter(startAt)) ||
|
(startAt && isAfter(theDate, startAt)) ||
|
||||||
(endAt && theDate.isAfter(endAt)) ||
|
(endAt && isAfter(theDate, endAt)) ||
|
||||||
(dueAt && theDate.isAfter(dueAt))
|
(dueAt && isAfter(theDate, dueAt))
|
||||||
)
|
) {
|
||||||
classes += 'long-overdue';
|
classes += 'overdue';
|
||||||
else classes += 'current';
|
} else {
|
||||||
|
classes += 'not-due';
|
||||||
|
}
|
||||||
return classes;
|
return classes;
|
||||||
}
|
}
|
||||||
|
|
||||||
showTitle() {
|
showTitle() {
|
||||||
return `${TAPi18n.__('card-received-on')} ${this.date
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
.get()
|
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||||
.format('LLLL')}`;
|
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||||
|
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
events() {
|
events() {
|
||||||
|
|
@ -192,21 +208,30 @@ class CardStartDate extends CardDate {
|
||||||
}
|
}
|
||||||
|
|
||||||
classes() {
|
classes() {
|
||||||
let classes = 'start-date' + ' ';
|
let classes = 'start-date ';
|
||||||
const dueAt = this.data().getDue();
|
const dueAt = this.data().getDue();
|
||||||
const endAt = this.data().getEnd();
|
const endAt = this.data().getEnd();
|
||||||
const theDate = this.date.get();
|
const theDate = this.date.get();
|
||||||
const now = this.now.get();
|
const now = this.now.get();
|
||||||
// if dueAt or endAt exist & are > startAt, startAt doesn't need to be flagged
|
|
||||||
if ((endAt && theDate.isAfter(endAt)) || (dueAt && theDate.isAfter(dueAt)))
|
// Start date logic: if start date is after due or end dates, it's overdue
|
||||||
classes += 'long-overdue';
|
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
|
||||||
else if (theDate.isAfter(now)) classes += '';
|
classes += 'overdue';
|
||||||
else classes += 'current';
|
} else if (isAfter(theDate, now)) {
|
||||||
|
// Start date is in the future - not due yet
|
||||||
|
classes += 'not-due';
|
||||||
|
} else {
|
||||||
|
// Start date is today or in the past - current/active
|
||||||
|
classes += 'current';
|
||||||
|
}
|
||||||
return classes;
|
return classes;
|
||||||
}
|
}
|
||||||
|
|
||||||
showTitle() {
|
showTitle() {
|
||||||
return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||||
|
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||||
|
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
events() {
|
events() {
|
||||||
|
|
@ -227,22 +252,43 @@ class CardDueDate extends CardDate {
|
||||||
}
|
}
|
||||||
|
|
||||||
classes() {
|
classes() {
|
||||||
let classes = 'due-date' + ' ';
|
let classes = 'due-date ';
|
||||||
const endAt = this.data().getEnd();
|
const endAt = this.data().getEnd();
|
||||||
const theDate = this.date.get();
|
const theDate = this.date.get();
|
||||||
const now = this.now.get();
|
const now = this.now.get();
|
||||||
// if the due date is after the end date, green - done early
|
|
||||||
if (endAt && theDate.isAfter(endAt)) classes += 'current';
|
// If there's an end date and it's before the due date, task is completed early
|
||||||
// if there is an end date, don't need to flag the due date
|
if (endAt && isBefore(endAt, theDate)) {
|
||||||
else if (endAt) classes += '';
|
classes += 'completed-early';
|
||||||
else if (now.diff(theDate, 'days') >= 2) classes += 'long-overdue';
|
}
|
||||||
else if (now.diff(theDate, 'minute') >= 0) classes += 'due';
|
// If there's an end date, don't show due date status since task is completed
|
||||||
else if (now.diff(theDate, 'days') >= -1) classes += 'almost-due';
|
else if (endAt) {
|
||||||
|
classes += 'completed';
|
||||||
|
}
|
||||||
|
// Due date logic based on current time
|
||||||
|
else {
|
||||||
|
const daysDiff = diff(theDate, now, 'days');
|
||||||
|
|
||||||
|
if (daysDiff < 0) {
|
||||||
|
// Due date is in the past - overdue
|
||||||
|
classes += 'overdue';
|
||||||
|
} else if (daysDiff <= 1) {
|
||||||
|
// Due today or tomorrow - due soon
|
||||||
|
classes += 'due-soon';
|
||||||
|
} else {
|
||||||
|
// Due date is more than 1 day away - not due yet
|
||||||
|
classes += 'not-due';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return classes;
|
return classes;
|
||||||
}
|
}
|
||||||
|
|
||||||
showTitle() {
|
showTitle() {
|
||||||
return `${TAPi18n.__('card-due-on')} ${this.date.get().format('LLLL')}`;
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||||
|
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||||
|
return `${TAPi18n.__('card-due-on')} ${formattedDate}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
events() {
|
events() {
|
||||||
|
|
@ -263,17 +309,28 @@ class CardEndDate extends CardDate {
|
||||||
}
|
}
|
||||||
|
|
||||||
classes() {
|
classes() {
|
||||||
let classes = 'end-date' + ' ';
|
let classes = 'end-date ';
|
||||||
const dueAt = this.data().getDue();
|
const dueAt = this.data().getDue();
|
||||||
const theDate = this.date.get();
|
const theDate = this.date.get();
|
||||||
if (!dueAt) classes += '';
|
|
||||||
else if (theDate.isBefore(dueAt)) classes += 'current';
|
if (!dueAt) {
|
||||||
else if (theDate.isAfter(dueAt)) classes += 'due';
|
// No due date set - just show as completed
|
||||||
|
classes += 'completed';
|
||||||
|
} else if (isBefore(theDate, dueAt)) {
|
||||||
|
// End date is before due date - completed early
|
||||||
|
classes += 'completed-early';
|
||||||
|
} else if (isAfter(theDate, dueAt)) {
|
||||||
|
// End date is after due date - completed late
|
||||||
|
classes += 'completed-late';
|
||||||
|
} else {
|
||||||
|
// End date equals due date - completed on time
|
||||||
|
classes += 'completed-on-time';
|
||||||
|
}
|
||||||
return classes;
|
return classes;
|
||||||
}
|
}
|
||||||
|
|
||||||
showTitle() {
|
showTitle() {
|
||||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
|
return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
events() {
|
events() {
|
||||||
|
|
@ -302,7 +359,12 @@ class CardCustomFieldDate extends CardDate {
|
||||||
}
|
}
|
||||||
|
|
||||||
showWeekOfYear() {
|
showWeekOfYear() {
|
||||||
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
|
const user = ReactiveCache.getCurrentUser();
|
||||||
|
if (!user) {
|
||||||
|
// For non-logged-in users, week of year is not shown
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return user.isShowWeekOfYear();
|
||||||
}
|
}
|
||||||
|
|
||||||
showDate() {
|
showDate() {
|
||||||
|
|
@ -315,7 +377,10 @@ class CardCustomFieldDate extends CardDate {
|
||||||
}
|
}
|
||||||
|
|
||||||
showTitle() {
|
showTitle() {
|
||||||
return `${this.date.get().format('LLLL')}`;
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||||
|
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||||
|
return `${formattedDate}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
classes() {
|
classes() {
|
||||||
|
|
@ -334,7 +399,9 @@ CardCustomFieldDate.register('cardCustomFieldDate');
|
||||||
}
|
}
|
||||||
|
|
||||||
showDate() {
|
showDate() {
|
||||||
return format(this.date.get(), 'L');
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||||
|
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||||
}
|
}
|
||||||
}.register('minicardReceivedDate'));
|
}.register('minicardReceivedDate'));
|
||||||
|
|
||||||
|
|
@ -344,7 +411,9 @@ CardCustomFieldDate.register('cardCustomFieldDate');
|
||||||
}
|
}
|
||||||
|
|
||||||
showDate() {
|
showDate() {
|
||||||
return format(this.date.get(), 'YYYY-MM-DD HH:mm');
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||||
|
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||||
}
|
}
|
||||||
}.register('minicardStartDate'));
|
}.register('minicardStartDate'));
|
||||||
|
|
||||||
|
|
@ -354,7 +423,9 @@ CardCustomFieldDate.register('cardCustomFieldDate');
|
||||||
}
|
}
|
||||||
|
|
||||||
showDate() {
|
showDate() {
|
||||||
return format(this.date.get(), 'YYYY-MM-DD HH:mm');
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||||
|
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||||
}
|
}
|
||||||
}.register('minicardDueDate'));
|
}.register('minicardDueDate'));
|
||||||
|
|
||||||
|
|
@ -364,7 +435,9 @@ CardCustomFieldDate.register('cardCustomFieldDate');
|
||||||
}
|
}
|
||||||
|
|
||||||
showDate() {
|
showDate() {
|
||||||
return format(this.date.get(), 'YYYY-MM-DD HH:mm');
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||||
|
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||||
}
|
}
|
||||||
}.register('minicardEndDate'));
|
}.register('minicardEndDate'));
|
||||||
|
|
||||||
|
|
@ -374,7 +447,9 @@ CardCustomFieldDate.register('cardCustomFieldDate');
|
||||||
}
|
}
|
||||||
|
|
||||||
showDate() {
|
showDate() {
|
||||||
return format(this.date.get(), 'L');
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||||
|
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||||
}
|
}
|
||||||
}.register('minicardCustomFieldDate'));
|
}.register('minicardCustomFieldDate'));
|
||||||
|
|
||||||
|
|
@ -391,7 +466,9 @@ class VoteEndDate extends CardDate {
|
||||||
return classes;
|
return classes;
|
||||||
}
|
}
|
||||||
showDate() {
|
showDate() {
|
||||||
return format(this.date.get(), 'L') + ' ' + format(this.date.get(), 'HH:mm');
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||||
|
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||||
}
|
}
|
||||||
showTitle() {
|
showTitle() {
|
||||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().toLocaleString()}`;
|
return `${TAPi18n.__('card-end-on')} ${this.date.get().toLocaleString()}`;
|
||||||
|
|
@ -418,10 +495,12 @@ class PokerEndDate extends CardDate {
|
||||||
return classes;
|
return classes;
|
||||||
}
|
}
|
||||||
showDate() {
|
showDate() {
|
||||||
return this.date.get().format('l LT');
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||||
|
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||||
}
|
}
|
||||||
showTitle() {
|
showTitle() {
|
||||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
|
return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
events() {
|
events() {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,31 @@
|
||||||
|
/* Date Format Selector */
|
||||||
|
.card-details-item-date-format {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-details-item-date-format .card-details-item-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-details-item-date-format .js-date-format-selector {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-details-item-date-format .js-date-format-selector:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007cba;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.assignee {
|
.assignee {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,16 @@ template(name="cardDetails")
|
||||||
if currentBoard.hasAnyAllowsDate
|
if currentBoard.hasAnyAllowsDate
|
||||||
hr
|
hr
|
||||||
|
|
||||||
|
.card-details-item.card-details-item-date-format
|
||||||
|
h3.card-details-item-title
|
||||||
|
| 📅
|
||||||
|
| {{_ 'date-format'}}
|
||||||
|
.card-details-item-content
|
||||||
|
select.js-date-format-selector
|
||||||
|
option(value="YYYY-MM-DD" selected="{{#if isDateFormat 'YYYY-MM-DD'}}selected{{/if}}") {{_ 'date-format-yyyy-mm-dd'}}
|
||||||
|
option(value="DD-MM-YYYY" selected="{{#if isDateFormat 'DD-MM-YYYY'}}selected{{/if}}") {{_ 'date-format-dd-mm-yyyy'}}
|
||||||
|
option(value="MM-DD-YYYY" selected="{{#if isDateFormat 'MM-DD-YYYY'}}selected{{/if}}") {{_ 'date-format-mm-dd-yyyy'}}
|
||||||
|
|
||||||
if currentBoard.allowsReceivedDate
|
if currentBoard.allowsReceivedDate
|
||||||
.card-details-item.card-details-item-received
|
.card-details-item.card-details-item-received
|
||||||
h3.card-details-item-title
|
h3.card-details-item-title
|
||||||
|
|
@ -181,7 +191,7 @@ template(name="cardDetails")
|
||||||
if currentBoard.allowsMembers
|
if currentBoard.allowsMembers
|
||||||
.card-details-item.card-details-item-members
|
.card-details-item.card-details-item-members
|
||||||
h3.card-details-item-title
|
h3.card-details-item-title
|
||||||
| 👤s
|
| 👥
|
||||||
| {{_ 'members'}}
|
| {{_ 'members'}}
|
||||||
each userId in getMembers
|
each userId in getMembers
|
||||||
+userAvatar(userId=userId cardId=_id)
|
+userAvatar(userId=userId cardId=_id)
|
||||||
|
|
@ -232,7 +242,7 @@ template(name="cardDetails")
|
||||||
if currentBoard.allowsAssignedBy
|
if currentBoard.allowsAssignedBy
|
||||||
.card-details-item.card-details-item-name
|
.card-details-item.card-details-item-name
|
||||||
h3.card-details-item-title
|
h3.card-details-item-title
|
||||||
| 👤-plus
|
| ✍️
|
||||||
| {{_ 'assigned-by'}}
|
| {{_ 'assigned-by'}}
|
||||||
if canModifyCard
|
if canModifyCard
|
||||||
unless currentUser.isWorker
|
unless currentUser.isWorker
|
||||||
|
|
|
||||||
|
|
@ -306,6 +306,10 @@ BlazeComponent.extendComponent({
|
||||||
const $tooltip = this.$('.card-details-header .copied-tooltip');
|
const $tooltip = this.$('.card-details-header .copied-tooltip');
|
||||||
Utils.showCopied(promise, $tooltip);
|
Utils.showCopied(promise, $tooltip);
|
||||||
},
|
},
|
||||||
|
'change .js-date-format-selector'(event) {
|
||||||
|
const dateFormat = event.target.value;
|
||||||
|
Meteor.call('changeDateFormat', dateFormat);
|
||||||
|
},
|
||||||
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
|
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
|
||||||
'submit .js-card-description'(event) {
|
'submit .js-card-description'(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -426,56 +430,57 @@ BlazeComponent.extendComponent({
|
||||||
) {
|
) {
|
||||||
newState = forIt;
|
newState = forIt;
|
||||||
}
|
}
|
||||||
this.data().setVote(Meteor.userId(), newState);
|
// Use secure server method; direct client updates to vote are blocked
|
||||||
|
Meteor.call('cards.vote', this.data()._id, newState);
|
||||||
},
|
},
|
||||||
'click .js-poker'(e) {
|
'click .js-poker'(e) {
|
||||||
let newState = null;
|
let newState = null;
|
||||||
if ($(e.target).hasClass('js-poker-vote-one')) {
|
if ($(e.target).hasClass('js-poker-vote-one')) {
|
||||||
newState = 'one';
|
newState = 'one';
|
||||||
this.data().setPoker(Meteor.userId(), newState);
|
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||||
}
|
}
|
||||||
if ($(e.target).hasClass('js-poker-vote-two')) {
|
if ($(e.target).hasClass('js-poker-vote-two')) {
|
||||||
newState = 'two';
|
newState = 'two';
|
||||||
this.data().setPoker(Meteor.userId(), newState);
|
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||||
}
|
}
|
||||||
if ($(e.target).hasClass('js-poker-vote-three')) {
|
if ($(e.target).hasClass('js-poker-vote-three')) {
|
||||||
newState = 'three';
|
newState = 'three';
|
||||||
this.data().setPoker(Meteor.userId(), newState);
|
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||||
}
|
}
|
||||||
if ($(e.target).hasClass('js-poker-vote-five')) {
|
if ($(e.target).hasClass('js-poker-vote-five')) {
|
||||||
newState = 'five';
|
newState = 'five';
|
||||||
this.data().setPoker(Meteor.userId(), newState);
|
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||||
}
|
}
|
||||||
if ($(e.target).hasClass('js-poker-vote-eight')) {
|
if ($(e.target).hasClass('js-poker-vote-eight')) {
|
||||||
newState = 'eight';
|
newState = 'eight';
|
||||||
this.data().setPoker(Meteor.userId(), newState);
|
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||||
}
|
}
|
||||||
if ($(e.target).hasClass('js-poker-vote-thirteen')) {
|
if ($(e.target).hasClass('js-poker-vote-thirteen')) {
|
||||||
newState = 'thirteen';
|
newState = 'thirteen';
|
||||||
this.data().setPoker(Meteor.userId(), newState);
|
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||||
}
|
}
|
||||||
if ($(e.target).hasClass('js-poker-vote-twenty')) {
|
if ($(e.target).hasClass('js-poker-vote-twenty')) {
|
||||||
newState = 'twenty';
|
newState = 'twenty';
|
||||||
this.data().setPoker(Meteor.userId(), newState);
|
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||||
}
|
}
|
||||||
if ($(e.target).hasClass('js-poker-vote-forty')) {
|
if ($(e.target).hasClass('js-poker-vote-forty')) {
|
||||||
newState = 'forty';
|
newState = 'forty';
|
||||||
this.data().setPoker(Meteor.userId(), newState);
|
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||||
}
|
}
|
||||||
if ($(e.target).hasClass('js-poker-vote-one-hundred')) {
|
if ($(e.target).hasClass('js-poker-vote-one-hundred')) {
|
||||||
newState = 'oneHundred';
|
newState = 'oneHundred';
|
||||||
this.data().setPoker(Meteor.userId(), newState);
|
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||||
}
|
}
|
||||||
if ($(e.target).hasClass('js-poker-vote-unsure')) {
|
if ($(e.target).hasClass('js-poker-vote-unsure')) {
|
||||||
newState = 'unsure';
|
newState = 'unsure';
|
||||||
this.data().setPoker(Meteor.userId(), newState);
|
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'click .js-poker-finish'(e) {
|
'click .js-poker-finish'(e) {
|
||||||
if ($(e.target).hasClass('js-poker-finish')) {
|
if ($(e.target).hasClass('js-poker-finish')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const now = formatDateTime(new Date());
|
const now = new Date();
|
||||||
this.data().setPokerEnd(now);
|
Meteor.call('cards.setPokerEnd', this.data()._id, now);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -483,9 +488,9 @@ BlazeComponent.extendComponent({
|
||||||
if ($(e.target).hasClass('js-poker-replay')) {
|
if ($(e.target).hasClass('js-poker-replay')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.currentCard = this.currentData();
|
this.currentCard = this.currentData();
|
||||||
this.currentCard.replayPoker();
|
Meteor.call('cards.replayPoker', this.currentCard._id);
|
||||||
this.data().unsetPokerEnd();
|
Meteor.call('cards.unsetPokerEnd', this.currentCard._id);
|
||||||
this.data().unsetPokerEstimation();
|
Meteor.call('cards.unsetPokerEstimation', this.currentCard._id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'click .js-poker-estimation'(event) {
|
'click .js-poker-estimation'(event) {
|
||||||
|
|
@ -496,63 +501,66 @@ BlazeComponent.extendComponent({
|
||||||
this.find('#pokerEstimation').value = '';
|
this.find('#pokerEstimation').value = '';
|
||||||
|
|
||||||
if (ruleTitle) {
|
if (ruleTitle) {
|
||||||
this.data().setPokerEstimation(parseInt(ruleTitle, 10));
|
Meteor.call('cards.setPokerEstimation', this.data()._id, parseInt(ruleTitle, 10));
|
||||||
} else {
|
} else {
|
||||||
this.data().setPokerEstimation('');
|
Meteor.call('cards.unsetPokerEstimation', this.data()._id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Drag and drop file upload handlers
|
// Drag and drop file upload handlers
|
||||||
'dragover .js-card-details'(event) {
|
'dragover .js-card-details'(event) {
|
||||||
event.preventDefault();
|
// Only prevent default for file drags to avoid interfering with other drag operations
|
||||||
event.stopPropagation();
|
const dataTransfer = event.originalEvent.dataTransfer;
|
||||||
|
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'dragenter .js-card-details'(event) {
|
'dragenter .js-card-details'(event) {
|
||||||
event.preventDefault();
|
const dataTransfer = event.originalEvent.dataTransfer;
|
||||||
event.stopPropagation();
|
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||||
const card = this.data();
|
event.preventDefault();
|
||||||
const board = card.board();
|
event.stopPropagation();
|
||||||
// Only allow drag-and-drop if user can modify card and board allows attachments
|
const card = this.data();
|
||||||
if (card.canModifyCard() && board && board.allowsAttachments) {
|
const board = card.board();
|
||||||
// Check if the drag contains files
|
// Only allow drag-and-drop if user can modify card and board allows attachments
|
||||||
const dataTransfer = event.originalEvent.dataTransfer;
|
if (Utils.canModifyCard() && board && board.allowsAttachments) {
|
||||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
|
||||||
$(event.currentTarget).addClass('is-dragging-over');
|
$(event.currentTarget).addClass('is-dragging-over');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'dragleave .js-card-details'(event) {
|
'dragleave .js-card-details'(event) {
|
||||||
event.preventDefault();
|
const dataTransfer = event.originalEvent.dataTransfer;
|
||||||
event.stopPropagation();
|
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||||
$(event.currentTarget).removeClass('is-dragging-over');
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
$(event.currentTarget).removeClass('is-dragging-over');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'drop .js-card-details'(event) {
|
'drop .js-card-details'(event) {
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
$(event.currentTarget).removeClass('is-dragging-over');
|
|
||||||
|
|
||||||
const card = this.data();
|
|
||||||
const board = card.board();
|
|
||||||
|
|
||||||
// Check permissions
|
|
||||||
if (!card.canModifyCard() || !board || !board.allowsAttachments) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a file drop (not a checklist item reorder)
|
|
||||||
const dataTransfer = event.originalEvent.dataTransfer;
|
const dataTransfer = event.originalEvent.dataTransfer;
|
||||||
if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) {
|
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||||
return;
|
event.preventDefault();
|
||||||
}
|
event.stopPropagation();
|
||||||
|
$(event.currentTarget).removeClass('is-dragging-over');
|
||||||
|
|
||||||
// Check if the drop contains files (not just text/HTML)
|
const card = this.data();
|
||||||
if (!dataTransfer.types.includes('Files')) {
|
const board = card.board();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = dataTransfer.files;
|
// Check permissions
|
||||||
if (files && files.length > 0) {
|
if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
|
||||||
handleFileUpload(card, files);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a file drop (not a checklist item reorder)
|
||||||
|
if (!dataTransfer.files || dataTransfer.files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = dataTransfer.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
handleFileUpload(card, files);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -565,6 +573,11 @@ Template.cardDetails.helpers({
|
||||||
let ret = !!Utils.getPopupCardId();
|
let ret = !!Utils.getPopupCardId();
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
|
isDateFormat(format) {
|
||||||
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
if (!currentUser) return format === 'YYYY-MM-DD';
|
||||||
|
return currentUser.getDateFormat() === format;
|
||||||
|
},
|
||||||
// Upload progress helpers
|
// Upload progress helpers
|
||||||
hasActiveUploads() {
|
hasActiveUploads() {
|
||||||
return uploadProgressManager.hasActiveUploads(this._id);
|
return uploadProgressManager.hasActiveUploads(this._id);
|
||||||
|
|
@ -1093,20 +1106,15 @@ BlazeComponent.extendComponent({
|
||||||
'is-checked',
|
'is-checked',
|
||||||
);
|
);
|
||||||
const endString = this.currentCard.getVoteEnd();
|
const endString = this.currentCard.getVoteEnd();
|
||||||
|
Meteor.call('cards.setVoteQuestion', this.currentCard._id, voteQuestion, publicVote, allowNonBoardMembers);
|
||||||
this.currentCard.setVoteQuestion(
|
|
||||||
voteQuestion,
|
|
||||||
publicVote,
|
|
||||||
allowNonBoardMembers,
|
|
||||||
);
|
|
||||||
if (endString) {
|
if (endString) {
|
||||||
this.currentCard.setVoteEnd(endString);
|
Meteor.call('cards.setVoteEnd', this.currentCard._id, endString);
|
||||||
}
|
}
|
||||||
Popup.back();
|
Popup.back();
|
||||||
},
|
},
|
||||||
'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => {
|
'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.currentCard.unsetVote();
|
Meteor.call('cards.unsetVote', this.currentCard._id);
|
||||||
Popup.back();
|
Popup.back();
|
||||||
}),
|
}),
|
||||||
'click a.js-toggle-vote-public'(event) {
|
'click a.js-toggle-vote-public'(event) {
|
||||||
|
|
@ -1305,10 +1313,10 @@ BlazeComponent.extendComponent({
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
_storeDate(newDate) {
|
_storeDate(newDate) {
|
||||||
this.card.setVoteEnd(newDate);
|
Meteor.call('cards.setVoteEnd', this.card._id, newDate);
|
||||||
}
|
}
|
||||||
_deleteDate() {
|
_deleteDate() {
|
||||||
this.card.unsetVoteEnd();
|
Meteor.call('cards.unsetVoteEnd', this.card._id);
|
||||||
}
|
}
|
||||||
}.register('editVoteEndDatePopup'));
|
}.register('editVoteEndDatePopup'));
|
||||||
|
|
||||||
|
|
@ -1330,17 +1338,14 @@ BlazeComponent.extendComponent({
|
||||||
);
|
);
|
||||||
const endString = this.currentCard.getPokerEnd();
|
const endString = this.currentCard.getPokerEnd();
|
||||||
|
|
||||||
this.currentCard.setPokerQuestion(
|
Meteor.call('cards.setPokerQuestion', this.currentCard._id, pokerQuestion, allowNonBoardMembers);
|
||||||
pokerQuestion,
|
|
||||||
allowNonBoardMembers,
|
|
||||||
);
|
|
||||||
if (endString) {
|
if (endString) {
|
||||||
this.currentCard.setPokerEnd(endString);
|
Meteor.call('cards.setPokerEnd', this.currentCard._id, new Date(endString));
|
||||||
}
|
}
|
||||||
Popup.back();
|
Popup.back();
|
||||||
},
|
},
|
||||||
'click .js-remove-poker': Popup.afterConfirm('deletePoker', (event) => {
|
'click .js-remove-poker': Popup.afterConfirm('deletePoker', (event) => {
|
||||||
this.currentCard.unsetPoker();
|
Meteor.call('cards.unsetPoker', this.currentCard._id);
|
||||||
Popup.back();
|
Popup.back();
|
||||||
}),
|
}),
|
||||||
'click a.js-toggle-poker-allow-non-members'(event) {
|
'click a.js-toggle-poker-allow-non-members'(event) {
|
||||||
|
|
@ -1561,10 +1566,10 @@ BlazeComponent.extendComponent({
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
_storeDate(newDate) {
|
_storeDate(newDate) {
|
||||||
this.card.setPokerEnd(newDate);
|
Meteor.call('cards.setPokerEnd', this.card._id, newDate);
|
||||||
}
|
}
|
||||||
_deleteDate() {
|
_deleteDate() {
|
||||||
this.card.unsetPokerEnd();
|
Meteor.call('cards.unsetPokerEnd', this.card._id);
|
||||||
}
|
}
|
||||||
}.register('editPokerEndDatePopup'));
|
}.register('editPokerEndDatePopup'));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,10 @@ textarea.js-edit-checklist-item {
|
||||||
padding-top: 3px;
|
padding-top: 3px;
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
.checklist-title span.fa.checklist-handle.fa-arrows::before {
|
||||||
|
content: "↕️" !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
}
|
||||||
#card-details-overlay {
|
#card-details-overlay {
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: -600px;
|
bottom: -600px;
|
||||||
|
|
@ -148,6 +152,10 @@ textarea.js-edit-checklist-item {
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
|
.checklist-item span.fa.checklistitem-handle.fa-arrows::before {
|
||||||
|
content: "↕️" !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
}
|
||||||
.js-delete-checklist-item,
|
.js-delete-checklist-item,
|
||||||
.js-convert-checklist-item-to-card {
|
.js-convert-checklist-item-to-card {
|
||||||
margin: 0 0 0.5em 1.33em;
|
margin: 0 0 0.5em 1.33em;
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ template(name="addChecklistItemForm")
|
||||||
.edit-controls.clearfix
|
.edit-controls.clearfix
|
||||||
button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
|
button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
|
||||||
a.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
|
a.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
|
||||||
|
| ❌
|
||||||
if showNewlineBecomesNewChecklistItem
|
if showNewlineBecomesNewChecklistItem
|
||||||
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItem'}}")
|
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItem'}}")
|
||||||
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItem")
|
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItem")
|
||||||
|
|
@ -91,6 +92,7 @@ template(name="editChecklistItemForm")
|
||||||
.edit-controls.clearfix
|
.edit-controls.clearfix
|
||||||
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
|
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
|
||||||
a.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
|
a.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
|
||||||
|
| ❌
|
||||||
span(title=createdAt) {{ moment createdAt }}
|
span(title=createdAt) {{ moment createdAt }}
|
||||||
if canModifyCard
|
if canModifyCard
|
||||||
a.js-delete-checklist-item {{_ "delete"}}...
|
a.js-delete-checklist-item {{_ "delete"}}...
|
||||||
|
|
@ -125,8 +127,7 @@ template(name='checklistItemDetail')
|
||||||
if canModifyCard
|
if canModifyCard
|
||||||
.check-box-container
|
.check-box-container
|
||||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||||
if isTouchScreenOrShowDesktopDragHandles
|
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
|
||||||
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
|
|
||||||
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
|
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||||
+viewer
|
+viewer
|
||||||
= item.title
|
= item.title
|
||||||
|
|
|
||||||
|
|
@ -125,8 +125,19 @@ Template.createLabelPopup.events({
|
||||||
.$('#labelName')
|
.$('#labelName')
|
||||||
.val()
|
.val()
|
||||||
.trim();
|
.trim();
|
||||||
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
|
|
||||||
board.addLabel(name, color);
|
// Find the selected color by looking for the palette color that contains the checkmark
|
||||||
|
let selectedColor = null;
|
||||||
|
templateInstance.$('.js-palette-color').each(function() {
|
||||||
|
if ($(this).text().includes('✅')) {
|
||||||
|
selectedColor = Blaze.getData(this).color;
|
||||||
|
return false; // break out of loop
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedColor) {
|
||||||
|
board.addLabel(name, selectedColor);
|
||||||
|
}
|
||||||
Popup.back();
|
Popup.back();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -144,8 +155,19 @@ Template.editLabelPopup.events({
|
||||||
.$('#labelName')
|
.$('#labelName')
|
||||||
.val()
|
.val()
|
||||||
.trim();
|
.trim();
|
||||||
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
|
|
||||||
board.editLabel(this._id, name, color);
|
// Find the selected color by looking for the palette color that contains the checkmark
|
||||||
|
let selectedColor = null;
|
||||||
|
templateInstance.$('.js-palette-color').each(function() {
|
||||||
|
if ($(this).text().includes('✅')) {
|
||||||
|
selectedColor = Blaze.getData(this).color;
|
||||||
|
return false; // break out of loop
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedColor) {
|
||||||
|
board.editLabel(this._id, name, selectedColor);
|
||||||
|
}
|
||||||
Popup.back();
|
Popup.back();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -99,8 +99,8 @@
|
||||||
float: none;
|
float: none;
|
||||||
}
|
}
|
||||||
.minicard .minicard-labels .minicard-label {
|
.minicard .minicard-labels .minicard-label {
|
||||||
width: 1.5vw;
|
width: clamp(12px, 1.5vw, 16px);
|
||||||
height: 1.5vw;
|
height: clamp(12px, 1.5vw, 16px);
|
||||||
border-radius: 0.3vw;
|
border-radius: 0.3vw;
|
||||||
margin-right: 0.4vw;
|
margin-right: 0.4vw;
|
||||||
margin-bottom: 0.4vh;
|
margin-bottom: 0.4vh;
|
||||||
|
|
@ -130,8 +130,8 @@
|
||||||
margin-right: 0.5vw;
|
margin-right: 0.5vw;
|
||||||
}
|
}
|
||||||
.minicard .handle {
|
.minicard .handle {
|
||||||
width: 2.5vw;
|
width: clamp(20px, 2.5vw, 28px);
|
||||||
height: 2.5vw;
|
height: clamp(20px, 2.5vw, 28px);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0.7vw;
|
right: 0.7vw;
|
||||||
top: 0.7vh;
|
top: 0.7vh;
|
||||||
|
|
@ -169,6 +169,134 @@
|
||||||
margin-right: 0.4vw;
|
margin-right: 0.4vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Unicode icons for minicard dates - matching cardDate.css */
|
||||||
|
.minicard .card-date.end-date time::before {
|
||||||
|
content: "🏁"; /* Finish flag - represents end/completion */
|
||||||
|
}
|
||||||
|
.minicard .card-date.due-date time::before {
|
||||||
|
content: "⏰"; /* Alarm clock - represents due/deadline */
|
||||||
|
}
|
||||||
|
.minicard .card-date.start-date time::before {
|
||||||
|
content: "🚀"; /* Rocket - represents start/launch */
|
||||||
|
}
|
||||||
|
.minicard .card-date.received-date time::before {
|
||||||
|
content: "📥"; /* Inbox tray - represents received/incoming */
|
||||||
|
}
|
||||||
|
|
||||||
|
.minicard .card-date time::before {
|
||||||
|
font-size: inherit;
|
||||||
|
margin-right: 0.3em;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Date type specific colors for minicards - matching cardDate.css */
|
||||||
|
.minicard .card-date.received-date {
|
||||||
|
background-color: #dbdbdb; /* Grey for received - same as base card-date */
|
||||||
|
}
|
||||||
|
|
||||||
|
.minicard .card-date.received-date:hover,
|
||||||
|
.minicard .card-date.received-date.is-active {
|
||||||
|
background-color: #b3b3b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minicard .card-date.start-date {
|
||||||
|
background-color: #90ee90; /* Light green for start */
|
||||||
|
color: #000; /* Black text for start */
|
||||||
|
}
|
||||||
|
|
||||||
|
.minicard .card-date.start-date:hover,
|
||||||
|
.minicard .card-date.start-date.is-active {
|
||||||
|
background-color: #7dd87d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minicard .card-date.due-date {
|
||||||
|
background-color: #ffd700; /* Yellow for due */
|
||||||
|
color: #000; /* Black text for due */
|
||||||
|
}
|
||||||
|
|
||||||
|
.minicard .card-date.due-date:hover,
|
||||||
|
.minicard .card-date.due-date.is-active {
|
||||||
|
background-color: #e6c200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minicard .card-date.end-date {
|
||||||
|
background-color: #ffb3b3; /* Light red for end */
|
||||||
|
color: #000; /* Black text for end */
|
||||||
|
}
|
||||||
|
|
||||||
|
.minicard .card-date.end-date:hover,
|
||||||
|
.minicard .card-date.end-date.is-active {
|
||||||
|
background-color: #ff9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Date status colors for minicards - matching cardDate.css */
|
||||||
|
.minicard .card-date.overdue {
|
||||||
|
background-color: #ff4444 !important; /* Red for overdue */
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
.minicard .card-date.overdue:hover,
|
||||||
|
.minicard .card-date.overdue.is-active {
|
||||||
|
background-color: #cc3333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minicard .card-date.due-soon {
|
||||||
|
background-color: #ffaa00 !important; /* Amber for due soon */
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
.minicard .card-date.due-soon:hover,
|
||||||
|
.minicard .card-date.due-soon.is-active {
|
||||||
|
background-color: #e69900 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minicard .card-date.not-due {
|
||||||
|
/* No special background - uses default date type colors */
|
||||||
|
}
|
||||||
|
|
||||||
|
.minicard .card-date.current {
|
||||||
|
background-color: #5ba639 !important; /* Green for current/active */
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
.minicard .card-date.current:hover,
|
||||||
|
.minicard .card-date.current.is-active {
|
||||||
|
background-color: #46802c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minicard .card-date.completed {
|
||||||
|
background-color: #90ee90 !important; /* Light green for completed */
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
.minicard .card-date.completed:hover,
|
||||||
|
.minicard .card-date.completed.is-active {
|
||||||
|
background-color: #7dd87d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minicard .card-date.completed-early {
|
||||||
|
background-color: #4caf50 !important; /* Green for completed early */
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
.minicard .card-date.completed-early:hover,
|
||||||
|
.minicard .card-date.completed-early.is-active {
|
||||||
|
background-color: #45a049 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minicard .card-date.completed-late {
|
||||||
|
background-color: #ff9800 !important; /* Orange for completed late */
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
.minicard .card-date.completed-late:hover,
|
||||||
|
.minicard .card-date.completed-late.is-active {
|
||||||
|
background-color: #f57c00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minicard .card-date.completed-on-time {
|
||||||
|
background-color: #2196f3 !important; /* Blue for completed on time */
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
.minicard .card-date.completed-on-time:hover,
|
||||||
|
.minicard .card-date.completed-on-time.is-active {
|
||||||
|
background-color: #1976d2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Font Awesome icons in minicard dates */
|
/* Font Awesome icons in minicard dates */
|
||||||
.minicard .card-date i.fa {
|
.minicard .card-date i.fa {
|
||||||
margin-right: 0.3vw;
|
margin-right: 0.3vw;
|
||||||
|
|
@ -234,8 +362,8 @@
|
||||||
.minicard .minicard-creator .member {
|
.minicard .minicard-creator .member {
|
||||||
float: right;
|
float: right;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
height: 3.5vw;
|
height: clamp(24px, 3.5vw, 32px);
|
||||||
width: 3.5vw;
|
width: clamp(24px, 3.5vw, 32px);
|
||||||
margin-bottom: 0.5vh;
|
margin-bottom: 0.5vh;
|
||||||
}
|
}
|
||||||
.minicard .minicard-members .assignee,
|
.minicard .minicard-members .assignee,
|
||||||
|
|
@ -243,8 +371,8 @@
|
||||||
.minicard .minicard-creator .assignee {
|
.minicard .minicard-creator .assignee {
|
||||||
float: right;
|
float: right;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
height: 3.5vw;
|
height: clamp(24px, 3.5vw, 32px);
|
||||||
width: 3.5vw;
|
width: clamp(24px, 3.5vw, 32px);
|
||||||
}
|
}
|
||||||
.minicard .minicard-members + .badges,
|
.minicard .minicard-members + .badges,
|
||||||
.minicard .minicard-assignees + .badges,
|
.minicard .minicard-assignees + .badges,
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,14 @@ template(name="minicard")
|
||||||
class="{{#if isLinkedBoard}}linked-board{{/if}}"
|
class="{{#if isLinkedBoard}}linked-board{{/if}}"
|
||||||
class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}")
|
class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}")
|
||||||
if canModifyCard
|
if canModifyCard
|
||||||
if isTouchScreenOrShowDesktopDragHandles
|
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") ☰
|
||||||
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") | ☰
|
if canMoveCard
|
||||||
.handle
|
.handle
|
||||||
| ↔️
|
| ↕️
|
||||||
else
|
|
||||||
a.minicard-details-menu.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") | ☰
|
|
||||||
.dates
|
.dates
|
||||||
if getReceived
|
if getReceived
|
||||||
unless getStart
|
.date
|
||||||
unless getDue
|
+minicardReceivedDate
|
||||||
unless getEnd
|
|
||||||
.date
|
|
||||||
+minicardReceivedDate
|
|
||||||
if getStart
|
if getStart
|
||||||
.date
|
.date
|
||||||
+minicardStartDate
|
+minicardStartDate
|
||||||
|
|
@ -147,7 +142,7 @@ template(name="minicard")
|
||||||
if canModifyCard
|
if canModifyCard
|
||||||
if comments.length
|
if comments.length
|
||||||
.badge(title="{{_ 'card-comments-title' comments.length }}")
|
.badge(title="{{_ 'card-comments-title' comments.length }}")
|
||||||
span.badge-icon.badge-comment.badge-text | 💬
|
span.badge-icon.badge-comment.badge-text 💬
|
||||||
= ' '
|
= ' '
|
||||||
= comments.length
|
= comments.length
|
||||||
//span.badge-comment.badge-text
|
//span.badge-comment.badge-text
|
||||||
|
|
@ -155,36 +150,36 @@ template(name="minicard")
|
||||||
if getDescription
|
if getDescription
|
||||||
unless currentBoard.allowsDescriptionTextOnMinicard
|
unless currentBoard.allowsDescriptionTextOnMinicard
|
||||||
.badge.badge-state-image-only(title=getDescription)
|
.badge.badge-state-image-only(title=getDescription)
|
||||||
span.badge-icon | 📝
|
span.badge-icon 📝
|
||||||
if getVoteQuestion
|
if getVoteQuestion
|
||||||
.badge.badge-state-image-only(title=getVoteQuestion)
|
.badge.badge-state-image-only(title=getVoteQuestion)
|
||||||
span.badge-icon(class="{{#if voteState}}text-green{{/if}}") | 👍
|
span.badge-icon(class="{{#if voteState}}text-green{{/if}}") 👍
|
||||||
span.badge-text {{ voteCountPositive }}
|
span.badge-text {{ voteCountPositive }}
|
||||||
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}") | 👎
|
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}") 👎
|
||||||
span.badge-text {{ voteCountNegative }}
|
span.badge-text {{ voteCountNegative }}
|
||||||
if getPokerQuestion
|
if getPokerQuestion
|
||||||
.badge.badge-state-image-only(title=getPokerQuestion)
|
.badge.badge-state-image-only(title=getPokerQuestion)
|
||||||
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}") | ✅
|
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}") ✅
|
||||||
if expiredPoker
|
if expiredPoker
|
||||||
span.badge-text {{ getPokerEstimation }}
|
span.badge-text {{ getPokerEstimation }}
|
||||||
if attachments.length
|
if attachments.length
|
||||||
if currentBoard.allowsBadgeAttachmentOnMinicard
|
if currentBoard.allowsBadgeAttachmentOnMinicard
|
||||||
.badge
|
.badge
|
||||||
span.badge-icon | 📎
|
span.badge-icon 📎
|
||||||
span.badge-text= attachments.length
|
span.badge-text= attachments.length
|
||||||
if checklists.length
|
if checklists.length
|
||||||
.badge(class="{{#if checklistFinished}}is-finished{{/if}}")
|
.badge(class="{{#if checklistFinished}}is-finished{{/if}}")
|
||||||
span.badge-icon | ☑️
|
span.badge-icon ☑️
|
||||||
span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
|
span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
|
||||||
if allSubtasks.count
|
if allSubtasks.count
|
||||||
.badge
|
.badge
|
||||||
span.badge-icon | 🌐
|
span.badge-icon 🌐
|
||||||
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
|
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
|
||||||
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
|
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
|
||||||
if currentBoard.allowsCardSortingByNumber
|
if currentBoard.allowsCardSortingByNumber
|
||||||
if currentBoard.allowsCardSortingByNumberOnMinicard
|
if currentBoard.allowsCardSortingByNumberOnMinicard
|
||||||
.badge
|
.badge
|
||||||
span.badge-icon | 🔢
|
span.badge-icon 🔢
|
||||||
span.badge-text.check-list-sort {{ sort }}
|
span.badge-text.check-list-sort {{ sort }}
|
||||||
if currentBoard.allowsDescriptionTextOnMinicard
|
if currentBoard.allowsDescriptionTextOnMinicard
|
||||||
if getDescription
|
if getDescription
|
||||||
|
|
|
||||||
|
|
@ -111,55 +111,58 @@ BlazeComponent.extendComponent({
|
||||||
'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
|
'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
|
||||||
// Drag and drop file upload handlers
|
// Drag and drop file upload handlers
|
||||||
'dragover .minicard'(event) {
|
'dragover .minicard'(event) {
|
||||||
event.preventDefault();
|
// Only prevent default for file drags to avoid interfering with sortable
|
||||||
event.stopPropagation();
|
const dataTransfer = event.originalEvent.dataTransfer;
|
||||||
|
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'dragenter .minicard'(event) {
|
'dragenter .minicard'(event) {
|
||||||
event.preventDefault();
|
const dataTransfer = event.originalEvent.dataTransfer;
|
||||||
event.stopPropagation();
|
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||||
const card = this.data();
|
event.preventDefault();
|
||||||
const board = card.board();
|
event.stopPropagation();
|
||||||
// Only allow drag-and-drop if user can modify card and board allows attachments
|
const card = this.data();
|
||||||
if (card.canModifyCard() && board && board.allowsAttachments) {
|
const board = card.board();
|
||||||
// Check if the drag contains files
|
// Only allow drag-and-drop if user can modify card and board allows attachments
|
||||||
const dataTransfer = event.originalEvent.dataTransfer;
|
if (Utils.canModifyCard() && board && board.allowsAttachments) {
|
||||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
|
||||||
$(event.currentTarget).addClass('is-dragging-over');
|
$(event.currentTarget).addClass('is-dragging-over');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'dragleave .minicard'(event) {
|
'dragleave .minicard'(event) {
|
||||||
event.preventDefault();
|
const dataTransfer = event.originalEvent.dataTransfer;
|
||||||
event.stopPropagation();
|
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||||
$(event.currentTarget).removeClass('is-dragging-over');
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
$(event.currentTarget).removeClass('is-dragging-over');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'drop .minicard'(event) {
|
'drop .minicard'(event) {
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
$(event.currentTarget).removeClass('is-dragging-over');
|
|
||||||
|
|
||||||
const card = this.data();
|
|
||||||
const board = card.board();
|
|
||||||
|
|
||||||
// Check permissions
|
|
||||||
if (!card.canModifyCard() || !board || !board.allowsAttachments) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a file drop (not a card reorder)
|
|
||||||
const dataTransfer = event.originalEvent.dataTransfer;
|
const dataTransfer = event.originalEvent.dataTransfer;
|
||||||
if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) {
|
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||||
return;
|
event.preventDefault();
|
||||||
}
|
event.stopPropagation();
|
||||||
|
$(event.currentTarget).removeClass('is-dragging-over');
|
||||||
|
|
||||||
// Check if the drop contains files (not just text/HTML)
|
const card = this.data();
|
||||||
if (!dataTransfer.types.includes('Files')) {
|
const board = card.board();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = dataTransfer.files;
|
// Check permissions
|
||||||
if (files && files.length > 0) {
|
if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
|
||||||
handleFileUpload(card, files);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a file drop (not a card reorder)
|
||||||
|
if (!dataTransfer.files || dataTransfer.files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = dataTransfer.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
handleFileUpload(card, files);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,17 +56,17 @@ template(name="importMapMembersAddPopup")
|
||||||
p
|
p
|
||||||
| {{_ 'import-user-select'}}
|
| {{_ 'import-user-select'}}
|
||||||
.js-map-member
|
.js-map-member
|
||||||
+EasySearch.Input(index=searchIndex)
|
input.js-search-member-input(type="text" placeholder="{{_ 'search-users'}}")
|
||||||
ul.pop-over-list
|
ul.pop-over-list
|
||||||
+EasySearch.Each(index=searchIndex)
|
each searchResults
|
||||||
li.item.js-member-item
|
li.item.js-member-item
|
||||||
a.name.js-select-import(title="{{profile.fullname}} ({{username}})" data-id="{{__originalId}}")
|
a.name.js-select-import(title="{{profile.fullname}} ({{username}})" data-id="{{_id}}")
|
||||||
+userAvatar(userId=__originalId)
|
+userAvatar(userId=_id)
|
||||||
span.full-name
|
span.full-name
|
||||||
= profile.fullname
|
= profile.fullname
|
||||||
| (<span class="username">{{username}}</span>)
|
| (<span class="username">{{username}}</span>)
|
||||||
+EasySearch.IfSearching(index=searchIndex)
|
if searching.get
|
||||||
+spinner
|
+spinner
|
||||||
+EasySearch.IfNoResults(index=searchIndex)
|
if noResults.get
|
||||||
.manage-member-section
|
.manage-member-section
|
||||||
p.quiet {{_ 'no-results'}}
|
p.quiet {{_ 'no-results'}}
|
||||||
|
|
|
||||||
|
|
@ -311,6 +311,73 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
}).register('importMapMembersAddPopup');
|
}).register('importMapMembersAddPopup');
|
||||||
|
|
||||||
|
// Global reactive variables for import member popup
|
||||||
|
const importMemberPopupState = {
|
||||||
|
searching: new ReactiveVar(false),
|
||||||
|
searchResults: new ReactiveVar([]),
|
||||||
|
noResults: new ReactiveVar(false),
|
||||||
|
searchTimeout: null
|
||||||
|
};
|
||||||
|
|
||||||
|
BlazeComponent.extendComponent({
|
||||||
|
onCreated() {
|
||||||
|
// Use global state
|
||||||
|
this.searching = importMemberPopupState.searching;
|
||||||
|
this.searchResults = importMemberPopupState.searchResults;
|
||||||
|
this.noResults = importMemberPopupState.noResults;
|
||||||
|
this.searchTimeout = importMemberPopupState.searchTimeout;
|
||||||
|
},
|
||||||
|
|
||||||
|
onRendered() {
|
||||||
|
this.find('.js-search-member-input').focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
performSearch(query) {
|
||||||
|
if (!query || query.length < 2) {
|
||||||
|
this.searchResults.set([]);
|
||||||
|
this.noResults.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searching.set(true);
|
||||||
|
this.noResults.set(false);
|
||||||
|
|
||||||
|
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
|
||||||
|
this.searchResults.set(results);
|
||||||
|
this.searching.set(false);
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
this.noResults.set(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
events() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'keyup .js-search-member-input'(event) {
|
||||||
|
const query = event.target.value.trim();
|
||||||
|
|
||||||
|
if (this.searchTimeout) {
|
||||||
|
clearTimeout(this.searchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchTimeout = setTimeout(() => {
|
||||||
|
this.performSearch(query);
|
||||||
|
}, 300);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
}).register('importMapMembersAddPopupSearch');
|
||||||
|
|
||||||
Template.importMapMembersAddPopup.helpers({
|
Template.importMapMembersAddPopup.helpers({
|
||||||
searchIndex: () => UserSearchIndex,
|
searchResults() {
|
||||||
|
return importMemberPopupState.searchResults.get();
|
||||||
|
},
|
||||||
|
searching() {
|
||||||
|
return importMemberPopupState.searching;
|
||||||
|
},
|
||||||
|
noResults() {
|
||||||
|
return importMemberPopupState.noResults;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,11 @@ body.list-resizing-active * {
|
||||||
margin-right: 0 !important;
|
margin-right: 0 !important;
|
||||||
/* Ensure proper display */
|
/* Ensure proper display */
|
||||||
display: inline-block !important;
|
display: inline-block !important;
|
||||||
|
/* Ensure it's clickable and shows proper cursor */
|
||||||
|
cursor: move !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
/* Add some padding for better clickability */
|
||||||
|
padding: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure buttons maintain original positioning */
|
/* Ensure buttons maintain original positioning */
|
||||||
|
|
@ -373,9 +378,6 @@ body.list-resizing-active * {
|
||||||
position: relative;
|
position: relative;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
|
||||||
.list-header .list-rotated {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.list-header .list-header-watch-icon {
|
.list-header .list-header-watch-icon {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
|
|
@ -639,17 +641,22 @@ body.list-resizing-active * {
|
||||||
.mini-list.mobile-view {
|
.mini-list.mobile-view {
|
||||||
flex: 0 0 60px;
|
flex: 0 0 60px;
|
||||||
height: auto;
|
height: auto;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
min-width: 100%;
|
max-width: 100vw;
|
||||||
border-left: 0px;
|
min-width: 100vw;
|
||||||
|
border-left: 0px !important;
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
|
display: block !important;
|
||||||
}
|
}
|
||||||
.list.mobile-view {
|
.list.mobile-view {
|
||||||
display: contents;
|
display: block !important;
|
||||||
flex-basis: auto;
|
flex-basis: auto;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
min-width: 100%;
|
max-width: 100vw;
|
||||||
border-left: 0px;
|
min-width: 100vw;
|
||||||
|
border-left: 0px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
.list.mobile-view:first-child {
|
.list.mobile-view:first-child {
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
|
|
@ -657,9 +664,11 @@ body.list-resizing-active * {
|
||||||
.list.mobile-view.ui-sortable-helper {
|
.list.mobile-view.ui-sortable-helper {
|
||||||
flex: 0 0 60px;
|
flex: 0 0 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
border-left: 0px;
|
max-width: 100vw;
|
||||||
|
border-left: 0px !important;
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
|
display: block !important;
|
||||||
}
|
}
|
||||||
.list.mobile-view.ui-sortable-helper .list-header.ui-sortable-handle {
|
.list.mobile-view.ui-sortable-helper .list-header.ui-sortable-handle {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
|
|
@ -667,14 +676,17 @@ body.list-resizing-active * {
|
||||||
.list.mobile-view.placeholder {
|
.list.mobile-view.placeholder {
|
||||||
flex: 0 0 60px;
|
flex: 0 0 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
border-left: 0px;
|
max-width: 100vw;
|
||||||
|
border-left: 0px !important;
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
|
display: block !important;
|
||||||
}
|
}
|
||||||
.list.mobile-view .list-body {
|
.list.mobile-view .list-body {
|
||||||
padding: 15px 19px;
|
padding: 15px 19px;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
min-width: 100%;
|
max-width: 100vw;
|
||||||
|
min-width: 100vw;
|
||||||
}
|
}
|
||||||
.list.mobile-view .list-header {
|
.list.mobile-view .list-header {
|
||||||
/*Updated padding values for mobile devices, this should fix text grouping issue*/
|
/*Updated padding values for mobile devices, this should fix text grouping issue*/
|
||||||
|
|
@ -683,8 +695,9 @@ body.list-resizing-active * {
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
min-width: 100%;
|
max-width: 100vw;
|
||||||
|
min-width: 100vw;
|
||||||
/* Force grid layout for iPhone */
|
/* Force grid layout for iPhone */
|
||||||
display: grid !important;
|
display: grid !important;
|
||||||
grid-template-columns: 30px 1fr auto auto !important;
|
grid-template-columns: 30px 1fr auto auto !important;
|
||||||
|
|
@ -760,21 +773,27 @@ body.list-resizing-active * {
|
||||||
align-items: initial;
|
align-items: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px),
|
||||||
|
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||||
.mini-list {
|
.mini-list {
|
||||||
flex: 0 0 60px;
|
flex: 0 0 60px;
|
||||||
height: auto;
|
height: auto;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
min-width: 100%;
|
max-width: 100vw;
|
||||||
border-left: 0px;
|
min-width: 100vw;
|
||||||
|
border-left: 0px !important;
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
|
display: block !important;
|
||||||
}
|
}
|
||||||
.list {
|
.list {
|
||||||
display: contents;
|
display: block !important;
|
||||||
flex-basis: auto;
|
flex-basis: auto;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
min-width: 100%;
|
max-width: 100vw;
|
||||||
border-left: 0px;
|
min-width: 100vw;
|
||||||
|
border-left: 0px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
.list:first-child {
|
.list:first-child {
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
|
|
@ -782,9 +801,11 @@ body.list-resizing-active * {
|
||||||
.list.ui-sortable-helper {
|
.list.ui-sortable-helper {
|
||||||
flex: 0 0 60px;
|
flex: 0 0 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
border-left: 0px;
|
max-width: 100vw;
|
||||||
|
border-left: 0px !important;
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
|
display: block !important;
|
||||||
}
|
}
|
||||||
.list.ui-sortable-helper .list-header.ui-sortable-handle {
|
.list.ui-sortable-helper .list-header.ui-sortable-handle {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
|
|
@ -792,14 +813,17 @@ body.list-resizing-active * {
|
||||||
.list.placeholder {
|
.list.placeholder {
|
||||||
flex: 0 0 60px;
|
flex: 0 0 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
border-left: 0px;
|
max-width: 100vw;
|
||||||
|
border-left: 0px !important;
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
|
display: block !important;
|
||||||
}
|
}
|
||||||
.list-body {
|
.list-body {
|
||||||
padding: 15px 19px;
|
padding: 15px 19px;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
min-width: 100%;
|
max-width: 100vw;
|
||||||
|
min-width: 100vw;
|
||||||
}
|
}
|
||||||
.list-header {
|
.list-header {
|
||||||
/*Updated padding values for mobile devices, this should fix text grouping issue*/
|
/*Updated padding values for mobile devices, this should fix text grouping issue*/
|
||||||
|
|
@ -808,8 +832,9 @@ body.list-resizing-active * {
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
min-width: 100%;
|
max-width: 100vw;
|
||||||
|
min-width: 100vw;
|
||||||
}
|
}
|
||||||
.list-header .list-header-left-icon {
|
.list-header .list-header-left-icon {
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
|
|
|
||||||
|
|
@ -150,17 +150,13 @@ BlazeComponent.extendComponent({
|
||||||
});
|
});
|
||||||
|
|
||||||
this.autorun(() => {
|
this.autorun(() => {
|
||||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
|
||||||
$cards.sortable({
|
|
||||||
handle: '.handle',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$cards.sortable({
|
|
||||||
handle: '.minicard',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($cards.data('uiSortable') || $cards.data('sortable')) {
|
if ($cards.data('uiSortable') || $cards.data('sortable')) {
|
||||||
|
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||||
|
$cards.sortable('option', 'handle', '.handle');
|
||||||
|
} else {
|
||||||
|
$cards.sortable('option', 'handle', '.minicard');
|
||||||
|
}
|
||||||
|
|
||||||
$cards.sortable(
|
$cards.sortable(
|
||||||
'option',
|
'option',
|
||||||
'disabled',
|
'disabled',
|
||||||
|
|
@ -201,20 +197,60 @@ BlazeComponent.extendComponent({
|
||||||
listWidth() {
|
listWidth() {
|
||||||
const user = ReactiveCache.getCurrentUser();
|
const user = ReactiveCache.getCurrentUser();
|
||||||
const list = Template.currentData();
|
const list = Template.currentData();
|
||||||
if (!user || !list) return 270; // Return default width if user or list is not available
|
if (!list) return 270; // Return default width if list is not available
|
||||||
return user.getListWidthFromStorage(list.boardId, list._id);
|
|
||||||
|
if (user) {
|
||||||
|
// For logged-in users, get from user profile
|
||||||
|
return user.getListWidthFromStorage(list.boardId, list._id);
|
||||||
|
} else {
|
||||||
|
// For non-logged-in users, get from localStorage
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('wekan-list-widths');
|
||||||
|
if (stored) {
|
||||||
|
const widths = JSON.parse(stored);
|
||||||
|
if (widths[list.boardId] && widths[list.boardId][list._id]) {
|
||||||
|
return widths[list.boardId][list._id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error reading list width from localStorage:', e);
|
||||||
|
}
|
||||||
|
return 270; // Return default width if not found
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
listConstraint() {
|
listConstraint() {
|
||||||
const user = ReactiveCache.getCurrentUser();
|
const user = ReactiveCache.getCurrentUser();
|
||||||
const list = Template.currentData();
|
const list = Template.currentData();
|
||||||
if (!user || !list) return 550; // Return default constraint if user or list is not available
|
if (!list) return 550; // Return default constraint if list is not available
|
||||||
return user.getListConstraintFromStorage(list.boardId, list._id);
|
|
||||||
|
if (user) {
|
||||||
|
// For logged-in users, get from user profile
|
||||||
|
return user.getListConstraintFromStorage(list.boardId, list._id);
|
||||||
|
} else {
|
||||||
|
// For non-logged-in users, get from localStorage
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('wekan-list-constraints');
|
||||||
|
if (stored) {
|
||||||
|
const constraints = JSON.parse(stored);
|
||||||
|
if (constraints[list.boardId] && constraints[list.boardId][list._id]) {
|
||||||
|
return constraints[list.boardId][list._id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error reading list constraint from localStorage:', e);
|
||||||
|
}
|
||||||
|
return 550; // Return default constraint if not found
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
autoWidth() {
|
autoWidth() {
|
||||||
const user = ReactiveCache.getCurrentUser();
|
const user = ReactiveCache.getCurrentUser();
|
||||||
const list = Template.currentData();
|
const list = Template.currentData();
|
||||||
|
if (!user) {
|
||||||
|
// For non-logged-in users, auto-width is disabled
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return user.isAutoWidth(list.boardId);
|
return user.isAutoWidth(list.boardId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -333,14 +369,49 @@ BlazeComponent.extendComponent({
|
||||||
// Use the new storage method that handles both logged-in and non-logged-in users
|
// Use the new storage method that handles both logged-in and non-logged-in users
|
||||||
if (process.env.DEBUG === 'true') {
|
if (process.env.DEBUG === 'true') {
|
||||||
}
|
}
|
||||||
Meteor.call('applyListWidthToStorage', boardId, listId, finalWidth, listConstraint, (error, result) => {
|
|
||||||
if (error) {
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
console.error('Error saving list width:', error);
|
if (currentUser) {
|
||||||
} else {
|
// For logged-in users, use server method
|
||||||
|
Meteor.call('applyListWidthToStorage', boardId, listId, finalWidth, listConstraint, (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('Error saving list width:', error);
|
||||||
|
} else {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// For non-logged-in users, save to localStorage directly
|
||||||
|
try {
|
||||||
|
// Save list width
|
||||||
|
const storedWidths = localStorage.getItem('wekan-list-widths');
|
||||||
|
let widths = storedWidths ? JSON.parse(storedWidths) : {};
|
||||||
|
|
||||||
|
if (!widths[boardId]) {
|
||||||
|
widths[boardId] = {};
|
||||||
|
}
|
||||||
|
widths[boardId][listId] = finalWidth;
|
||||||
|
|
||||||
|
localStorage.setItem('wekan-list-widths', JSON.stringify(widths));
|
||||||
|
|
||||||
|
// Save list constraint
|
||||||
|
const storedConstraints = localStorage.getItem('wekan-list-constraints');
|
||||||
|
let constraints = storedConstraints ? JSON.parse(storedConstraints) : {};
|
||||||
|
|
||||||
|
if (!constraints[boardId]) {
|
||||||
|
constraints[boardId] = {};
|
||||||
|
}
|
||||||
|
constraints[boardId][listId] = listConstraint;
|
||||||
|
|
||||||
|
localStorage.setItem('wekan-list-constraints', JSON.stringify(constraints));
|
||||||
|
|
||||||
if (process.env.DEBUG === 'true') {
|
if (process.env.DEBUG === 'true') {
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error saving list width/constraint to localStorage:', e);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -51,11 +51,12 @@ template(name="listHeader")
|
||||||
div.list-header-menu
|
div.list-header-menu
|
||||||
unless currentUser.isCommentOnly
|
unless currentUser.isCommentOnly
|
||||||
if canSeeAddCard
|
if canSeeAddCard
|
||||||
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") | ➕
|
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") ➕
|
||||||
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") | ☰
|
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
|
||||||
else
|
else
|
||||||
a.list-header-menu-icon.js-select-list | ▶️
|
a.list-header-menu-icon.js-select-list ▶️
|
||||||
a.list-header-handle.handle.js-list-handle | ↔️
|
unless currentUser.isWorker
|
||||||
|
a.list-header-handle.handle.js-list-handle ↕️
|
||||||
else if currentUser.isBoardMember
|
else if currentUser.isBoardMember
|
||||||
if isWatching
|
if isWatching
|
||||||
i.list-header-watch-icon | 👁️
|
i.list-header-watch-icon | 👁️
|
||||||
|
|
@ -65,14 +66,15 @@ template(name="listHeader")
|
||||||
//if isBoardAdmin
|
//if isBoardAdmin
|
||||||
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
|
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
|
||||||
if canSeeAddCard
|
if canSeeAddCard
|
||||||
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") | ➕
|
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") ➕
|
||||||
a.js-collapse(title="{{_ 'collapse'}}")
|
a.js-collapse(title="{{_ 'collapse'}}")
|
||||||
| ⬅️
|
| ⬅️
|
||||||
| ➡️
|
| ➡️
|
||||||
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") | ☰
|
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
|
||||||
if currentUser.isBoardAdmin
|
if currentUser.isBoardMember
|
||||||
if isTouchScreenOrShowDesktopDragHandles
|
unless currentUser.isCommentOnly
|
||||||
a.list-header-handle.handle.js-list-handle | ↔️
|
unless currentUser.isWorker
|
||||||
|
a.list-header-handle.handle.js-list-handle ↕️
|
||||||
|
|
||||||
template(name="editListTitleForm")
|
template(name="editListTitleForm")
|
||||||
.list-composer
|
.list-composer
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,16 @@ template(name="dueCards")
|
||||||
span.global-search-error-messages
|
span.global-search-error-messages
|
||||||
= msg
|
= msg
|
||||||
else
|
else
|
||||||
+resultsPaged(this)
|
.due-cards-results-header
|
||||||
|
h1
|
||||||
|
= resultsText
|
||||||
|
each card in dueCardsList
|
||||||
|
+resultCard(card)
|
||||||
|
else
|
||||||
|
.global-search-results-list-wrapper
|
||||||
|
.no-results
|
||||||
|
h3 {{_ 'dueCards-noResults-title'}}
|
||||||
|
p {{_ 'dueCards-noResults-description'}}
|
||||||
|
|
||||||
template(name="dueCardsViewChangePopup")
|
template(name="dueCardsViewChangePopup")
|
||||||
if currentUser
|
if currentUser
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,6 @@
|
||||||
import { ReactiveCache } from '/imports/reactiveCache';
|
import { ReactiveCache } from '/imports/reactiveCache';
|
||||||
import { CardSearchPagedComponent } from '../../lib/cardSearch';
|
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
|
||||||
import {
|
import { TAPi18n } from '/imports/i18n';
|
||||||
OPERATOR_HAS,
|
|
||||||
OPERATOR_SORT,
|
|
||||||
OPERATOR_USER,
|
|
||||||
ORDER_ASCENDING,
|
|
||||||
PREDICATE_DUE_AT,
|
|
||||||
} from '../../../config/search-const';
|
|
||||||
import { QueryParams } from '../../../config/query-classes';
|
|
||||||
|
|
||||||
// const subManager = new SubsManager();
|
// const subManager = new SubsManager();
|
||||||
|
|
||||||
|
|
@ -15,7 +8,7 @@ BlazeComponent.extendComponent({
|
||||||
dueCardsView() {
|
dueCardsView() {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
// console.log('sort:', Utils.dueCardsView());
|
// console.log('sort:', Utils.dueCardsView());
|
||||||
return Utils.dueCardsView();
|
return Utils && Utils.dueCardsView ? Utils.dueCardsView() : 'me';
|
||||||
},
|
},
|
||||||
|
|
||||||
events() {
|
events() {
|
||||||
|
|
@ -31,6 +24,47 @@ Template.dueCards.helpers({
|
||||||
userId() {
|
userId() {
|
||||||
return Meteor.userId();
|
return Meteor.userId();
|
||||||
},
|
},
|
||||||
|
dueCardsList() {
|
||||||
|
const component = BlazeComponent.getComponentForElement(this.firstNode);
|
||||||
|
if (component && component.dueCardsList) {
|
||||||
|
return component.dueCardsList();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
hasResults() {
|
||||||
|
const component = BlazeComponent.getComponentForElement(this.firstNode);
|
||||||
|
if (component && component.hasResults) {
|
||||||
|
return component.hasResults.get();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
searching() {
|
||||||
|
const component = BlazeComponent.getComponentForElement(this.firstNode);
|
||||||
|
if (component && component.isLoading) {
|
||||||
|
return component.isLoading.get();
|
||||||
|
}
|
||||||
|
return true; // Show loading by default
|
||||||
|
},
|
||||||
|
hasQueryErrors() {
|
||||||
|
return false; // No longer using search, so always false
|
||||||
|
},
|
||||||
|
errorMessages() {
|
||||||
|
return []; // No longer using search, so always empty
|
||||||
|
},
|
||||||
|
cardsCount() {
|
||||||
|
const component = BlazeComponent.getComponentForElement(this.firstNode);
|
||||||
|
if (component && component.cardsCount) {
|
||||||
|
return component.cardsCount();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
resultsText() {
|
||||||
|
const component = BlazeComponent.getComponentForElement(this.firstNode);
|
||||||
|
if (component && component.resultsText) {
|
||||||
|
return component.resultsText();
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
BlazeComponent.extendComponent({
|
BlazeComponent.extendComponent({
|
||||||
|
|
@ -38,12 +72,16 @@ BlazeComponent.extendComponent({
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'click .js-due-cards-view-me'() {
|
'click .js-due-cards-view-me'() {
|
||||||
Utils.setDueCardsView('me');
|
if (Utils && Utils.setDueCardsView) {
|
||||||
|
Utils.setDueCardsView('me');
|
||||||
|
}
|
||||||
Popup.back();
|
Popup.back();
|
||||||
},
|
},
|
||||||
|
|
||||||
'click .js-due-cards-view-all'() {
|
'click .js-due-cards-view-all'() {
|
||||||
Utils.setDueCardsView('all');
|
if (Utils && Utils.setDueCardsView) {
|
||||||
|
Utils.setDueCardsView('all');
|
||||||
|
}
|
||||||
Popup.back();
|
Popup.back();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -51,61 +89,162 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
}).register('dueCardsViewChangePopup');
|
}).register('dueCardsViewChangePopup');
|
||||||
|
|
||||||
class DueCardsComponent extends CardSearchPagedComponent {
|
class DueCardsComponent extends BlazeComponent {
|
||||||
onCreated() {
|
onCreated() {
|
||||||
super.onCreated();
|
super.onCreated();
|
||||||
|
|
||||||
const queryParams = new QueryParams();
|
this._cachedCards = null;
|
||||||
queryParams.addPredicate(OPERATOR_HAS, {
|
this._cachedTimestamp = null;
|
||||||
field: PREDICATE_DUE_AT,
|
this.subscriptionHandle = null;
|
||||||
exists: true,
|
this.isLoading = new ReactiveVar(true);
|
||||||
});
|
this.hasResults = new ReactiveVar(false);
|
||||||
// queryParams[OPERATOR_LIMIT] = 5;
|
this.searching = new ReactiveVar(false);
|
||||||
queryParams.addPredicate(OPERATOR_SORT, {
|
|
||||||
name: PREDICATE_DUE_AT,
|
// Subscribe to the optimized due cards publication
|
||||||
order: ORDER_ASCENDING,
|
this.autorun(() => {
|
||||||
|
const allUsers = this.dueCardsView() === 'all';
|
||||||
|
if (this.subscriptionHandle) {
|
||||||
|
this.subscriptionHandle.stop();
|
||||||
|
}
|
||||||
|
this.subscriptionHandle = Meteor.subscribe('dueCards', allUsers);
|
||||||
|
|
||||||
|
// Update loading state based on subscription
|
||||||
|
this.autorun(() => {
|
||||||
|
if (this.subscriptionHandle && this.subscriptionHandle.ready()) {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('dueCards: subscription ready, loading data...');
|
||||||
|
}
|
||||||
|
this.isLoading.set(false);
|
||||||
|
const cards = this.dueCardsList();
|
||||||
|
this.hasResults.set(cards && cards.length > 0);
|
||||||
|
} else {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('dueCards: subscription not ready, showing loading...');
|
||||||
|
}
|
||||||
|
this.isLoading.set(true);
|
||||||
|
this.hasResults.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (Utils.dueCardsView() !== 'all') {
|
onDestroyed() {
|
||||||
queryParams.addPredicate(OPERATOR_USER, ReactiveCache.getCurrentUser().username);
|
super.onDestroyed();
|
||||||
|
if (this.subscriptionHandle) {
|
||||||
|
this.subscriptionHandle.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.runGlobalSearch(queryParams);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dueCardsView() {
|
dueCardsView() {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
//console.log('sort:', Utils.dueCardsView());
|
//console.log('sort:', Utils.dueCardsView());
|
||||||
return Utils.dueCardsView();
|
return Utils && Utils.dueCardsView ? Utils.dueCardsView() : 'me';
|
||||||
}
|
}
|
||||||
|
|
||||||
sortByBoard() {
|
sortByBoard() {
|
||||||
return this.dueCardsView() === 'board';
|
return this.dueCardsView() === 'board';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasResults() {
|
||||||
|
return this.hasResults.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
cardsCount() {
|
||||||
|
const cards = this.dueCardsList();
|
||||||
|
return cards ? cards.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsText() {
|
||||||
|
const count = this.cardsCount();
|
||||||
|
if (count === 1) {
|
||||||
|
return TAPi18n.__('one-card-found');
|
||||||
|
} else {
|
||||||
|
// Get the translated text and manually replace %s with the count
|
||||||
|
const baseText = TAPi18n.__('n-cards-found');
|
||||||
|
const result = baseText.replace('%s', count);
|
||||||
|
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('dueCards: base text:', baseText, 'count:', count, 'result:', result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dueCardsList() {
|
dueCardsList() {
|
||||||
const results = this.getResults();
|
// Check if subscription is ready
|
||||||
console.log('results:', results);
|
if (!this.subscriptionHandle || !this.subscriptionHandle.ready()) {
|
||||||
const cards = [];
|
if (process.env.DEBUG === 'true') {
|
||||||
if (results) {
|
console.log('dueCards client: subscription not ready');
|
||||||
results.forEach(card => {
|
}
|
||||||
cards.push(card);
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cached results if available to avoid expensive re-sorting
|
||||||
|
if (this._cachedCards && this._cachedTimestamp && (Date.now() - this._cachedTimestamp < 5000)) {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('dueCards client: using cached results,', this._cachedCards.length, 'cards');
|
||||||
|
}
|
||||||
|
return this._cachedCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cards directly from the subscription (already sorted by the publication)
|
||||||
|
const cards = ReactiveCache.getCards({
|
||||||
|
type: 'cardType-card',
|
||||||
|
archived: false,
|
||||||
|
dueAt: { $exists: true, $nin: [null, ''] }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('dueCards client: found', cards.length, 'cards with due dates');
|
||||||
|
console.log('dueCards client: cards details:', cards.map(c => ({
|
||||||
|
id: c._id,
|
||||||
|
title: c.title,
|
||||||
|
dueAt: c.dueAt,
|
||||||
|
boardId: c.boardId,
|
||||||
|
members: c.members,
|
||||||
|
assignees: c.assignees,
|
||||||
|
userId: c.userId
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter cards based on user view preference
|
||||||
|
const allUsers = this.dueCardsView() === 'all';
|
||||||
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
let filteredCards = cards;
|
||||||
|
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('dueCards client: current user:', currentUser ? currentUser._id : 'none');
|
||||||
|
console.log('dueCards client: showing all users:', allUsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allUsers && currentUser) {
|
||||||
|
filteredCards = cards.filter(card => {
|
||||||
|
const isMember = card.members && card.members.includes(currentUser._id);
|
||||||
|
const isAssignee = card.assignees && card.assignees.includes(currentUser._id);
|
||||||
|
const isAuthor = card.userId === currentUser._id;
|
||||||
|
const matches = isMember || isAssignee || isAuthor;
|
||||||
|
|
||||||
|
if (process.env.DEBUG === 'true' && matches) {
|
||||||
|
console.log('dueCards client: card matches user:', card.title, { isMember, isAssignee, isAuthor });
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cards.sort((a, b) => {
|
if (process.env.DEBUG === 'true') {
|
||||||
const x = a.dueAt === null ? new Date('2100-12-31') : a.dueAt;
|
console.log('dueCards client: filtered to', filteredCards.length, 'cards');
|
||||||
const y = b.dueAt === null ? new Date('2100-12-31') : b.dueAt;
|
}
|
||||||
|
|
||||||
if (x > y) return 1;
|
// Cache the results for 5 seconds to avoid re-filtering on every render
|
||||||
else if (x < y) return -1;
|
this._cachedCards = filteredCards;
|
||||||
|
this._cachedTimestamp = Date.now();
|
||||||
|
|
||||||
return 0;
|
// Update reactive variables
|
||||||
});
|
this.hasResults.set(filteredCards && filteredCards.length > 0);
|
||||||
|
this.isLoading.set(false);
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
return filteredCards;
|
||||||
console.log('cards:', cards);
|
|
||||||
return cards;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,8 +100,9 @@
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
padding: 10px 0px;
|
padding: 10px 0px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap; /* Allow wrapping on mobile */
|
flex-wrap: nowrap; /* Prevent wrapping to keep single row */
|
||||||
min-height: 28px; /* Allow height to grow */
|
min-height: 28px;
|
||||||
|
overflow: hidden; /* Prevent content from overflowing */
|
||||||
}
|
}
|
||||||
#header-quick-access .home-icon {
|
#header-quick-access .home-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -167,13 +168,39 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin: -10px;
|
margin: -10px;
|
||||||
|
flex: 1; /* Take up available space */
|
||||||
|
min-width: 0; /* Allow shrinking below content size */
|
||||||
|
display: flex; /* Use flexbox for better control */
|
||||||
|
align-items: center;
|
||||||
|
scrollbar-width: thin; /* Firefox */
|
||||||
|
scrollbar-color: rgba(255, 255, 255, 0.3) transparent; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Webkit scrollbar styling for better UX */
|
||||||
|
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
#header-quick-access ul.header-quick-access-list li {
|
#header-quick-access ul.header-quick-access-list li {
|
||||||
display: inline;
|
display: inline-block; /* Keep inline-block for proper spacing */
|
||||||
width: auto;
|
width: auto;
|
||||||
color: #d9d9d9;
|
color: #d9d9d9;
|
||||||
padding: 12px 0px;
|
padding: 12px 0px;
|
||||||
margin: -10px 0px;
|
margin: -10px 0px;
|
||||||
|
flex-shrink: 0; /* Prevent items from shrinking */
|
||||||
|
white-space: nowrap; /* Prevent text wrapping within items */
|
||||||
}
|
}
|
||||||
#header-quick-access ul.header-quick-access-list li a {
|
#header-quick-access ul.header-quick-access-list li a {
|
||||||
padding: 12px 10px;
|
padding: 12px 10px;
|
||||||
|
|
@ -220,6 +247,7 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#header-quick-access #header-user-bar .header-user-bar-name,
|
#header-quick-access #header-user-bar .header-user-bar-name,
|
||||||
#header-quick-access #header-help {
|
#header-quick-access #header-help {
|
||||||
margin: 4px 8px 0 0;
|
margin: 4px 8px 0 0;
|
||||||
|
|
@ -314,7 +342,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make zoom input wider on all mobile screens */
|
/* Make zoom input wider on all mobile screens */
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px),
|
||||||
|
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||||
#header-quick-access .zoom-controls .zoom-input {
|
#header-quick-access .zoom-controls .zoom-input {
|
||||||
min-width: 50px !important; /* Wider on mobile */
|
min-width: 50px !important; /* Wider on mobile */
|
||||||
width: 50px !important; /* Fixed width to show all numbers */
|
width: 50px !important; /* Fixed width to show all numbers */
|
||||||
|
|
@ -424,7 +453,8 @@
|
||||||
margin: 6px 5px 0;
|
margin: 6px 5px 0;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px),
|
||||||
|
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||||
#header #header-main-bar {
|
#header #header-main-bar {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
@ -446,6 +476,8 @@
|
||||||
transition: background-color 0.4s;
|
transition: background-color 0.4s;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
|
flex-wrap: nowrap !important; /* Force single row on mobile */
|
||||||
|
overflow: hidden; /* Prevent content overflow */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile home icon styling */
|
/* Mobile home icon styling */
|
||||||
|
|
@ -489,11 +521,12 @@
|
||||||
screen and (max-width: 800px) and (orientation: portrait),
|
screen and (max-width: 800px) and (orientation: portrait),
|
||||||
screen and (max-width: 800px) and (orientation: landscape) {
|
screen and (max-width: 800px) and (orientation: landscape) {
|
||||||
#header-quick-access {
|
#header-quick-access {
|
||||||
height: auto !important; /* Allow height to grow */
|
height: 48px !important; /* Fixed height for mobile */
|
||||||
min-height: 48px !important; /* Minimum height for mobile */
|
min-height: 48px !important; /* Minimum height for mobile */
|
||||||
flex-wrap: wrap !important; /* Force wrapping */
|
flex-wrap: nowrap !important; /* Force single row */
|
||||||
align-items: flex-start !important; /* Align to top when wrapping */
|
align-items: center !important; /* Center align items */
|
||||||
padding: 8px 0px !important; /* Adjust padding for mobile */
|
padding: 8px 0px !important; /* Adjust padding for mobile */
|
||||||
|
overflow: hidden !important; /* Prevent content overflow */
|
||||||
}
|
}
|
||||||
#header-quick-access {
|
#header-quick-access {
|
||||||
font-size: 2em !important; /* 2x bigger base font size */
|
font-size: 2em !important; /* 2x bigger base font size */
|
||||||
|
|
|
||||||
|
|
@ -83,10 +83,6 @@ template(name="header")
|
||||||
i.mobile-icon(class="{{#if mobileMode}}active{{/if}}") 📱
|
i.mobile-icon(class="{{#if mobileMode}}active{{/if}}") 📱
|
||||||
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}") 🖥️
|
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}") 🖥️
|
||||||
|
|
||||||
// Bookmarks button - desktop opens popup, mobile routes to page
|
|
||||||
a.board-header-btn.js-open-bookmarks(title="{{_ 'bookmarks'}}")
|
|
||||||
| 🔖
|
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
+notifications
|
+notifications
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -470,8 +470,10 @@ a:not(.disabled).is-active i.fa {
|
||||||
MOBILE & TABLET RESPONSIVE IMPROVEMENTS
|
MOBILE & TABLET RESPONSIVE IMPROVEMENTS
|
||||||
======================================== */
|
======================================== */
|
||||||
|
|
||||||
/* Mobile devices (up to 800px) */
|
/* Mobile devices (up to 800px) and all iPhone models */
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px),
|
||||||
|
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) and (orientation: landscape),
|
||||||
|
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) and (orientation: portrait) {
|
||||||
#content {
|
#content {
|
||||||
margin: 1px 0px 0px 0px;
|
margin: 1px 0px 0px 0px;
|
||||||
height: calc(100% - 0px);
|
height: calc(100% - 0px);
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,18 @@ Template.userFormsLayout.onRendered(() => {
|
||||||
if (loginInput && loginInput.name && (loginInput.name.toLowerCase().includes('user') || loginInput.name.toLowerCase().includes('email'))) {
|
if (loginInput && loginInput.name && (loginInput.name.toLowerCase().includes('user') || loginInput.name.toLowerCase().includes('email'))) {
|
||||||
loginInput.setAttribute('autocomplete', 'username email');
|
loginInput.setAttribute('autocomplete', 'username email');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add autocomplete attributes to password fields for WCAG compliance
|
||||||
|
const passwordInputs = document.querySelectorAll('input[type="password"]');
|
||||||
|
passwordInputs.forEach(input => {
|
||||||
|
if (input.name && input.name.includes('password')) {
|
||||||
|
if (input.name.includes('password_again') || input.name.includes('new_password')) {
|
||||||
|
input.setAttribute('autocomplete', 'new-password');
|
||||||
|
} else {
|
||||||
|
input.setAttribute('autocomplete', 'current-password');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,29 @@
|
||||||
max-height: inherit;
|
max-height: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Admin edit popups: use full height */
|
||||||
|
.pop-over[data-popup="editUser"],
|
||||||
|
.pop-over[data-popup="editOrg"],
|
||||||
|
.pop-over[data-popup="editTeam"] {
|
||||||
|
height: calc(100vh - 20px) !important;
|
||||||
|
max-height: calc(100vh - 20px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pop-over[data-popup="editUser"] .content-wrapper,
|
||||||
|
.pop-over[data-popup="editOrg"] .content-wrapper,
|
||||||
|
.pop-over[data-popup="editTeam"] .content-wrapper {
|
||||||
|
max-height: calc(100vh - 80px) !important; /* Subtract header height */
|
||||||
|
height: calc(100vh - 80px) !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pop-over[data-popup="editUser"] .content-container,
|
||||||
|
.pop-over[data-popup="editOrg"] .content-container,
|
||||||
|
.pop-over[data-popup="editTeam"] .content-container {
|
||||||
|
max-height: calc(100vh - 80px) !important; /* Subtract header height */
|
||||||
|
height: calc(100vh - 80px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Ensure language popup list can scroll properly */
|
/* Ensure language popup list can scroll properly */
|
||||||
.pop-over .pop-over-list {
|
.pop-over .pop-over-list {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
|
|
@ -270,6 +293,8 @@
|
||||||
overflow-y: auto !important;
|
overflow-y: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date button,
|
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date button,
|
||||||
.pop-over[data-popup="editCardStartDatePopup"] .edit-date button,
|
.pop-over[data-popup="editCardStartDatePopup"] .edit-date button,
|
||||||
.pop-over[data-popup="editCardDueDatePopup"] .edit-date button,
|
.pop-over[data-popup="editCardDueDatePopup"] .edit-date button,
|
||||||
|
|
@ -364,9 +389,6 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
.pop-over .quiet {
|
|
||||||
/* padding: 6px 6px 4px;*/
|
|
||||||
}
|
|
||||||
.pop-over.search-over {
|
.pop-over.search-over {
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
min-height: 14vh;
|
min-height: 14vh;
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,33 @@
|
||||||
/* Migration Progress Styles */
|
/* Migration Progress Styles */
|
||||||
.migration-overlay {
|
.migration-progress-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
right: 0;
|
||||||
height: 100%;
|
bottom: 0;
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
z-index: 10000;
|
z-index: 9999;
|
||||||
display: none;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow-y: auto;
|
backdrop-filter: blur(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.migration-overlay.active {
|
.migration-progress-modal {
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.migration-modal {
|
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||||
max-width: 800px;
|
max-width: 500px;
|
||||||
width: 95%;
|
width: 90%;
|
||||||
max-height: 90vh;
|
max-height: 80vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
animation: slideInScale 0.4s ease-out;
|
animation: migrationModalSlideIn 0.3s ease-out;
|
||||||
margin: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideInScale {
|
@keyframes migrationModalSlideIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-30px) scale(0.95);
|
transform: translateY(-20px) scale(0.95);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
@ -40,333 +35,235 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.migration-header {
|
.migration-progress-header {
|
||||||
padding: 24px 32px 20px;
|
|
||||||
border-bottom: 2px solid #e0e0e0;
|
|
||||||
text-align: center;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: white;
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.migration-header h3 {
|
.migration-progress-title {
|
||||||
margin: 0 0 8px 0;
|
margin: 0;
|
||||||
font-size: 24px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.migration-header h3 i {
|
.migration-progress-close {
|
||||||
margin-right: 12px;
|
cursor: pointer;
|
||||||
color: #FFD700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.migration-header p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
opacity: 0.9;
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.migration-content {
|
.migration-progress-close:hover {
|
||||||
padding: 24px 32px;
|
opacity: 1;
|
||||||
max-height: 60vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.migration-overview {
|
.migration-progress-content {
|
||||||
margin-bottom: 32px;
|
padding: 30px;
|
||||||
padding: 20px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 8px;
|
|
||||||
border-left: 4px solid #667eea;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.overall-progress {
|
.migration-progress-overall {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.migration-progress-overall-label {
|
||||||
width: 100%;
|
font-weight: 600;
|
||||||
height: 12px;
|
color: #333;
|
||||||
background-color: #e0e0e0;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
position: relative;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-fill {
|
.migration-progress-overall-bar {
|
||||||
|
background: #e9ecef;
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-progress-overall-fill {
|
||||||
|
background: linear-gradient(90deg, #28a745, #20c997);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
border-radius: 10px;
|
||||||
border-radius: 6px;
|
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-fill::after {
|
.migration-progress-overall-fill::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: linear-gradient(
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||||
90deg,
|
animation: migrationProgressShimmer 2s infinite;
|
||||||
transparent,
|
|
||||||
rgba(255, 255, 255, 0.4),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
animation: shimmer 2s infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes migrationProgressShimmer {
|
||||||
0% {
|
0% { transform: translateX(-100%); }
|
||||||
transform: translateX(-100%);
|
100% { transform: translateX(100%); }
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-text {
|
.migration-progress-overall-percentage {
|
||||||
text-align: center;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #667eea;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-label {
|
|
||||||
text-align: center;
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-step {
|
|
||||||
text-align: center;
|
|
||||||
color: #333;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-step i {
|
|
||||||
margin-right: 8px;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.estimated-time {
|
|
||||||
text-align: center;
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
background-color: #fff3cd;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid #ffeaa7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.estimated-time i {
|
|
||||||
margin-right: 6px;
|
|
||||||
color: #f39c12;
|
|
||||||
}
|
|
||||||
|
|
||||||
.migration-steps {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.migration-steps h4 {
|
|
||||||
margin: 0 0 16px 0;
|
|
||||||
color: #333;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.steps-list {
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.migration-step {
|
|
||||||
padding: 16px 20px;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.migration-step:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.migration-step.completed {
|
|
||||||
background-color: #d4edda;
|
|
||||||
border-left: 4px solid #28a745;
|
|
||||||
}
|
|
||||||
|
|
||||||
.migration-step.current {
|
|
||||||
background-color: #cce7ff;
|
|
||||||
border-left: 4px solid #667eea;
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4);
|
|
||||||
}
|
|
||||||
70% {
|
|
||||||
box-shadow: 0 0 0 10px rgba(102, 126, 234, 0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-icon {
|
|
||||||
margin-right: 12px;
|
|
||||||
font-size: 18px;
|
|
||||||
width: 24px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-icon i.fa-check-circle {
|
|
||||||
color: #28a745;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-icon i.fa-cog.fa-spin {
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-icon i.fa-circle-o {
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-info {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-name {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-description {
|
|
||||||
color: #666;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-progress {
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
min-width: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-progress .progress-text {
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-progress-bar {
|
.migration-progress-current-step {
|
||||||
width: 100%;
|
margin-bottom: 25px;
|
||||||
height: 4px;
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
border-radius: 2px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-progress-bar .progress-fill {
|
.migration-progress-step-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-progress-step-bar {
|
||||||
|
background: #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-progress-step-fill {
|
||||||
|
background: linear-gradient(90deg, #007bff, #0056b3);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
border-radius: 8px;
|
||||||
border-radius: 2px;
|
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.migration-status {
|
.migration-progress-step-percentage {
|
||||||
text-align: center;
|
text-align: right;
|
||||||
color: #333;
|
font-size: 12px;
|
||||||
font-size: 16px;
|
color: #666;
|
||||||
background-color: #e3f2fd;
|
font-weight: 600;
|
||||||
padding: 12px 16px;
|
}
|
||||||
|
|
||||||
|
.migration-progress-status {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8f9fa;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid #bbdefb;
|
border-left: 4px solid #007bff;
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.migration-status i {
|
.migration-progress-status-label {
|
||||||
margin-right: 8px;
|
font-weight: 600;
|
||||||
color: #2196f3;
|
color: #333;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.migration-footer {
|
.migration-progress-status-text {
|
||||||
padding: 16px 32px 24px;
|
color: #555;
|
||||||
border-top: 1px solid #e0e0e0;
|
font-size: 14px;
|
||||||
background-color: #f8f9fa;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.migration-info {
|
.migration-progress-details {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #e3f2fd;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-progress-details-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1976d2;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-progress-details-text {
|
||||||
|
color: #1565c0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-progress-footer {
|
||||||
|
padding: 20px 30px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-progress-note {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #666;
|
color: #666;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.4;
|
font-style: italic;
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.migration-info i {
|
|
||||||
margin-right: 6px;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.migration-warning {
|
|
||||||
text-align: center;
|
|
||||||
color: #856404;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.3;
|
|
||||||
background-color: #fff3cd;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid #ffeaa7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.migration-warning i {
|
|
||||||
margin-right: 6px;
|
|
||||||
color: #f39c12;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive design */
|
/* Responsive design */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 600px) {
|
||||||
.migration-modal {
|
.migration-progress-modal {
|
||||||
width: 98%;
|
width: 95%;
|
||||||
margin: 10px;
|
margin: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.migration-header,
|
.migration-progress-content {
|
||||||
.migration-content,
|
padding: 20px;
|
||||||
.migration-footer {
|
|
||||||
padding-left: 16px;
|
|
||||||
padding-right: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.migration-header h3 {
|
.migration-progress-header {
|
||||||
font-size: 20px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-header {
|
.migration-progress-title {
|
||||||
flex-direction: column;
|
font-size: 16px;
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-progress {
|
|
||||||
text-align: left;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.steps-list {
|
|
||||||
max-height: 200px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark mode support */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.migration-progress-modal {
|
||||||
|
background: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-progress-overall-label,
|
||||||
|
.migration-progress-step-label,
|
||||||
|
.migration-progress-status-label {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-progress-status {
|
||||||
|
background: #4a5568;
|
||||||
|
border-left-color: #63b3ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-progress-status-text {
|
||||||
|
color: #cbd5e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-progress-details {
|
||||||
|
background: #2b6cb0;
|
||||||
|
border-left-color: #4299e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-progress-details-label {
|
||||||
|
color: #bee3f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-progress-details-text {
|
||||||
|
color: #90cdf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-progress-footer {
|
||||||
|
background: #4a5568;
|
||||||
|
border-top-color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-progress-note {
|
||||||
|
color: #a0aec0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,63 +1,43 @@
|
||||||
template(name="migrationProgress")
|
template(name="migrationProgress")
|
||||||
.migration-overlay(class="{{#if isMigrating}}active{{/if}}")
|
if isMigrating
|
||||||
.migration-modal
|
.migration-progress-overlay
|
||||||
.migration-header
|
.migration-progress-modal
|
||||||
h3
|
.migration-progress-header
|
||||||
| 🗄️
|
h3.migration-progress-title
|
||||||
| {{_ 'database-migration'}}
|
| 🔄 Board Migration in Progress
|
||||||
p {{_ 'database-migration-description'}}
|
.migration-progress-close.js-close-migration-progress
|
||||||
|
| ❌
|
||||||
.migration-content
|
|
||||||
.migration-overview
|
|
||||||
.overall-progress
|
|
||||||
.progress-bar
|
|
||||||
.progress-fill(style="width: {{migrationProgress}}%")
|
|
||||||
.progress-text {{migrationProgress}}%
|
|
||||||
.progress-label {{_ 'overall-progress'}}
|
|
||||||
|
|
||||||
.current-step
|
|
||||||
| ⚙️
|
|
||||||
| {{migrationCurrentStep}}
|
|
||||||
|
|
||||||
.estimated-time(style="{{#unless migrationEstimatedTime}}display: none;{{/unless}}")
|
|
||||||
| ⏰
|
|
||||||
| {{_ 'estimated-time-remaining'}}: {{migrationEstimatedTime}}
|
|
||||||
|
|
||||||
.migration-steps
|
.migration-progress-content
|
||||||
h4 {{_ 'migration-steps'}}
|
.migration-progress-overall
|
||||||
.steps-list
|
.migration-progress-overall-label
|
||||||
each migrationSteps
|
| Overall Progress: {{currentStep}} of {{totalSteps}} steps
|
||||||
.migration-step(class="{{#if completed}}completed{{/if}}" class="{{#if isCurrentStep}}current{{/if}}")
|
.migration-progress-overall-bar
|
||||||
.step-header
|
.migration-progress-overall-fill(style="{{progressBarStyle}}")
|
||||||
.step-icon
|
.migration-progress-overall-percentage
|
||||||
if completed
|
| {{overallProgress}}%
|
||||||
| ✅
|
|
||||||
else if isCurrentStep
|
.migration-progress-current-step
|
||||||
| ⚙️
|
.migration-progress-step-label
|
||||||
else
|
| Current Step: {{stepNameFormatted}}
|
||||||
| ⭕
|
.migration-progress-step-bar
|
||||||
.step-info
|
.migration-progress-step-fill(style="{{stepProgressBarStyle}}")
|
||||||
.step-name {{name}}
|
.migration-progress-step-percentage
|
||||||
.step-description {{description}}
|
| {{stepProgress}}%
|
||||||
.step-progress
|
|
||||||
if completed
|
.migration-progress-status
|
||||||
.progress-text 100%
|
.migration-progress-status-label
|
||||||
else if isCurrentStep
|
| Status:
|
||||||
.progress-text {{progress}}%
|
.migration-progress-status-text
|
||||||
else
|
| {{stepStatus}}
|
||||||
.progress-text 0%
|
|
||||||
if isCurrentStep
|
if stepDetailsFormatted
|
||||||
.step-progress-bar
|
.migration-progress-details
|
||||||
.progress-fill(style="width: {{progress}}%")
|
.migration-progress-details-label
|
||||||
|
| Details:
|
||||||
|
.migration-progress-details-text
|
||||||
|
| {{stepDetailsFormatted}}
|
||||||
|
|
||||||
.migration-status
|
.migration-progress-footer
|
||||||
| ℹ️
|
.migration-progress-note
|
||||||
| {{migrationStatus}}
|
| Please wait while we migrate your board to the latest structure...
|
||||||
|
|
||||||
.migration-footer
|
|
||||||
.migration-info
|
|
||||||
| 💡
|
|
||||||
| {{_ 'migration-info-text'}}
|
|
||||||
.migration-warning
|
|
||||||
| ⚠️
|
|
||||||
| {{_ 'migration-warning-text'}}
|
|
||||||
|
|
@ -1,54 +1,212 @@
|
||||||
import { Template } from 'meteor/templating';
|
/**
|
||||||
import {
|
* Migration Progress Component
|
||||||
migrationManager,
|
* Displays detailed progress for comprehensive board migration
|
||||||
isMigrating,
|
*/
|
||||||
migrationProgress,
|
|
||||||
migrationStatus,
|
|
||||||
migrationCurrentStep,
|
|
||||||
migrationEstimatedTime,
|
|
||||||
migrationSteps
|
|
||||||
} from '/client/lib/migrationManager';
|
|
||||||
|
|
||||||
|
import { ReactiveVar } from 'meteor/reactive-var';
|
||||||
|
import { ReactiveCache } from '/imports/reactiveCache';
|
||||||
|
|
||||||
|
// Reactive variables for migration progress
|
||||||
|
export const migrationProgress = new ReactiveVar(0);
|
||||||
|
export const migrationStatus = new ReactiveVar('');
|
||||||
|
export const migrationStepName = new ReactiveVar('');
|
||||||
|
export const migrationStepProgress = new ReactiveVar(0);
|
||||||
|
export const migrationStepStatus = new ReactiveVar('');
|
||||||
|
export const migrationStepDetails = new ReactiveVar(null);
|
||||||
|
export const migrationCurrentStep = new ReactiveVar(0);
|
||||||
|
export const migrationTotalSteps = new ReactiveVar(0);
|
||||||
|
export const isMigrating = new ReactiveVar(false);
|
||||||
|
|
||||||
|
class MigrationProgressManager {
|
||||||
|
constructor() {
|
||||||
|
this.progressHistory = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update migration progress
|
||||||
|
*/
|
||||||
|
updateProgress(progressData) {
|
||||||
|
const {
|
||||||
|
overallProgress,
|
||||||
|
currentStep,
|
||||||
|
totalSteps,
|
||||||
|
stepName,
|
||||||
|
stepProgress,
|
||||||
|
stepStatus,
|
||||||
|
stepDetails,
|
||||||
|
boardId
|
||||||
|
} = progressData;
|
||||||
|
|
||||||
|
// Update reactive variables
|
||||||
|
migrationProgress.set(overallProgress);
|
||||||
|
migrationCurrentStep.set(currentStep);
|
||||||
|
migrationTotalSteps.set(totalSteps);
|
||||||
|
migrationStepName.set(stepName);
|
||||||
|
migrationStepProgress.set(stepProgress);
|
||||||
|
migrationStepStatus.set(stepStatus);
|
||||||
|
migrationStepDetails.set(stepDetails);
|
||||||
|
|
||||||
|
// Store in history
|
||||||
|
this.progressHistory.push({
|
||||||
|
timestamp: new Date(),
|
||||||
|
...progressData
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update overall status
|
||||||
|
migrationStatus.set(`${stepName}: ${stepStatus}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start migration
|
||||||
|
*/
|
||||||
|
startMigration() {
|
||||||
|
isMigrating.set(true);
|
||||||
|
migrationProgress.set(0);
|
||||||
|
migrationStatus.set('Starting migration...');
|
||||||
|
migrationStepName.set('');
|
||||||
|
migrationStepProgress.set(0);
|
||||||
|
migrationStepStatus.set('');
|
||||||
|
migrationStepDetails.set(null);
|
||||||
|
migrationCurrentStep.set(0);
|
||||||
|
migrationTotalSteps.set(0);
|
||||||
|
this.progressHistory = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete migration
|
||||||
|
*/
|
||||||
|
completeMigration() {
|
||||||
|
isMigrating.set(false);
|
||||||
|
migrationProgress.set(100);
|
||||||
|
migrationStatus.set('Migration completed successfully!');
|
||||||
|
|
||||||
|
// Clear step details after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
migrationStepName.set('');
|
||||||
|
migrationStepProgress.set(0);
|
||||||
|
migrationStepStatus.set('');
|
||||||
|
migrationStepDetails.set(null);
|
||||||
|
migrationCurrentStep.set(0);
|
||||||
|
migrationTotalSteps.set(0);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fail migration
|
||||||
|
*/
|
||||||
|
failMigration(error) {
|
||||||
|
isMigrating.set(false);
|
||||||
|
migrationStatus.set(`Migration failed: ${error.message || error}`);
|
||||||
|
migrationStepStatus.set('Error occurred');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get progress history
|
||||||
|
*/
|
||||||
|
getProgressHistory() {
|
||||||
|
return this.progressHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear progress
|
||||||
|
*/
|
||||||
|
clearProgress() {
|
||||||
|
isMigrating.set(false);
|
||||||
|
migrationProgress.set(0);
|
||||||
|
migrationStatus.set('');
|
||||||
|
migrationStepName.set('');
|
||||||
|
migrationStepProgress.set(0);
|
||||||
|
migrationStepStatus.set('');
|
||||||
|
migrationStepDetails.set(null);
|
||||||
|
migrationCurrentStep.set(0);
|
||||||
|
migrationTotalSteps.set(0);
|
||||||
|
this.progressHistory = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const migrationProgressManager = new MigrationProgressManager();
|
||||||
|
|
||||||
|
// Template helpers
|
||||||
Template.migrationProgress.helpers({
|
Template.migrationProgress.helpers({
|
||||||
isMigrating() {
|
isMigrating() {
|
||||||
return isMigrating.get();
|
return isMigrating.get();
|
||||||
},
|
},
|
||||||
|
|
||||||
migrationProgress() {
|
overallProgress() {
|
||||||
return migrationProgress.get();
|
return migrationProgress.get();
|
||||||
},
|
},
|
||||||
|
|
||||||
migrationStatus() {
|
overallStatus() {
|
||||||
return migrationStatus.get();
|
return migrationStatus.get();
|
||||||
},
|
},
|
||||||
|
|
||||||
migrationCurrentStep() {
|
currentStep() {
|
||||||
return migrationCurrentStep.get();
|
return migrationCurrentStep.get();
|
||||||
},
|
},
|
||||||
|
|
||||||
migrationEstimatedTime() {
|
totalSteps() {
|
||||||
return migrationEstimatedTime.get();
|
return migrationTotalSteps.get();
|
||||||
},
|
},
|
||||||
|
|
||||||
migrationSteps() {
|
stepName() {
|
||||||
const steps = migrationSteps.get();
|
return migrationStepName.get();
|
||||||
const currentStep = migrationCurrentStep.get();
|
},
|
||||||
|
|
||||||
|
stepProgress() {
|
||||||
|
return migrationStepProgress.get();
|
||||||
|
},
|
||||||
|
|
||||||
|
stepStatus() {
|
||||||
|
return migrationStepStatus.get();
|
||||||
|
},
|
||||||
|
|
||||||
|
stepDetails() {
|
||||||
|
return migrationStepDetails.get();
|
||||||
|
},
|
||||||
|
|
||||||
|
progressBarStyle() {
|
||||||
|
const progress = migrationProgress.get();
|
||||||
|
return `width: ${progress}%`;
|
||||||
|
},
|
||||||
|
|
||||||
|
stepProgressBarStyle() {
|
||||||
|
const progress = migrationStepProgress.get();
|
||||||
|
return `width: ${progress}%`;
|
||||||
|
},
|
||||||
|
|
||||||
|
stepNameFormatted() {
|
||||||
|
const stepName = migrationStepName.get();
|
||||||
|
if (!stepName) return '';
|
||||||
|
|
||||||
return steps.map(step => ({
|
// Convert snake_case to Title Case
|
||||||
...step,
|
return stepName
|
||||||
isCurrentStep: step.name === currentStep
|
.split('_')
|
||||||
}));
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
},
|
||||||
|
|
||||||
|
stepDetailsFormatted() {
|
||||||
|
const details = migrationStepDetails.get();
|
||||||
|
if (!details) return '';
|
||||||
|
|
||||||
|
const formatted = [];
|
||||||
|
for (const [key, value] of Object.entries(details)) {
|
||||||
|
const formattedKey = key
|
||||||
|
.split(/(?=[A-Z])/)
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/^\w/, c => c.toUpperCase());
|
||||||
|
formatted.push(`${formattedKey}: ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted.join(', ');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Template.migrationProgress.onCreated(function() {
|
// Template events
|
||||||
// Subscribe to migration state changes
|
Template.migrationProgress.events({
|
||||||
this.autorun(() => {
|
'click .js-close-migration-progress'() {
|
||||||
isMigrating.get();
|
migrationProgressManager.clearProgress();
|
||||||
migrationProgress.get();
|
}
|
||||||
migrationStatus.get();
|
});
|
||||||
migrationCurrentStep.get();
|
|
||||||
migrationEstimatedTime.get();
|
|
||||||
migrationSteps.get();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -71,6 +71,13 @@ table tr:nth-child(even) {
|
||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Make checkbox column fit content */
|
||||||
|
table th:first-child,
|
||||||
|
table td:first-child {
|
||||||
|
width: auto;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
#divAddOrRemoveTeam {
|
#divAddOrRemoveTeam {
|
||||||
background: #008000;
|
background: #008000;
|
||||||
display: none;
|
display: none;
|
||||||
|
|
@ -141,7 +148,7 @@ table tr:nth-child(even) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-active-status {
|
.account-active-status {
|
||||||
width: 20px;
|
width: auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,7 +177,7 @@ table tr:nth-child(even) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-status {
|
.account-status {
|
||||||
width: 20px;
|
width: auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ template(name="people")
|
||||||
option(value="locked") {{_ 'admin-people-filter-locked'}}
|
option(value="locked") {{_ 'admin-people-filter-locked'}}
|
||||||
option(value="active") {{_ 'admin-people-filter-active'}}
|
option(value="active") {{_ 'admin-people-filter-active'}}
|
||||||
option(value="inactive") {{_ 'admin-people-filter-inactive'}}
|
option(value="inactive") {{_ 'admin-people-filter-inactive'}}
|
||||||
|
option(value="admin") Admin
|
||||||
button#unlockAllUsers.unlock-all-btn
|
button#unlockAllUsers.unlock-all-btn
|
||||||
| 🔓
|
| 🔓
|
||||||
| {{_ 'accounts-lockout-unlock-all'}}
|
| {{_ 'accounts-lockout-unlock-all'}}
|
||||||
|
|
@ -57,7 +58,7 @@ template(name="people")
|
||||||
| {{_ 'add'}} / {{_ 'delete'}} {{_ 'teams'}}
|
| {{_ 'add'}} / {{_ 'delete'}} {{_ 'teams'}}
|
||||||
else if lockedUsersSetting.get
|
else if lockedUsersSetting.get
|
||||||
span
|
span
|
||||||
| 🔒.text-red
|
span.text-red 🔒
|
||||||
unless isMiniScreen
|
unless isMiniScreen
|
||||||
| {{_ 'accounts-lockout-locked-users'}}
|
| {{_ 'accounts-lockout-locked-users'}}
|
||||||
|
|
||||||
|
|
@ -78,7 +79,7 @@ template(name="people")
|
||||||
| {{_ 'people'}}
|
| {{_ 'people'}}
|
||||||
li
|
li
|
||||||
a.js-locked-users-menu(data-id="locked-users-setting")
|
a.js-locked-users-menu(data-id="locked-users-setting")
|
||||||
| 🔒.text-red
|
span.text-red 🔒
|
||||||
| {{_ 'accounts-lockout-locked-users'}}
|
| {{_ 'accounts-lockout-locked-users'}}
|
||||||
.main-body
|
.main-body
|
||||||
if loading.get
|
if loading.get
|
||||||
|
|
@ -100,7 +101,6 @@ template(name="orgGeneral")
|
||||||
th {{_ 'displayName'}}
|
th {{_ 'displayName'}}
|
||||||
th {{_ 'description'}}
|
th {{_ 'description'}}
|
||||||
th {{_ 'shortName'}}
|
th {{_ 'shortName'}}
|
||||||
th {{_ 'autoAddUsersWithDomainName'}}
|
|
||||||
th {{_ 'website'}}
|
th {{_ 'website'}}
|
||||||
th {{_ 'createdAt'}}
|
th {{_ 'createdAt'}}
|
||||||
th {{_ 'active'}}
|
th {{_ 'active'}}
|
||||||
|
|
@ -140,16 +140,9 @@ template(name="peopleGeneral")
|
||||||
th {{_ 'admin-people-active-status'}}
|
th {{_ 'admin-people-active-status'}}
|
||||||
th {{_ 'username'}}
|
th {{_ 'username'}}
|
||||||
th {{_ 'fullname'}}
|
th {{_ 'fullname'}}
|
||||||
th {{_ 'initials'}}
|
|
||||||
th {{_ 'admin'}}
|
th {{_ 'admin'}}
|
||||||
th {{_ 'email'}}
|
th {{_ 'email'}}
|
||||||
th {{_ 'verified'}}
|
|
||||||
th {{_ 'createdAt'}}
|
th {{_ 'createdAt'}}
|
||||||
th {{_ 'active'}}
|
|
||||||
th {{_ 'authentication-method'}}
|
|
||||||
th {{_ 'import-usernames'}}
|
|
||||||
th {{_ 'organizations'}}
|
|
||||||
th {{_ 'teams'}}
|
|
||||||
th
|
th
|
||||||
+newUserRow
|
+newUserRow
|
||||||
tbody
|
tbody
|
||||||
|
|
@ -190,10 +183,6 @@ template(name="orgRow")
|
||||||
td {{ orgData.orgShortName }}
|
td {{ orgData.orgShortName }}
|
||||||
else
|
else
|
||||||
td <s>{{ orgData.orgShortName }}</s>
|
td <s>{{ orgData.orgShortName }}</s>
|
||||||
if orgData.orgIsActive
|
|
||||||
td {{ orgData.orgAutoAddUsersWithDomainName }}
|
|
||||||
else
|
|
||||||
td <s>{{ orgData.orgAutoAddUsersWithDomainName }}</s>
|
|
||||||
if orgData.orgIsActive
|
if orgData.orgIsActive
|
||||||
td {{ orgData.orgWebsite }}
|
td {{ orgData.orgWebsite }}
|
||||||
else
|
else
|
||||||
|
|
@ -258,14 +247,14 @@ template(name="peopleRow")
|
||||||
input.selectUserChkBox(type="checkbox", id="{{userData._id}}")
|
input.selectUserChkBox(type="checkbox", id="{{userData._id}}")
|
||||||
td.account-status
|
td.account-status
|
||||||
if isUserLocked
|
if isUserLocked
|
||||||
| 🔒.text-red.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="true", title="{{_ 'accounts-lockout-click-to-unlock'}}")
|
span.text-red.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="true", title="{{_ 'accounts-lockout-click-to-unlock'}}") 🔒
|
||||||
else
|
else
|
||||||
| 🔓.text-green.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="false", title="{{_ 'accounts-lockout-user-unlocked'}}")
|
span.text-green.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="false", title="{{_ 'accounts-lockout-user-unlocked'}}") 🔓
|
||||||
td.account-active-status
|
td.account-active-status
|
||||||
if userData.loginDisabled
|
if userData.loginDisabled
|
||||||
| 🚫.text-red.js-toggle-active-status(data-user-id=userData._id, data-is-active="false", title="{{_ 'admin-people-user-inactive'}}")
|
span.text-red.js-toggle-active-status(data-user-id=userData._id, data-is-active="false", title="{{_ 'admin-people-user-inactive'}}") 🚫
|
||||||
else
|
else
|
||||||
| ✅.text-green.js-toggle-active-status(data-user-id=userData._id, data-is-active="true", title="{{_ 'admin-people-user-active'}}")
|
span.text-green.js-toggle-active-status(data-user-id=userData._id, data-is-active="true", title="{{_ 'admin-people-user-active'}}") ✅
|
||||||
if userData.loginDisabled
|
if userData.loginDisabled
|
||||||
td.username <s>{{ userData.username }}</s>
|
td.username <s>{{ userData.username }}</s>
|
||||||
else if isUserLocked
|
else if isUserLocked
|
||||||
|
|
@ -276,10 +265,6 @@ template(name="peopleRow")
|
||||||
td <s>{{ userData.profile.fullname }}</s>
|
td <s>{{ userData.profile.fullname }}</s>
|
||||||
else
|
else
|
||||||
td {{ userData.profile.fullname }}
|
td {{ userData.profile.fullname }}
|
||||||
if userData.loginDisabled
|
|
||||||
td <s>{{ userData.profile.initials }}</s>
|
|
||||||
else
|
|
||||||
td {{ userData.profile.initials }}
|
|
||||||
if userData.loginDisabled
|
if userData.loginDisabled
|
||||||
td
|
td
|
||||||
if userData.isAdmin
|
if userData.isAdmin
|
||||||
|
|
@ -296,43 +281,10 @@ template(name="peopleRow")
|
||||||
td <s>{{ userData.emails.[0].address }}</s>
|
td <s>{{ userData.emails.[0].address }}</s>
|
||||||
else
|
else
|
||||||
td {{ userData.emails.[0].address }}
|
td {{ userData.emails.[0].address }}
|
||||||
if userData.loginDisabled
|
|
||||||
td
|
|
||||||
if userData.emails.[0].verified
|
|
||||||
| <s>{{_ 'yes'}}</s>
|
|
||||||
else
|
|
||||||
| <s>{{_ 'no'}}</s>
|
|
||||||
else
|
|
||||||
td
|
|
||||||
if userData.emails.[0].verified
|
|
||||||
| {{_ 'yes'}}
|
|
||||||
else
|
|
||||||
| {{_ 'no'}}
|
|
||||||
if userData.loginDisabled
|
if userData.loginDisabled
|
||||||
td <s>{{ moment userData.createdAt 'LLL' }}</s>
|
td <s>{{ moment userData.createdAt 'LLL' }}</s>
|
||||||
else
|
else
|
||||||
td {{ moment userData.createdAt 'LLL' }}
|
td {{ moment userData.createdAt 'LLL' }}
|
||||||
td
|
|
||||||
if userData.loginDisabled
|
|
||||||
| {{_ 'no'}}
|
|
||||||
else
|
|
||||||
| {{_ 'yes'}}
|
|
||||||
if userData.loginDisabled
|
|
||||||
td <s>{{_ userData.authenticationMethod }}</s>
|
|
||||||
else
|
|
||||||
td {{_ userData.authenticationMethod }}
|
|
||||||
if userData.loginDisabled
|
|
||||||
td <s>{{ userData.importUsernamesString }}</s>
|
|
||||||
else
|
|
||||||
td {{ userData.importUsernamesString }}
|
|
||||||
if userData.loginDisabled
|
|
||||||
td <s>{{ userData.orgsUserBelongs }}</s>
|
|
||||||
else
|
|
||||||
td {{ userData.orgsUserBelongs }}
|
|
||||||
if userData.loginDisabled
|
|
||||||
td <s>{{ userData.teamsUserBelongs }}</s>
|
|
||||||
else
|
|
||||||
td {{ userData.teamsUserBelongs }}
|
|
||||||
td
|
td
|
||||||
a.edit-user
|
a.edit-user
|
||||||
| ✏️
|
| ✏️
|
||||||
|
|
@ -448,8 +400,8 @@ template(name="editUserPopup")
|
||||||
option(value="{{value}}") {{_ value}}
|
option(value="{{value}}") {{_ value}}
|
||||||
label
|
label
|
||||||
| {{_ 'organizations'}}
|
| {{_ 'organizations'}}
|
||||||
| ➕#addUserOrg
|
span#addUserOrg ➕
|
||||||
| ➖#removeUserOrg
|
span#removeUserOrg ➖
|
||||||
select.js-orgs#jsOrgs
|
select.js-orgs#jsOrgs
|
||||||
option(value="-1") {{_ 'organizations'}} :
|
option(value="-1") {{_ 'organizations'}} :
|
||||||
each value in orgsDatas
|
each value in orgsDatas
|
||||||
|
|
@ -458,8 +410,8 @@ template(name="editUserPopup")
|
||||||
input#jsUserOrgIdsInPut.js-userOrgIds.hide(type="hidden" value=user.orgIdsUserBelongs)
|
input#jsUserOrgIdsInPut.js-userOrgIds.hide(type="hidden" value=user.orgIdsUserBelongs)
|
||||||
label
|
label
|
||||||
| {{_ 'teams'}}
|
| {{_ 'teams'}}
|
||||||
| ➕#addUserTeam
|
span#addUserTeam ➕
|
||||||
| ➖#removeUserTeam
|
span#removeUserTeam ➖
|
||||||
select.js-teams#jsTeams
|
select.js-teams#jsTeams
|
||||||
option(value="-1") {{_ 'teams'}} :
|
option(value="-1") {{_ 'teams'}} :
|
||||||
each value in teamsDatas
|
each value in teamsDatas
|
||||||
|
|
@ -591,8 +543,8 @@ template(name="newUserPopup")
|
||||||
option(value="{{value}}") {{_ value}}
|
option(value="{{value}}") {{_ value}}
|
||||||
label
|
label
|
||||||
| {{_ 'organizations'}}
|
| {{_ 'organizations'}}
|
||||||
| ➕#addUserOrgNewUser
|
span#addUserOrgNewUser ➕
|
||||||
| ➖#removeUserOrgNewUser
|
span#removeUserOrgNewUser ➖
|
||||||
select.js-orgsNewUser#jsOrgsNewUser
|
select.js-orgsNewUser#jsOrgsNewUser
|
||||||
option(value="-1") {{_ 'organizations'}} :
|
option(value="-1") {{_ 'organizations'}} :
|
||||||
each value in orgsDatas
|
each value in orgsDatas
|
||||||
|
|
@ -601,8 +553,8 @@ template(name="newUserPopup")
|
||||||
input#jsUserOrgIdsInPutNewUser.js-userOrgIdsNewUser.hide(type="text" value=user.orgIdsUserBelongs)
|
input#jsUserOrgIdsInPutNewUser.js-userOrgIdsNewUser.hide(type="text" value=user.orgIdsUserBelongs)
|
||||||
label
|
label
|
||||||
| {{_ 'teams'}}
|
| {{_ 'teams'}}
|
||||||
| ➕#addUserTeamNewUser
|
span#addUserTeamNewUser ➕
|
||||||
| ➖#removeUserTeamNewUser
|
span#removeUserTeamNewUser ➖
|
||||||
select.js-teamsNewUser#jsTeamsNewUser
|
select.js-teamsNewUser#jsTeamsNewUser
|
||||||
option(value="-1") {{_ 'teams'}} :
|
option(value="-1") {{_ 'teams'}} :
|
||||||
each value in teamsDatas
|
each value in teamsDatas
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,10 @@ BlazeComponent.extendComponent({
|
||||||
// Show only inactive users (loginDisabled is true)
|
// Show only inactive users (loginDisabled is true)
|
||||||
query['loginDisabled'] = true;
|
query['loginDisabled'] = true;
|
||||||
break;
|
break;
|
||||||
|
case 'admin':
|
||||||
|
// Show only admin users (isAdmin is true)
|
||||||
|
query['isAdmin'] = true;
|
||||||
|
break;
|
||||||
case 'all':
|
case 'all':
|
||||||
default:
|
default:
|
||||||
// Show all users, no additional filter
|
// Show all users, no additional filter
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,59 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Use checklist-style green checkboxes for all sidebar checkboxes */
|
||||||
|
.sidebar .materialCheckBox.is-checked,
|
||||||
|
.boardCardSettingsPopup .materialCheckBox.is-checked,
|
||||||
|
.boardSubtaskSettingsPopup .materialCheckBox.is-checked {
|
||||||
|
top: -4px !important;
|
||||||
|
left: -3px !important;
|
||||||
|
width: 7px !important;
|
||||||
|
height: 15px !important;
|
||||||
|
margin-right: 6px !important;
|
||||||
|
border-top: 2px solid transparent !important;
|
||||||
|
border-left: 2px solid transparent !important;
|
||||||
|
border-bottom: 2px solid #3cb500 !important;
|
||||||
|
border-right: 2px solid #3cb500 !important;
|
||||||
|
transform: rotate(40deg) !important;
|
||||||
|
-webkit-backface-visibility: hidden !important;
|
||||||
|
backface-visibility: hidden !important;
|
||||||
|
transform-origin: 100% 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Settings 3-column grid layout */
|
||||||
|
.card-settings-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 2fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-settings-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 2fr;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-settings-column {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-settings-column:last-child {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-settings-column h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
.sidebar .sidebar-content ul.sidebar-list li > a {
|
.sidebar .sidebar-content ul.sidebar-list li > a {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
|
|
||||||
|
|
@ -28,38 +28,32 @@ template(name="sidebar")
|
||||||
+Template.dynamic(template=getViewTemplate)
|
+Template.dynamic(template=getViewTemplate)
|
||||||
|
|
||||||
template(name='homeSidebar')
|
template(name='homeSidebar')
|
||||||
hr
|
|
||||||
+membersWidget
|
+membersWidget
|
||||||
hr
|
hr
|
||||||
+labelsWidget
|
+labelsWidget
|
||||||
hr
|
hr
|
||||||
ul#cards.label-text-hidden
|
ul#cards.label-text-hidden
|
||||||
a.flex.js-toggle-minicard-label-text(title="{{_ 'hide-minicard-label-text'}}")
|
a.flex.js-toggle-minicard-label-text(title="{{_ 'hide-minicard-label-text'}}")
|
||||||
|
span {{#if hiddenMinicardLabelText}}✅{{else}}⬜{{/if}}
|
||||||
span {{_ 'hide-minicard-label-text'}}
|
span {{_ 'hide-minicard-label-text'}}
|
||||||
b
|
if currentUser
|
||||||
.materialCheckBox(class="{{#if hiddenMinicardLabelText}}is-checked{{/if}}")
|
ul#cards.vertical-scrollbars-toggle
|
||||||
ul#cards.vertical-scrollbars-toggle
|
a.flex.js-vertical-scrollbars-toggle(title="{{_ 'enable-vertical-scrollbars'}}")
|
||||||
a.flex.js-vertical-scrollbars-toggle(title="{{_ 'enable-vertical-scrollbars'}}")
|
span {{#if isVerticalScrollbars}}✅{{else}}⬜{{/if}}
|
||||||
span {{_ 'enable-vertical-scrollbars'}}
|
span {{_ 'enable-vertical-scrollbars'}}
|
||||||
b
|
|
||||||
.materialCheckBox(class="{{#if isVerticalScrollbars}}is-checked{{/if}}")
|
|
||||||
ul#cards.show-week-of-year-toggle
|
ul#cards.show-week-of-year-toggle
|
||||||
a.flex.js-show-week-of-year-toggle(title="{{_ 'show-week-of-year'}}")
|
a.flex.js-show-week-of-year-toggle(title="{{_ 'show-week-of-year'}}")
|
||||||
|
span {{#if isShowWeekOfYear}}✅{{else}}⬜{{/if}}
|
||||||
span {{_ 'show-week-of-year'}}
|
span {{_ 'show-week-of-year'}}
|
||||||
b
|
|
||||||
.materialCheckBox(class="{{#if isShowWeekOfYear}}is-checked{{/if}}")
|
|
||||||
hr
|
hr
|
||||||
unless currentUser.isNoComments
|
unless currentUser.isNoComments
|
||||||
h3.activity-title
|
h3.activity-title
|
||||||
| 💬
|
| 💬
|
||||||
| {{_ 'activities'}}
|
| {{_ 'activities'}}
|
||||||
|
|
||||||
.material-toggle-switch(title="{{_ 'show-activities'}}")
|
a.flex.js-toggle-show-activities(title="{{_ 'show-activities'}}")
|
||||||
if showActivities
|
span {{#if showActivities}}✅{{else}}⬜{{/if}}
|
||||||
input.toggle-switch(type="checkbox" id="toggleShowActivitiesBoard" checked="checked")
|
span {{_ 'show-activities'}}
|
||||||
else
|
|
||||||
input.toggle-switch(type="checkbox" id="toggleShowActivitiesBoard")
|
|
||||||
label.toggle-label(for="toggleShowActivitiesBoard")
|
|
||||||
+activities(mode="board")
|
+activities(mode="board")
|
||||||
|
|
||||||
template(name="membersWidget")
|
template(name="membersWidget")
|
||||||
|
|
@ -185,165 +179,282 @@ template(name="boardInfoOnMyBoardsPopup")
|
||||||
unless currentSetting.hideCardCounterList
|
unless currentSetting.hideCardCounterList
|
||||||
div.check-div
|
div.check-div
|
||||||
a.flex.js-field-has-cardcounterlist(class="{{#if allowsCardCounterList}}is-checked{{/if}}")
|
a.flex.js-field-has-cardcounterlist(class="{{#if allowsCardCounterList}}is-checked{{/if}}")
|
||||||
.materialCheckBox(class="{{#if allowsCardCounterList}}is-checked{{/if}}")
|
span {{#if allowsCardCounterList}}✅{{else}}⬜{{/if}}
|
||||||
span
|
span
|
||||||
| 🚪
|
| 🚪
|
||||||
| {{_ 'show-card-counter-per-list'}}
|
| {{_ 'show-card-counter-per-list'}}
|
||||||
unless currentSetting.hideBoardMemberList
|
unless currentSetting.hideBoardMemberList
|
||||||
div.check-div
|
div.check-div
|
||||||
a.flex.js-field-has-boardmemberlist(class="{{#if allowsBoardMemberList}}is-checked{{/if}}")
|
a.flex.js-field-has-boardmemberlist(class="{{#if allowsBoardMemberList}}is-checked{{/if}}")
|
||||||
.materialCheckBox(class="{{#if allowsBoardMemberList}}is-checked{{/if}}")
|
span {{#if allowsBoardMemberList}}✅{{else}}⬜{{/if}}
|
||||||
span
|
span
|
||||||
| ⏳
|
| ⏳
|
||||||
| {{_ 'show-board_members-avatar'}}
|
| {{_ 'show-board_members-avatar'}}
|
||||||
|
|
||||||
template(name="boardCardSettingsPopup")
|
template(name="boardCardSettingsPopup")
|
||||||
form.board-card-settings
|
form.board-card-settings
|
||||||
h3 {{_ 'show-on-card'}}, {{_ 'show-on-minicard'}}
|
.card-settings-grid
|
||||||
div.check-div
|
.card-settings-column
|
||||||
a.flex.js-field-has-receiveddate(class="{{#if allowsReceivedDate}}is-checked{{/if}}")
|
h4 {{_ 'show-on-card'}}
|
||||||
.materialCheckBox(class="{{#if allowsReceivedDate}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
|
h4 {{_ 'show-on-minicard'}}
|
||||||
|
.card-settings-column
|
||||||
|
h4 {{_ 'description'}}
|
||||||
|
.card-settings-row
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-receiveddate(title="{{_ 'card-received'}}" class="{{#if allowsReceivedDate}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsReceivedDate}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-receiveddate(title="{{_ 'card-received'}}" class="{{#if allowsReceivedDate}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsReceivedDate}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 🚪
|
| 🚪
|
||||||
| {{_ 'card-received'}}
|
| {{_ 'card-received'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-startdate(class="{{#if allowsStartDate}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsStartDate}}is-checked{{/if}}")
|
a.flex.js-field-has-startdate(title="{{_ 'card-start'}}" class="{{#if allowsStartDate}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsStartDate}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-startdate(title="{{_ 'card-start'}}" class="{{#if allowsStartDate}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsStartDate}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| ⏳
|
| ⏳
|
||||||
| {{_ 'card-start'}}
|
| {{_ 'card-start'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-duedate(class="{{#if allowsDueDate}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsDueDate}}is-checked{{/if}}")
|
a.flex.js-field-has-duedate(title="{{_ 'card-due'}}" class="{{#if allowsDueDate}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsDueDate}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-duedate(title="{{_ 'card-due'}}" class="{{#if allowsDueDate}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsDueDate}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 🚪
|
| 🚪
|
||||||
| {{_ 'card-due'}}
|
| {{_ 'card-due'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-enddate(class="{{#if allowsEndDate}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsEndDate}}is-checked{{/if}}")
|
a.flex.js-field-has-enddate(title="{{_ 'card-end'}}" class="{{#if allowsEndDate}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsEndDate}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-enddate(title="{{_ 'card-end'}}" class="{{#if allowsEndDate}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsEndDate}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| ⏰
|
| ⏰
|
||||||
| {{_ 'card-end'}}
|
| {{_ 'card-end'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-members(class="{{#if allowsMembers}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsMembers}}is-checked{{/if}}")
|
a.flex.js-field-has-members(title="{{_ 'members'}}" class="{{#if allowsMembers}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsMembers}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-members(title="{{_ 'members'}}" class="{{#if allowsMembers}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsMembers}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 👥
|
| 👥
|
||||||
| {{_ 'members'}}
|
| {{_ 'members'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-creator(class="{{#if allowsCreator}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsCreator}}is-checked{{/if}}")
|
a.flex.js-field-has-creator(title="{{_ 'creator'}}" class="{{#if allowsCreator}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsCreator}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
span
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 👤
|
| 👤
|
||||||
| {{_ 'creator'}}
|
| {{_ 'creator'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-creator-on-minicard(class="{{#if allowsCreatorOnMinicard}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsCreatorOnMinicard}}is-checked{{/if}}")
|
span
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-creator-on-minicard(title="{{_ 'creator-on-minicard'}}" class="{{#if allowsCreatorOnMinicard}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsCreatorOnMinicard}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 👤
|
| 👤
|
||||||
| {{_ 'creator-on-minicard'}}
|
| {{_ 'creator-on-minicard'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-assignee(class="{{#if allowsAssignee}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsAssignee}}is-checked{{/if}}")
|
a.flex.js-field-has-assignee(title="{{_ 'assignee'}}" class="{{#if allowsAssignee}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsAssignee}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-assignee(title="{{_ 'assignee'}}" class="{{#if allowsAssignee}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsAssignee}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 👤
|
| 👤
|
||||||
| {{_ 'assignee'}}
|
| {{_ 'assignee'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-assigned-by(class="{{#if allowsAssignedBy}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsAssignedBy}}is-checked{{/if}}")
|
a.flex.js-field-has-assigned-by(title="{{_ 'assigned-by'}}" class="{{#if allowsAssignedBy}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsAssignedBy}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-assigned-by(title="{{_ 'assigned-by'}}" class="{{#if allowsAssignedBy}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsAssignedBy}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 🛒
|
| 🛒
|
||||||
| {{_ 'assigned-by'}}
|
| {{_ 'assigned-by'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-requested-by(class="{{#if allowsRequestedBy}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsRequestedBy}}is-checked{{/if}}")
|
a.flex.js-field-has-requested-by(title="{{_ 'requested-by'}}" class="{{#if allowsRequestedBy}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsRequestedBy}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-requested-by(title="{{_ 'requested-by'}}" class="{{#if allowsRequestedBy}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsRequestedBy}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 👤➕
|
| 👤➕
|
||||||
| {{_ 'requested-by'}}
|
| {{_ 'requested-by'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-card-sorting-by-number(class="{{#if allowsCardSortingByNumber}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsCardSortingByNumber}}is-checked{{/if}}")
|
a.flex.js-field-has-card-sorting-by-number(title="{{_ 'card-sorting-by-number'}}" class="{{#if allowsCardSortingByNumber}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsCardSortingByNumber}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
span
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 🔢
|
| 🔢
|
||||||
| {{_ 'card-sorting-by-number'}}
|
| {{_ 'card-sorting-by-number'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-card-sorting-by-number-on-minicard(class="{{#if allowsCardSortingByNumberOnMinicard}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsCardSortingByNumberOnMinicard}}is-checked{{/if}}")
|
span
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-card-sorting-by-number-on-minicard(title="{{_ 'card-sorting-by-number-on-minicard'}}" class="{{#if allowsCardSortingByNumberOnMinicard}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsCardSortingByNumberOnMinicard}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 🔢
|
| 🔢
|
||||||
| {{_ 'card-sorting-by-number-on-minicard'}}
|
| {{_ 'card-sorting-by-number-on-minicard'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-card-show-lists(class="{{#if allowsShowLists}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsShowLists}}is-checked{{/if}}")
|
a.flex.js-field-has-card-show-lists(title="{{_ 'card-show-lists'}}" class="{{#if allowsShowLists}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsShowLists}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
span
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 📋
|
| 📋
|
||||||
| {{_ 'card-show-lists'}}
|
| {{_ 'card-show-lists'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-labels(class="{{#if allowsLabels}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsLabels}}is-checked{{/if}}")
|
a.flex.js-field-has-labels(title="{{_ 'labels'}}" class="{{#if allowsLabels}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsLabels}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-labels(title="{{_ 'labels'}}" class="{{#if allowsLabels}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsLabels}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 🏷️
|
| 🏷️
|
||||||
| {{_ 'labels'}}
|
| {{_ 'labels'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-card-show-lists-on-minicard(class="{{#if allowsShowListsOnMinicard}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsShowListsOnMinicard}}is-checked{{/if}}")
|
span
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-card-show-lists-on-minicard(title="{{_ 'card-show-lists-on-minicard'}}" class="{{#if allowsShowListsOnMinicard}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsShowListsOnMinicard}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 📋
|
| 📋
|
||||||
| {{_ 'card-show-lists-on-minicard'}}
|
| {{_ 'card-show-lists-on-minicard'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-card-number(class="{{#if allowsCardNumber}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsCardNumber}}is-checked{{/if}}")
|
a.flex.js-field-has-card-number(title="{{_ 'card'}} {{_ 'number'}}" class="{{#if allowsCardNumber}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsCardNumber}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-card-number(title="{{_ 'card'}} {{_ 'number'}}" class="{{#if allowsCardNumber}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsCardNumber}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| #️⃣
|
| #️⃣
|
||||||
| {{_ 'card'}}
|
| {{_ 'card'}}
|
||||||
| {{_ 'number'}}
|
| {{_ 'number'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-description-title(class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
|
a.flex.js-field-has-description-title(title="{{_ 'description'}} {{_ 'title'}}" class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsDescriptionTitle}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-description-title(title="{{_ 'description'}} {{_ 'title'}}" class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsDescriptionTitle}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 📝
|
| 📝
|
||||||
| {{_ 'description'}}
|
| {{_ 'description'}}
|
||||||
| {{_ 'title'}}
|
| {{_ 'title'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-description-text(class="{{#if allowsDescriptionText}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsDescriptionText}}is-checked{{/if}}")
|
a.flex.js-field-has-description-text(title="{{_ 'description'}} {{_ 'custom-field-text'}}" class="{{#if allowsDescriptionText}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsDescriptionText}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-description-text(title="{{_ 'description'}} {{_ 'custom-field-text'}}" class="{{#if allowsDescriptionText}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsDescriptionText}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 📝
|
| 📝
|
||||||
| {{_ 'description'}}
|
| {{_ 'description'}}
|
||||||
| {{_ 'custom-field-text'}}
|
| {{_ 'custom-field-text'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-description-text-on-minicard(class="{{#if allowsDescriptionTextOnMinicard}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsDescriptionTextOnMinicard}}is-checked{{/if}}")
|
span
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-description-text-on-minicard(title="{{_ 'description-on-minicard'}}" class="{{#if allowsDescriptionTextOnMinicard}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsDescriptionTextOnMinicard}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 📝
|
| 📝
|
||||||
| {{_ 'description-on-minicard'}}
|
| {{_ 'description-on-minicard'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-checklists(class="{{#if allowsChecklists}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsChecklists}}is-checked{{/if}}")
|
a.flex.js-field-has-checklists(title="{{_ 'checklists'}}" class="{{#if allowsChecklists}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsChecklists}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-checklists(title="{{_ 'checklists'}}" class="{{#if allowsChecklists}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsChecklists}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| ✅
|
| ✅
|
||||||
| {{_ 'checklists'}}
|
| {{_ 'checklists'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-subtasks(class="{{#if allowsSubtasks}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsSubtasks}}is-checked{{/if}}")
|
a.flex.js-field-has-subtasks(title="{{_ 'subtasks'}}" class="{{#if allowsSubtasks}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsSubtasks}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-subtasks(title="{{_ 'subtasks'}}" class="{{#if allowsSubtasks}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsSubtasks}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 🌐
|
| 🌐
|
||||||
| {{_ 'subtasks'}}
|
| {{_ 'subtasks'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-attachments(class="{{#if allowsAttachments}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsAttachments}}is-checked{{/if}}")
|
a.flex.js-field-has-attachments(title="{{_ 'attachments'}}" class="{{#if allowsAttachments}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsAttachments}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-attachments(title="{{_ 'attachments'}}" class="{{#if allowsAttachments}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsAttachments}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 📎
|
| 📎
|
||||||
| {{_ 'attachments'}}
|
| {{_ 'attachments'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-badge-attachment-on-minicard(class="{{#if allowsBadgeAttachmentOnMinicard}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsBadgeAttachmentOnMinicard}}is-checked{{/if}}")
|
span
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-badge-attachment-on-minicard(title="{{_ 'badge-attachment-on-minicard'}}" class="{{#if allowsBadgeAttachmentOnMinicard}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsBadgeAttachmentOnMinicard}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 📎
|
| 📎
|
||||||
| {{_ 'badge-attachment-on-minicard'}}
|
| {{_ 'badge-attachment-on-minicard'}}
|
||||||
div.check-div
|
.card-settings-row
|
||||||
a.flex.js-field-has-cover-attachment-on-minicard(class="{{#if allowsCoverAttachmentOnMinicard}}is-checked{{/if}}")
|
.card-settings-column
|
||||||
.materialCheckBox(class="{{#if allowsCoverAttachmentOnMinicard}}is-checked{{/if}}")
|
span
|
||||||
|
.card-settings-column
|
||||||
|
a.flex.js-field-has-cover-attachment-on-minicard(title="{{_ 'cover-attachment-on-minicard'}}" class="{{#if allowsCoverAttachmentOnMinicard}}is-checked{{/if}}")
|
||||||
|
span {{#if allowsCoverAttachmentOnMinicard}}✅{{else}}⬜{{/if}}
|
||||||
|
.card-settings-column
|
||||||
span
|
span
|
||||||
| 📖
|
| 📖
|
||||||
| 🖼️
|
| 🖼️
|
||||||
|
|
@ -364,27 +475,27 @@ template(name="boardCardSettingsPopup")
|
||||||
template(name="boardSubtaskSettingsPopup")
|
template(name="boardSubtaskSettingsPopup")
|
||||||
form.board-subtask-settings
|
form.board-subtask-settings
|
||||||
h3 {{_ 'show-parent-in-minicard'}}
|
h3 {{_ 'show-parent-in-minicard'}}
|
||||||
a#prefix-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
|
a#prefix-with-full-path.flex.js-field-show-parent-in-minicard(title="{{_ 'prefix-with-full-path'}}" class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
|
||||||
.materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
|
span {{#if $eq presentParentTask 'prefix-with-full-path'}}✅{{else}}⬜{{/if}}
|
||||||
span {{_ 'prefix-with-full-path'}}
|
span {{_ 'prefix-with-full-path'}}
|
||||||
a#prefix-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
|
a#prefix-with-parent.flex.js-field-show-parent-in-minicard(title="{{_ 'prefix-with-parent'}}" class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
|
||||||
.materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
|
span {{#if $eq presentParentTask 'prefix-with-parent'}}✅{{else}}⬜{{/if}}
|
||||||
span {{_ 'prefix-with-parent'}}
|
span {{_ 'prefix-with-parent'}}
|
||||||
a#subtext-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
|
a#subtext-with-full-path.flex.js-field-show-parent-in-minicard(title="{{_ 'subtext-with-full-path'}}" class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
|
||||||
.materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
|
span {{#if $eq presentParentTask 'subtext-with-full-path'}}✅{{else}}⬜{{/if}}
|
||||||
span {{_ 'subtext-with-full-path'}}
|
span {{_ 'subtext-with-full-path'}}
|
||||||
a#subtext-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
|
a#subtext-with-parent.flex.js-field-show-parent-in-minicard(title="{{_ 'subtext-with-parent'}}" class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
|
||||||
.materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
|
span {{#if $eq presentParentTask 'subtext-with-parent'}}✅{{else}}⬜{{/if}}
|
||||||
span {{_ 'subtext-with-parent'}}
|
span {{_ 'subtext-with-parent'}}
|
||||||
a#no-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
|
a#no-parent.flex.js-field-show-parent-in-minicard(title="{{_ 'no-parent'}}" class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
|
||||||
.materialCheckBox(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
|
span {{#if $eq presentParentTask 'no-parent'}}✅{{else}}⬜{{/if}}
|
||||||
span {{_ 'no-parent'}}
|
span {{_ 'no-parent'}}
|
||||||
div
|
div
|
||||||
hr
|
hr
|
||||||
|
|
||||||
div.check-div
|
div.check-div
|
||||||
a.flex.js-field-has-subtasks(class="{{#if allowsSubtasks}}is-checked{{/if}}")
|
a.flex.js-field-has-subtasks(title="{{_ 'show-subtasks-field'}}" class="{{#if allowsSubtasks}}is-checked{{/if}}")
|
||||||
.materialCheckBox(class="{{#if allowsSubtasks}}is-checked{{/if}}")
|
span {{#if allowsSubtasks}}✅{{else}}⬜{{/if}}
|
||||||
span {{_ 'show-subtasks-field'}}
|
span {{_ 'show-subtasks-field'}}
|
||||||
|
|
||||||
label
|
label
|
||||||
|
|
@ -426,13 +537,18 @@ template(name="archiveBoardPopup")
|
||||||
| 📦
|
| 📦
|
||||||
| {{_ 'archive'}}
|
| {{_ 'archive'}}
|
||||||
|
|
||||||
|
template(name="deleteDuplicateListsPopup")
|
||||||
|
p {{_ 'delete-duplicate-lists-confirm'}}
|
||||||
|
button.js-confirm.negate.full(type="submit")
|
||||||
|
| 🗑️
|
||||||
|
| {{_ 'delete'}}
|
||||||
|
|
||||||
template(name="outgoingWebhooksPopup")
|
template(name="outgoingWebhooksPopup")
|
||||||
each integrations
|
each integrations
|
||||||
form.integration-form
|
form.integration-form
|
||||||
a.flex
|
a.flex
|
||||||
|
span {{#unless enabled}}✅{{else}}⬜{{/unless}}
|
||||||
span {{_ 'disable-webhook'}}
|
span {{_ 'disable-webhook'}}
|
||||||
b
|
|
||||||
.materialCheckBox(class="{{#unless enabled}}is-checked{{/unless}}")
|
|
||||||
input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" value=title)
|
input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" value=title)
|
||||||
input.js-outgoing-webhooks-url(type="text" name="url" value=url)
|
input.js-outgoing-webhooks-url(type="text" name="url" value=url)
|
||||||
input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" value=token name="token")
|
input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" value=token name="token")
|
||||||
|
|
@ -471,6 +587,10 @@ template(name="boardMenuPopup")
|
||||||
| 📦
|
| 📦
|
||||||
| {{_ 'archived-items'}}
|
| {{_ 'archived-items'}}
|
||||||
if currentUser.isBoardAdmin
|
if currentUser.isBoardAdmin
|
||||||
|
li
|
||||||
|
a.js-open-migrations
|
||||||
|
| 🔧
|
||||||
|
| {{_ 'migrations'}}
|
||||||
li
|
li
|
||||||
a.js-change-board-color
|
a.js-change-board-color
|
||||||
| 🎨
|
| 🎨
|
||||||
|
|
@ -511,6 +631,10 @@ template(name="boardMenuPopup")
|
||||||
if currentUser.isBoardAdmin
|
if currentUser.isBoardAdmin
|
||||||
hr
|
hr
|
||||||
ul.pop-over-list
|
ul.pop-over-list
|
||||||
|
// li
|
||||||
|
// a.js-delete-duplicate-lists
|
||||||
|
// | 🗑️
|
||||||
|
// | {{_ 'delete-duplicate-lists'}}
|
||||||
li
|
li
|
||||||
a.js-archive-board
|
a.js-archive-board
|
||||||
| ➡️📦
|
| ➡️📦
|
||||||
|
|
@ -631,7 +755,7 @@ template(name="removeBoardTeamPopup")
|
||||||
|
|
||||||
template(name="addMemberPopup")
|
template(name="addMemberPopup")
|
||||||
.js-search-member
|
.js-search-member
|
||||||
+EasySearch.Input(index=searchIndex)
|
input.js-search-member-input(type="text" placeholder="{{_ 'email-address'}}")
|
||||||
|
|
||||||
if loading.get
|
if loading.get
|
||||||
+spinner
|
+spinner
|
||||||
|
|
@ -639,25 +763,38 @@ template(name="addMemberPopup")
|
||||||
.warning {{_ error.get}}
|
.warning {{_ error.get}}
|
||||||
else
|
else
|
||||||
ul.pop-over-list
|
ul.pop-over-list
|
||||||
+EasySearch.Each(index=searchIndex)
|
each searchResults
|
||||||
li.item.js-member-item(class="{{#if isBoardMember}}disabled{{/if}}")
|
li.item.js-member-item(class="{{#if isBoardMember}}disabled{{/if}}")
|
||||||
a.name.js-select-member(title="{{profile.fullname}} ({{username}})")
|
a.name.js-select-member(title="{{profile.fullname}} ({{username}})")
|
||||||
+userAvatar(userId=__originalId)
|
+userAvatar(userId=_id)
|
||||||
span.full-name
|
span.full-name
|
||||||
= profile.fullname
|
= profile.fullname
|
||||||
| (<span class="username">{{username}}</span>)
|
| (<span class="username">{{username}}</span>)
|
||||||
if isBoardMember
|
if isBoardMember
|
||||||
.quiet ({{_ 'joined'}})
|
.quiet ({{_ 'joined'}})
|
||||||
|
|
||||||
+EasySearch.IfSearching(index=searchIndex)
|
if searching.get
|
||||||
+spinner
|
+spinner
|
||||||
|
|
||||||
+EasySearch.IfNoResults(index=searchIndex)
|
if noResults.get
|
||||||
.manage-member-section
|
.manage-member-section
|
||||||
p.quiet {{_ 'no-results'}}
|
p.quiet {{_ 'no-results'}}
|
||||||
button.js-email-invite.primary.full {{_ 'email-invite'}}
|
button.js-email-invite.primary.full {{_ 'email-invite'}}
|
||||||
|
|
||||||
|
|
||||||
|
template(name="addMemberPopupTest")
|
||||||
|
.js-search-member
|
||||||
|
input.js-search-member-input(type="text" placeholder="{{_ 'email-address'}}")
|
||||||
|
ul.pop-over-list
|
||||||
|
each searchResults
|
||||||
|
li.item.js-member-item
|
||||||
|
a.name.js-select-member(title="{{profile.fullname}} ({{username}})")
|
||||||
|
+userAvatar(userId=_id)
|
||||||
|
span.full-name
|
||||||
|
= profile.fullname
|
||||||
|
| (<span class="username">{{username}}</span>)
|
||||||
|
button.js-email-invite.primary.full {{_ 'email-invite'}}
|
||||||
|
|
||||||
template(name="changePermissionsPopup")
|
template(name="changePermissionsPopup")
|
||||||
ul.pop-over-list
|
ul.pop-over-list
|
||||||
li
|
li
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const viewTitles = {
|
||||||
multiselection: 'multi-selection',
|
multiselection: 'multi-selection',
|
||||||
customFields: 'custom-fields',
|
customFields: 'custom-fields',
|
||||||
archives: 'archives',
|
archives: 'archives',
|
||||||
|
migrations: 'migrations',
|
||||||
};
|
};
|
||||||
|
|
||||||
BlazeComponent.extendComponent({
|
BlazeComponent.extendComponent({
|
||||||
|
|
@ -195,7 +196,7 @@ BlazeComponent.extendComponent({
|
||||||
events() {
|
events() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'click #toggleShowActivitiesBoard'() {
|
'click .js-toggle-show-activities'() {
|
||||||
Utils.getCurrentBoard().toggleShowActivities();
|
Utils.getCurrentBoard().toggleShowActivities();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -203,6 +204,8 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
}).register('homeSidebar');
|
}).register('homeSidebar');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Template.boardInfoOnMyBoardsPopup.helpers({
|
Template.boardInfoOnMyBoardsPopup.helpers({
|
||||||
hideCardCounterList() {
|
hideCardCounterList() {
|
||||||
return Utils.isMiniScreen() && Session.get('currentBoard');
|
return Utils.isMiniScreen() && Session.get('currentBoard');
|
||||||
|
|
@ -269,10 +272,53 @@ Template.boardMenuPopup.events({
|
||||||
Sidebar.setView('archives');
|
Sidebar.setView('archives');
|
||||||
Popup.back();
|
Popup.back();
|
||||||
},
|
},
|
||||||
|
'click .js-open-migrations'() {
|
||||||
|
Sidebar.setView('migrations');
|
||||||
|
Popup.back();
|
||||||
|
},
|
||||||
'click .js-change-board-color': Popup.open('boardChangeColor'),
|
'click .js-change-board-color': Popup.open('boardChangeColor'),
|
||||||
'click .js-change-background-image': Popup.open('boardChangeBackgroundImage'),
|
'click .js-change-background-image': Popup.open('boardChangeBackgroundImage'),
|
||||||
'click .js-board-info-on-my-boards': Popup.open('boardInfoOnMyBoards'),
|
'click .js-board-info-on-my-boards': Popup.open('boardInfoOnMyBoards'),
|
||||||
'click .js-change-language': Popup.open('changeLanguage'),
|
'click .js-change-language': Popup.open('changeLanguage'),
|
||||||
|
'click .js-delete-duplicate-lists': Popup.afterConfirm('deleteDuplicateLists', function() {
|
||||||
|
const currentBoard = Utils.getCurrentBoard();
|
||||||
|
if (!currentBoard) return;
|
||||||
|
|
||||||
|
// Get all lists in the current board
|
||||||
|
const allLists = ReactiveCache.getLists({ boardId: currentBoard._id, archived: false });
|
||||||
|
|
||||||
|
// Group lists by title to find duplicates
|
||||||
|
const listsByTitle = {};
|
||||||
|
allLists.forEach(list => {
|
||||||
|
if (!listsByTitle[list.title]) {
|
||||||
|
listsByTitle[list.title] = [];
|
||||||
|
}
|
||||||
|
listsByTitle[list.title].push(list);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find and delete duplicate lists that have no cards
|
||||||
|
let deletedCount = 0;
|
||||||
|
Object.keys(listsByTitle).forEach(title => {
|
||||||
|
const listsWithSameTitle = listsByTitle[title];
|
||||||
|
if (listsWithSameTitle.length > 1) {
|
||||||
|
// Keep the first list, delete the rest if they have no cards
|
||||||
|
for (let i = 1; i < listsWithSameTitle.length; i++) {
|
||||||
|
const list = listsWithSameTitle[i];
|
||||||
|
const cardsInList = ReactiveCache.getCards({ listId: list._id, archived: false });
|
||||||
|
|
||||||
|
if (cardsInList.length === 0) {
|
||||||
|
Lists.remove(list._id);
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show notification
|
||||||
|
if (deletedCount > 0) {
|
||||||
|
// You could add a toast notification here if available
|
||||||
|
}
|
||||||
|
}),
|
||||||
'click .js-archive-board ': Popup.afterConfirm('archiveBoard', function() {
|
'click .js-archive-board ': Popup.afterConfirm('archiveBoard', function() {
|
||||||
const currentBoard = Utils.getCurrentBoard();
|
const currentBoard = Utils.getCurrentBoard();
|
||||||
currentBoard.archive();
|
currentBoard.archive();
|
||||||
|
|
@ -820,7 +866,11 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsSubtasks() {
|
allowsSubtasks() {
|
||||||
return this.currentBoard.allowsSubtasks;
|
// Get the current board reactively using board ID from Session
|
||||||
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
const result = currentBoard ? currentBoard.allowsSubtasks : false;
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsReceivedDate() {
|
allowsReceivedDate() {
|
||||||
|
|
@ -872,7 +922,11 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
presentParentTask() {
|
presentParentTask() {
|
||||||
let result = this.currentBoard.presentParentTask;
|
// Get the current board reactively using board ID from Session
|
||||||
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
|
||||||
|
let result = currentBoard ? currentBoard.presentParentTask : null;
|
||||||
if (result === null || result === undefined) {
|
if (result === null || result === undefined) {
|
||||||
result = 'no-parent';
|
result = 'no-parent';
|
||||||
}
|
}
|
||||||
|
|
@ -884,19 +938,11 @@ BlazeComponent.extendComponent({
|
||||||
{
|
{
|
||||||
'click .js-field-has-subtasks'(evt) {
|
'click .js-field-has-subtasks'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks;
|
const newValue = !this.currentBoard.allowsSubtasks;
|
||||||
this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks);
|
Boards.update(this.currentBoard._id, { $set: { allowsSubtasks: newValue } });
|
||||||
$(`.js-field-has-subtasks ${MCB}`).toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsSubtasks,
|
|
||||||
);
|
|
||||||
$('.js-field-has-subtasks').toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsSubtasks,
|
|
||||||
);
|
|
||||||
$('.js-field-deposit-board').prop(
|
$('.js-field-deposit-board').prop(
|
||||||
'disabled',
|
'disabled',
|
||||||
!this.currentBoard.allowsSubtasks,
|
!newValue,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
'change .js-field-deposit-board'(evt) {
|
'change .js-field-deposit-board'(evt) {
|
||||||
|
|
@ -912,28 +958,13 @@ BlazeComponent.extendComponent({
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
},
|
},
|
||||||
'click .js-field-show-parent-in-minicard'(evt) {
|
'click .js-field-show-parent-in-minicard'(evt) {
|
||||||
const value =
|
// Get the ID from the anchor element, not the span
|
||||||
evt.target.id ||
|
const anchorElement = $(evt.target).closest('.js-field-show-parent-in-minicard')[0];
|
||||||
$(evt.target).parent()[0].id ||
|
const value = anchorElement ? anchorElement.id : null;
|
||||||
$(evt.target)
|
|
||||||
.parent()[0]
|
if (value) {
|
||||||
.parent()[0].id;
|
Boards.update(this.currentBoard._id, { $set: { presentParentTask: value } });
|
||||||
const options = [
|
}
|
||||||
'prefix-with-full-path',
|
|
||||||
'prefix-with-parent',
|
|
||||||
'subtext-with-full-path',
|
|
||||||
'subtext-with-parent',
|
|
||||||
'no-parent',
|
|
||||||
];
|
|
||||||
options.forEach(function(element) {
|
|
||||||
if (element !== value) {
|
|
||||||
$(`#${element} ${MCB}`).toggleClass(CKCLS, false);
|
|
||||||
$(`#${element}`).toggleClass(CKCLS, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$(`#${value} ${MCB}`).toggleClass(CKCLS, true);
|
|
||||||
$(`#${value}`).toggleClass(CKCLS, true);
|
|
||||||
this.currentBoard.setPresentParentTask(value);
|
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -947,115 +978,168 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsReceivedDate() {
|
allowsReceivedDate() {
|
||||||
return this.currentBoard.allowsReceivedDate;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsReceivedDate : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsStartDate() {
|
allowsStartDate() {
|
||||||
return this.currentBoard.allowsStartDate;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsStartDate : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsDueDate() {
|
allowsDueDate() {
|
||||||
return this.currentBoard.allowsDueDate;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsDueDate : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsEndDate() {
|
allowsEndDate() {
|
||||||
return this.currentBoard.allowsEndDate;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsEndDate : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsSubtasks() {
|
allowsSubtasks() {
|
||||||
return this.currentBoard.allowsSubtasks;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsSubtasks : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsCreator() {
|
allowsCreator() {
|
||||||
return this.currentBoard.allowsCreator ?? false;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? (currentBoard.allowsCreator ?? false) : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsCreatorOnMinicard() {
|
allowsCreatorOnMinicard() {
|
||||||
return this.currentBoard.allowsCreatorOnMinicard ?? false;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? (currentBoard.allowsCreatorOnMinicard ?? false) : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsMembers() {
|
allowsMembers() {
|
||||||
return this.currentBoard.allowsMembers;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsMembers : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsAssignee() {
|
allowsAssignee() {
|
||||||
return this.currentBoard.allowsAssignee;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsAssignee : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsAssignedBy() {
|
allowsAssignedBy() {
|
||||||
return this.currentBoard.allowsAssignedBy;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsAssignedBy : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsRequestedBy() {
|
allowsRequestedBy() {
|
||||||
return this.currentBoard.allowsRequestedBy;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsRequestedBy : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsCardSortingByNumber() {
|
allowsCardSortingByNumber() {
|
||||||
return this.currentBoard.allowsCardSortingByNumber;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsCardSortingByNumber : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsShowLists() {
|
allowsShowLists() {
|
||||||
return this.currentBoard.allowsShowLists;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsShowLists : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
allowsLabels() {
|
allowsLabels() {
|
||||||
return this.currentBoard.allowsLabels;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsLabels : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsShowListsOnMinicard() {
|
allowsShowListsOnMinicard() {
|
||||||
return this.currentBoard.allowsShowListsOnMinicard;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsShowListsOnMinicard : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsChecklists() {
|
allowsChecklists() {
|
||||||
return this.currentBoard.allowsChecklists;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsChecklists : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsAttachments() {
|
allowsAttachments() {
|
||||||
return this.currentBoard.allowsAttachments;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsAttachments : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsComments() {
|
allowsComments() {
|
||||||
return this.currentBoard.allowsComments;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsComments : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsCardNumber() {
|
allowsCardNumber() {
|
||||||
return this.currentBoard.allowsCardNumber;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsCardNumber : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsDescriptionTitle() {
|
allowsDescriptionTitle() {
|
||||||
return this.currentBoard.allowsDescriptionTitle;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsDescriptionTitle : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsDescriptionText() {
|
allowsDescriptionText() {
|
||||||
return this.currentBoard.allowsDescriptionText;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsDescriptionText : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
isBoardSelected() {
|
isBoardSelected() {
|
||||||
return this.currentBoard.dateSettingsDefaultBoardID;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.dateSettingsDefaultBoardID : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
isNullBoardSelected() {
|
isNullBoardSelected() {
|
||||||
return (
|
const boardId = Session.get('currentBoard');
|
||||||
this.currentBoard.dateSettingsDefaultBoardId === null ||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
this.currentBoard.dateSettingsDefaultBoardId === undefined
|
return currentBoard ? (
|
||||||
);
|
currentBoard.dateSettingsDefaultBoardId === null ||
|
||||||
|
currentBoard.dateSettingsDefaultBoardId === undefined
|
||||||
|
) : true;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsDescriptionTextOnMinicard() {
|
allowsDescriptionTextOnMinicard() {
|
||||||
return this.currentBoard.allowsDescriptionTextOnMinicard;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsDescriptionTextOnMinicard : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsCoverAttachmentOnMinicard() {
|
allowsCoverAttachmentOnMinicard() {
|
||||||
return this.currentBoard.allowsCoverAttachmentOnMinicard;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsCoverAttachmentOnMinicard : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsBadgeAttachmentOnMinicard() {
|
allowsBadgeAttachmentOnMinicard() {
|
||||||
return this.currentBoard.allowsBadgeAttachmentOnMinicard;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsBadgeAttachmentOnMinicard : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsCardSortingByNumberOnMinicard() {
|
allowsCardSortingByNumberOnMinicard() {
|
||||||
return this.currentBoard.allowsCardSortingByNumberOnMinicard;
|
const boardId = Session.get('currentBoard');
|
||||||
|
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||||
|
return currentBoard ? currentBoard.allowsCardSortingByNumberOnMinicard : false;
|
||||||
},
|
},
|
||||||
|
|
||||||
boards() {
|
boards() {
|
||||||
|
|
@ -1098,203 +1182,73 @@ BlazeComponent.extendComponent({
|
||||||
{
|
{
|
||||||
'click .js-field-has-receiveddate'(evt) {
|
'click .js-field-has-receiveddate'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.currentBoard.allowsReceivedDate = !this.currentBoard
|
const newValue = !this.currentBoard.allowsReceivedDate;
|
||||||
.allowsReceivedDate;
|
Boards.update(this.currentBoard._id, { $set: { allowsReceivedDate: newValue } });
|
||||||
this.currentBoard.setAllowsReceivedDate(
|
|
||||||
this.currentBoard.allowsReceivedDate,
|
|
||||||
);
|
|
||||||
$(`.js-field-has-receiveddate ${MCB}`).toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsReceivedDate,
|
|
||||||
);
|
|
||||||
$('.js-field-has-receiveddate').toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsReceivedDate,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
'click .js-field-has-startdate'(evt) {
|
'click .js-field-has-startdate'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.currentBoard.allowsStartDate = !this.currentBoard
|
const newValue = !this.currentBoard.allowsStartDate;
|
||||||
.allowsStartDate;
|
Boards.update(this.currentBoard._id, { $set: { allowsStartDate: newValue } });
|
||||||
this.currentBoard.setAllowsStartDate(
|
|
||||||
this.currentBoard.allowsStartDate,
|
|
||||||
);
|
|
||||||
$(`.js-field-has-startdate ${MCB}`).toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsStartDate,
|
|
||||||
);
|
|
||||||
$('.js-field-has-startdate').toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsStartDate,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
'click .js-field-has-enddate'(evt) {
|
'click .js-field-has-enddate'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.currentBoard.allowsEndDate = !this.currentBoard.allowsEndDate;
|
const newValue = !this.currentBoard.allowsEndDate;
|
||||||
this.currentBoard.setAllowsEndDate(this.currentBoard.allowsEndDate);
|
Boards.update(this.currentBoard._id, { $set: { allowsEndDate: newValue } });
|
||||||
$(`.js-field-has-enddate ${MCB}`).toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsEndDate,
|
|
||||||
);
|
|
||||||
$('.js-field-has-enddate').toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsEndDate,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
'click .js-field-has-duedate'(evt) {
|
'click .js-field-has-duedate'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.currentBoard.allowsDueDate = !this.currentBoard.allowsDueDate;
|
const newValue = !this.currentBoard.allowsDueDate;
|
||||||
this.currentBoard.setAllowsDueDate(this.currentBoard.allowsDueDate);
|
Boards.update(this.currentBoard._id, { $set: { allowsDueDate: newValue } });
|
||||||
$(`.js-field-has-duedate ${MCB}`).toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsDueDate,
|
|
||||||
);
|
|
||||||
$('.js-field-has-duedate').toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsDueDate,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
'click .js-field-has-subtasks'(evt) {
|
'click .js-field-has-subtasks'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks;
|
const newValue = !this.currentBoard.allowsSubtasks;
|
||||||
this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks);
|
Boards.update(this.currentBoard._id, { $set: { allowsSubtasks: newValue } });
|
||||||
$(`.js-field-has-subtasks ${MCB}`).toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsSubtasks,
|
|
||||||
);
|
|
||||||
$('.js-field-has-subtasks').toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsSubtasks,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
'click .js-field-has-creator'(evt) {
|
'click .js-field-has-creator'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.currentBoard.allowsCreator = !this.currentBoard.allowsCreator;
|
const newValue = !this.currentBoard.allowsCreator;
|
||||||
this.currentBoard.setAllowsCreator(this.currentBoard.allowsCreator);
|
Boards.update(this.currentBoard._id, { $set: { allowsCreator: newValue } });
|
||||||
$(`.js-field-has-creator ${MCB}`).toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsCreator,
|
|
||||||
);
|
|
||||||
$('.js-field-has-creator').toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsCreator,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
'click .js-field-has-creator-on-minicard'(evt) {
|
'click .js-field-has-creator-on-minicard'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.currentBoard.allowsCreatorOnMinicard = !this.currentBoard.allowsCreatorOnMinicard;
|
const newValue = !this.currentBoard.allowsCreatorOnMinicard;
|
||||||
this.currentBoard.setAllowsCreatorOnMinicard(this.currentBoard.allowsCreatorOnMinicard);
|
Boards.update(this.currentBoard._id, { $set: { allowsCreatorOnMinicard: newValue } });
|
||||||
$(`.js-field-has-creator-on-minicard ${MCB}`).toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsCreatorOnMinicard,
|
|
||||||
);
|
|
||||||
$('.js-field-has-creator-on-minicard').toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsCreatorOnMinicard,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
'click .js-field-has-members'(evt) {
|
'click .js-field-has-members'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.currentBoard.allowsMembers = !this.currentBoard.allowsMembers;
|
const newValue = !this.currentBoard.allowsMembers;
|
||||||
this.currentBoard.setAllowsMembers(this.currentBoard.allowsMembers);
|
Boards.update(this.currentBoard._id, { $set: { allowsMembers: newValue } });
|
||||||
$(`.js-field-has-members ${MCB}`).toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsMembers,
|
|
||||||
);
|
|
||||||
$('.js-field-has-members').toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsMembers,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
'click .js-field-has-assignee'(evt) {
|
'click .js-field-has-assignee'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.currentBoard.allowsAssignee = !this.currentBoard.allowsAssignee;
|
const newValue = !this.currentBoard.allowsAssignee;
|
||||||
this.currentBoard.setAllowsAssignee(this.currentBoard.allowsAssignee);
|
Boards.update(this.currentBoard._id, { $set: { allowsAssignee: newValue } });
|
||||||
$(`.js-field-has-assignee ${MCB}`).toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsAssignee,
|
|
||||||
);
|
|
||||||
$('.js-field-has-assignee').toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsAssignee,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
'click .js-field-has-assigned-by'(evt) {
|
'click .js-field-has-assigned-by'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.currentBoard.allowsAssignedBy = !this.currentBoard
|
const newValue = !this.currentBoard.allowsAssignedBy;
|
||||||
.allowsAssignedBy;
|
Boards.update(this.currentBoard._id, { $set: { allowsAssignedBy: newValue } });
|
||||||
this.currentBoard.setAllowsAssignedBy(
|
|
||||||
this.currentBoard.allowsAssignedBy,
|
|
||||||
);
|
|
||||||
$(`.js-field-has-assigned-by ${MCB}`).toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsAssignedBy,
|
|
||||||
);
|
|
||||||
$('.js-field-has-assigned-by').toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsAssignedBy,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
'click .js-field-has-requested-by'(evt) {
|
'click .js-field-has-requested-by'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.currentBoard.allowsRequestedBy = !this.currentBoard
|
const newValue = !this.currentBoard.allowsRequestedBy;
|
||||||
.allowsRequestedBy;
|
Boards.update(this.currentBoard._id, { $set: { allowsRequestedBy: newValue } });
|
||||||
this.currentBoard.setAllowsRequestedBy(
|
|
||||||
this.currentBoard.allowsRequestedBy,
|
|
||||||
);
|
|
||||||
$(`.js-field-has-requested-by ${MCB}`).toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsRequestedBy,
|
|
||||||
);
|
|
||||||
$('.js-field-has-requested-by').toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsRequestedBy,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
'click .js-field-has-card-sorting-by-number'(evt) {
|
'click .js-field-has-card-sorting-by-number'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.currentBoard.allowsCardSortingByNumber = !this.currentBoard
|
const newValue = !this.currentBoard.allowsCardSortingByNumber;
|
||||||
.allowsCardSortingByNumber;
|
Boards.update(this.currentBoard._id, { $set: { allowsCardSortingByNumber: newValue } });
|
||||||
this.currentBoard.setAllowsCardSortingByNumber(
|
|
||||||
this.currentBoard.allowsCardSortingByNumber,
|
|
||||||
);
|
|
||||||
$(`.js-field-has-card-sorting-by-number ${MCB}`).toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsCardSortingByNumber,
|
|
||||||
);
|
|
||||||
$('.js-field-has-card-sorting-by-number').toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsCardSortingByNumber,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
'click .js-field-has-card-show-lists'(evt) {
|
'click .js-field-has-card-show-lists'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.currentBoard.allowsShowLists = !this.currentBoard
|
const newValue = !this.currentBoard.allowsShowLists;
|
||||||
.allowsShowLists;
|
Boards.update(this.currentBoard._id, { $set: { allowsShowLists: newValue } });
|
||||||
this.currentBoard.setAllowsShowLists(
|
|
||||||
this.currentBoard.allowsShowLists,
|
|
||||||
);
|
|
||||||
$(`.js-field-has-card-show-lists ${MCB}`).toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsShowLists,
|
|
||||||
);
|
|
||||||
$('.js-field-has-card-show-lists').toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsShowLists,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
'click .js-field-has-labels'(evt) {
|
'click .js-field-has-labels'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
this.currentBoard.allowsLabels = !this.currentBoard.allowsLabels;
|
const newValue = !this.currentBoard.allowsLabels;
|
||||||
this.currentBoard.setAllowsLabels(this.currentBoard.allowsLabels);
|
Boards.update(this.currentBoard._id, { $set: { allowsLabels: newValue } });
|
||||||
$(`.js-field-has-labels ${MCB}`).toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsLabels,
|
|
||||||
);
|
|
||||||
$('.js-field-has-labels').toggleClass(
|
|
||||||
CKCLS,
|
|
||||||
this.currentBoard.allowsLabels,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
'click .js-field-has-card-show-lists-on-minicard'(evt) {
|
'click .js-field-has-card-show-lists-on-minicard'(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
@ -1490,19 +1444,27 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
}).register('boardCardSettingsPopup');
|
}).register('boardCardSettingsPopup');
|
||||||
|
|
||||||
|
// Use Session variables instead of global ReactiveVars
|
||||||
|
Session.setDefault('addMemberPopup.searchResults', []);
|
||||||
|
Session.setDefault('addMemberPopup.searching', false);
|
||||||
|
Session.setDefault('addMemberPopup.noResults', false);
|
||||||
|
Session.setDefault('addMemberPopup.loading', false);
|
||||||
|
Session.setDefault('addMemberPopup.error', '');
|
||||||
|
|
||||||
|
|
||||||
BlazeComponent.extendComponent({
|
BlazeComponent.extendComponent({
|
||||||
onCreated() {
|
onCreated() {
|
||||||
this.error = new ReactiveVar('');
|
// Use Session variables
|
||||||
this.loading = new ReactiveVar(false);
|
this.searchTimeout = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
onRendered() {
|
onRendered() {
|
||||||
this.find('.js-search-member input').focus();
|
this.find('.js-search-member-input').focus();
|
||||||
this.setLoading(false);
|
this.setLoading(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
isBoardMember() {
|
isBoardMember() {
|
||||||
const userId = this.currentData().__originalId;
|
const userId = this.currentData()._id;
|
||||||
const user = ReactiveCache.getUser(userId);
|
const user = ReactiveCache.getUser(userId);
|
||||||
return user && user.isBoardMember();
|
return user && user.isBoardMember();
|
||||||
},
|
},
|
||||||
|
|
@ -1512,15 +1474,35 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
setError(error) {
|
setError(error) {
|
||||||
this.error.set(error);
|
Session.set('addMemberPopup.error', error);
|
||||||
},
|
},
|
||||||
|
|
||||||
setLoading(w) {
|
setLoading(w) {
|
||||||
this.loading.set(w);
|
Session.set('addMemberPopup.loading', w);
|
||||||
},
|
},
|
||||||
|
|
||||||
isLoading() {
|
isLoading() {
|
||||||
return this.loading.get();
|
return Session.get('addMemberPopup.loading');
|
||||||
|
},
|
||||||
|
|
||||||
|
performSearch(query) {
|
||||||
|
if (!query || query.length < 2) {
|
||||||
|
Session.set('addMemberPopup.searchResults', []);
|
||||||
|
Session.set('addMemberPopup.noResults', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Session.set('addMemberPopup.searching', true);
|
||||||
|
Session.set('addMemberPopup.noResults', false);
|
||||||
|
|
||||||
|
// Use the fallback search
|
||||||
|
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
|
||||||
|
Session.set('addMemberPopup.searchResults', results);
|
||||||
|
Session.set('addMemberPopup.searching', false);
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
Session.set('addMemberPopup.noResults', true);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
inviteUser(idNameEmail) {
|
inviteUser(idNameEmail) {
|
||||||
|
|
@ -1538,18 +1520,30 @@ BlazeComponent.extendComponent({
|
||||||
events() {
|
events() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'keyup input'() {
|
'keyup .js-search-member-input'(event) {
|
||||||
this.setError('');
|
this.setError('');
|
||||||
|
const query = event.target.value.trim();
|
||||||
|
this.searchQuery.set(query);
|
||||||
|
|
||||||
|
// Clear previous timeout
|
||||||
|
if (this.searchTimeout) {
|
||||||
|
clearTimeout(this.searchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce search
|
||||||
|
this.searchTimeout = setTimeout(() => {
|
||||||
|
this.performSearch(query);
|
||||||
|
}, 300);
|
||||||
},
|
},
|
||||||
'click .js-select-member'() {
|
'click .js-select-member'() {
|
||||||
const userId = this.currentData().__originalId;
|
const userId = this.currentData()._id;
|
||||||
const currentBoard = Utils.getCurrentBoard();
|
const currentBoard = Utils.getCurrentBoard();
|
||||||
if (!currentBoard.hasMember(userId)) {
|
if (!currentBoard.hasMember(userId)) {
|
||||||
this.inviteUser(userId);
|
this.inviteUser(userId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'click .js-email-invite'() {
|
'click .js-email-invite'() {
|
||||||
const idNameEmail = $('.js-search-member input').val();
|
const idNameEmail = $('.js-search-member-input').val();
|
||||||
if (idNameEmail.indexOf('@') < 0 || this.isValidEmail(idNameEmail)) {
|
if (idNameEmail.indexOf('@') < 0 || this.isValidEmail(idNameEmail)) {
|
||||||
this.inviteUser(idNameEmail);
|
this.inviteUser(idNameEmail);
|
||||||
} else this.setError('email-invalid');
|
} else this.setError('email-invalid');
|
||||||
|
|
@ -1560,7 +1554,35 @@ BlazeComponent.extendComponent({
|
||||||
}).register('addMemberPopup');
|
}).register('addMemberPopup');
|
||||||
|
|
||||||
Template.addMemberPopup.helpers({
|
Template.addMemberPopup.helpers({
|
||||||
searchIndex: () => UserSearchIndex,
|
searchResults() {
|
||||||
|
const results = Session.get('addMemberPopup.searchResults');
|
||||||
|
console.log('searchResults helper called, returning:', results);
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
searching() {
|
||||||
|
return Session.get('addMemberPopup.searching');
|
||||||
|
},
|
||||||
|
noResults() {
|
||||||
|
return Session.get('addMemberPopup.noResults');
|
||||||
|
},
|
||||||
|
loading() {
|
||||||
|
return Session.get('addMemberPopup.loading');
|
||||||
|
},
|
||||||
|
error() {
|
||||||
|
return Session.get('addMemberPopup.error');
|
||||||
|
},
|
||||||
|
isBoardMember() {
|
||||||
|
const userId = this._id;
|
||||||
|
const user = ReactiveCache.getUser(userId);
|
||||||
|
return user && user.isBoardMember();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Template.addMemberPopupTest.helpers({
|
||||||
|
searchResults() {
|
||||||
|
console.log('addMemberPopupTest searchResults helper called');
|
||||||
|
return Session.get('addMemberPopup.searchResults') || [];
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
BlazeComponent.extendComponent({
|
BlazeComponent.extendComponent({
|
||||||
|
|
|
||||||
|
|
@ -95,3 +95,7 @@ template(name="createCustomFieldPopup")
|
||||||
template(name="deleteCustomFieldPopup")
|
template(name="deleteCustomFieldPopup")
|
||||||
p {{_ "custom-field-delete-pop"}}
|
p {{_ "custom-field-delete-pop"}}
|
||||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||||
|
|
||||||
|
// Reuse the create form for editing to satisfy popup template lookup
|
||||||
|
template(name="editCustomFieldPopup")
|
||||||
|
+Template.dynamic(template="createCustomFieldPopup")
|
||||||
|
|
|
||||||
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');
|
||||||
|
|
@ -23,27 +23,28 @@ template(name="swimlaneFixedHeader")
|
||||||
+viewer
|
+viewer
|
||||||
| {{isTitleDefault title}}
|
| {{isTitleDefault title}}
|
||||||
.swimlane-header-menu
|
.swimlane-header-menu
|
||||||
unless currentUser.isCommentOnly
|
if currentUser
|
||||||
a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
|
unless currentUser.isCommentOnly
|
||||||
| ➕
|
unless currentUser.isWorker
|
||||||
a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
|
a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
|
||||||
| ☰
|
| ➕
|
||||||
//// TODO: Collapse Swimlane: make button working, etc.
|
a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
|
||||||
//unless collapsed
|
| ☰
|
||||||
// a.js-collapse-swimlane(title="{{_ 'collapse'}}")
|
//// TODO: Collapse Swimlane: make button working, etc.
|
||||||
// i.fa.fa-arrow-down.swimlane-header-collapse-down
|
//unless collapsed
|
||||||
// ⬆️.swimlane-header-collapse-up
|
// a.js-collapse-swimlane(title="{{_ 'collapse'}}")
|
||||||
//if collapsed
|
// i.fa.fa-arrow-down.swimlane-header-collapse-down
|
||||||
// a.js-collapse-swimlane(title="{{_ 'uncollapse'}}")
|
// ⬆️.swimlane-header-collapse-up
|
||||||
// ⬆️.swimlane-header-collapse-up
|
//if collapsed
|
||||||
// i.fa.fa-arrow-down.swimlane-header-collapse-down
|
// a.js-collapse-swimlane(title="{{_ 'uncollapse'}}")
|
||||||
unless isTouchScreen
|
// ⬆️.swimlane-header-collapse-up
|
||||||
if isShowDesktopDragHandles
|
// i.fa.fa-arrow-down.swimlane-header-collapse-down
|
||||||
a.swimlane-header-handle.handle.js-swimlane-header-handle
|
unless isTouchScreen
|
||||||
| ↕️
|
a.swimlane-header-handle.handle.js-swimlane-header-handle
|
||||||
if isTouchScreen
|
| ↕️
|
||||||
a.swimlane-header-miniscreen-handle.handle.js-swimlane-header-handle
|
if isTouchScreen
|
||||||
| ↕️
|
a.swimlane-header-miniscreen-handle.handle.js-swimlane-header-handle
|
||||||
|
| ↕️
|
||||||
|
|
||||||
template(name="editSwimlaneTitleForm")
|
template(name="editSwimlaneTitleForm")
|
||||||
.list-composer
|
.list-composer
|
||||||
|
|
@ -54,44 +55,46 @@ template(name="editSwimlaneTitleForm")
|
||||||
| ❌
|
| ❌
|
||||||
|
|
||||||
template(name="swimlaneActionPopup")
|
template(name="swimlaneActionPopup")
|
||||||
unless currentUser.isCommentOnly
|
if currentUser
|
||||||
ul.pop-over-list
|
unless currentUser.isCommentOnly
|
||||||
if currentUser.isBoardAdmin
|
ul.pop-over-list
|
||||||
li: a.js-set-swimlane-color
|
if currentUser.isBoardAdmin
|
||||||
| 🎨
|
li: a.js-set-swimlane-color
|
||||||
| {{_ 'select-color'}}
|
| 🎨
|
||||||
li: a.js-set-swimlane-height
|
| {{_ 'select-color'}}
|
||||||
| ↕️
|
li: a.js-set-swimlane-height
|
||||||
| {{_ 'set-swimlane-height'}}
|
| ↕️
|
||||||
if currentUser.isBoardAdmin
|
| {{_ 'set-swimlane-height'}}
|
||||||
unless this.isTemplateContainer
|
if currentUser.isBoardAdmin
|
||||||
hr
|
unless this.isTemplateContainer
|
||||||
ul.pop-over-list
|
hr
|
||||||
li: a.js-close-swimlane
|
ul.pop-over-list
|
||||||
| ▶️
|
li: a.js-close-swimlane
|
||||||
| 📦
|
| ▶️
|
||||||
| {{_ 'archive-swimlane'}}
|
| 📦
|
||||||
ul.pop-over-list
|
| {{_ 'archive-swimlane'}}
|
||||||
li: a.js-copy-swimlane
|
ul.pop-over-list
|
||||||
| 📋
|
li: a.js-copy-swimlane
|
||||||
| {{_ 'copy-swimlane'}}
|
| 📋
|
||||||
ul.pop-over-list
|
| {{_ 'copy-swimlane'}}
|
||||||
li: a.js-move-swimlane
|
ul.pop-over-list
|
||||||
| ⬆️
|
li: a.js-move-swimlane
|
||||||
| {{_ 'move-swimlane'}}
|
| ⬆️
|
||||||
|
| {{_ 'move-swimlane'}}
|
||||||
|
|
||||||
template(name="swimlaneAddPopup")
|
template(name="swimlaneAddPopup")
|
||||||
unless currentUser.isCommentOnly
|
if currentUser
|
||||||
form
|
unless currentUser.isCommentOnly
|
||||||
input.swimlane-name-input.full-line(type="text" placeholder="{{_ 'add-swimlane'}}"
|
form
|
||||||
autocomplete="off" autofocus)
|
input.swimlane-name-input.full-line(type="text" placeholder="{{_ 'add-swimlane'}}"
|
||||||
.edit-controls.clearfix
|
autocomplete="off" autofocus)
|
||||||
button.primary.confirm(type="submit") {{_ 'add'}}
|
.edit-controls.clearfix
|
||||||
unless currentBoard.isTemplatesBoard
|
button.primary.confirm(type="submit") {{_ 'add'}}
|
||||||
unless currentBoard.isTemplateBoard
|
unless currentBoard.isTemplatesBoard
|
||||||
span.quiet
|
unless currentBoard.isTemplateBoard
|
||||||
| {{_ 'or'}}
|
span.quiet
|
||||||
a.js-swimlane-template {{_ 'template'}}
|
| {{_ 'or'}}
|
||||||
|
a.js-swimlane-template {{_ 'template'}}
|
||||||
|
|
||||||
template(name="setSwimlaneColorPopup")
|
template(name="setSwimlaneColorPopup")
|
||||||
form.edit-label.swimlane-color-popup
|
form.edit-label.swimlane-color-popup
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,11 @@ BlazeComponent.extendComponent({
|
||||||
events() {
|
events() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
'submit form'(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.currentSwimlane.setColor(this.currentColor.get());
|
||||||
|
Popup.back();
|
||||||
|
},
|
||||||
'click .js-palette-color'() {
|
'click .js-palette-color'() {
|
||||||
this.currentColor.set(this.currentData().color);
|
this.currentColor.set(this.currentData().color);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
left: 87vw;
|
right: 10px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
|
|
|
||||||
|
|
@ -72,21 +72,23 @@ template(name="addListForm")
|
||||||
| ➕
|
| ➕
|
||||||
|
|
||||||
template(name="moveSwimlanePopup")
|
template(name="moveSwimlanePopup")
|
||||||
unless currentUser.isWorker
|
if currentUser
|
||||||
label {{_ 'boards'}}:
|
unless currentUser.isWorker
|
||||||
select.js-select-boards(autofocus)
|
label {{_ 'boards'}}:
|
||||||
each toBoard in toBoards
|
select.js-select-boards(autofocus)
|
||||||
option(value="{{toBoard._id}}") {{toBoard.title}}
|
each toBoard in toBoards
|
||||||
|
option(value="{{toBoard._id}}") {{toBoard.title}}
|
||||||
|
|
||||||
.edit-controls.clearfix
|
.edit-controls.clearfix
|
||||||
button.primary.confirm.js-done {{_ 'done'}}
|
button.primary.confirm.js-done {{_ 'done'}}
|
||||||
|
|
||||||
template(name="copySwimlanePopup")
|
template(name="copySwimlanePopup")
|
||||||
unless currentUser.isWorker
|
if currentUser
|
||||||
label {{_ 'boards'}}:
|
unless currentUser.isWorker
|
||||||
select.js-select-boards(autofocus)
|
label {{_ 'boards'}}:
|
||||||
each toBoard in toBoards
|
select.js-select-boards(autofocus)
|
||||||
option(value="{{toBoard._id}}" selected="{{#if $eq toBoard.title board.title}}1{{/if}}") {{toBoard.title}}
|
each toBoard in toBoards
|
||||||
|
option(value="{{toBoard._id}}" selected="{{#if $eq toBoard.title board.title}}1{{/if}}") {{toBoard.title}}
|
||||||
|
|
||||||
.edit-controls.clearfix
|
.edit-controls.clearfix
|
||||||
button.primary.confirm.js-done {{_ 'done'}}
|
button.primary.confirm.js-done {{_ 'done'}}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { ReactiveCache } from '/imports/reactiveCache';
|
import { ReactiveCache } from '/imports/reactiveCache';
|
||||||
|
import dragscroll from '@wekanteam/dragscroll';
|
||||||
const { calculateIndex } = Utils;
|
const { calculateIndex } = Utils;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function currentListIsInThisSwimlane(swimlaneId) {
|
function currentListIsInThisSwimlane(swimlaneId) {
|
||||||
const currentList = Utils.getCurrentList();
|
const currentList = Utils.getCurrentList();
|
||||||
return (
|
return (
|
||||||
|
|
@ -43,6 +46,18 @@ function currentCardIsInThisList(listId, swimlaneId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function initSortable(boardComponent, $listsDom) {
|
function initSortable(boardComponent, $listsDom) {
|
||||||
|
// Safety check: ensure we have valid DOM elements
|
||||||
|
if (!$listsDom || $listsDom.length === 0) {
|
||||||
|
console.error('initSortable: No valid DOM elements provided');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if sortable is already initialized
|
||||||
|
if ($listsDom.data('uiSortable') || $listsDom.data('sortable')) {
|
||||||
|
$listsDom.sortable('destroy');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// We want to animate the card details window closing. We rely on CSS
|
// We want to animate the card details window closing. We rely on CSS
|
||||||
// transition for the actual animation.
|
// transition for the actual animation.
|
||||||
$listsDom._uihooks = {
|
$listsDom._uihooks = {
|
||||||
|
|
@ -62,18 +77,68 @@ function initSortable(boardComponent, $listsDom) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
$listsDom.sortable({
|
|
||||||
connectWith: '.js-swimlane, .js-lists',
|
// Add click debugging for drag handles
|
||||||
tolerance: 'pointer',
|
$listsDom.on('mousedown', '.js-list-handle', function(e) {
|
||||||
helper: 'clone',
|
e.stopPropagation();
|
||||||
items: '.js-list:not(.js-list-composer)',
|
});
|
||||||
placeholder: 'js-list placeholder',
|
|
||||||
distance: 7,
|
$listsDom.on('mousedown', '.js-list-header', function(e) {
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add debugging for any mousedown on lists
|
||||||
|
$listsDom.on('mousedown', '.js-list', function(e) {
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add debugging for sortable events
|
||||||
|
$listsDom.on('sortstart', function(e, ui) {
|
||||||
|
});
|
||||||
|
|
||||||
|
$listsDom.on('sortbeforestop', function(e, ui) {
|
||||||
|
});
|
||||||
|
|
||||||
|
$listsDom.on('sortstop', function(e, ui) {
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
$listsDom.sortable({
|
||||||
|
connectWith: '.js-swimlane, .js-lists',
|
||||||
|
tolerance: 'pointer',
|
||||||
|
appendTo: '.board-canvas',
|
||||||
|
helper(evt, item) {
|
||||||
|
const helper = item.clone();
|
||||||
|
helper.css('z-index', 1000);
|
||||||
|
return helper;
|
||||||
|
},
|
||||||
|
items: '.js-list:not(.js-list-composer)',
|
||||||
|
placeholder: 'list placeholder',
|
||||||
|
distance: 3,
|
||||||
|
forcePlaceholderSize: true,
|
||||||
|
cursor: 'move',
|
||||||
start(evt, ui) {
|
start(evt, ui) {
|
||||||
|
ui.helper.css('z-index', 1000);
|
||||||
ui.placeholder.height(ui.helper.height());
|
ui.placeholder.height(ui.helper.height());
|
||||||
ui.placeholder.width(ui.helper.width());
|
ui.placeholder.width(ui.helper.width());
|
||||||
EscapeActions.executeUpTo('popup-close');
|
EscapeActions.executeUpTo('popup-close');
|
||||||
boardComponent.setIsDragging(true);
|
boardComponent.setIsDragging(true);
|
||||||
|
|
||||||
|
// Add visual feedback for list being dragged
|
||||||
|
ui.item.addClass('ui-sortable-helper');
|
||||||
|
|
||||||
|
// Disable dragscroll during list dragging to prevent interference
|
||||||
|
try {
|
||||||
|
dragscroll.reset();
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also disable dragscroll on all swimlanes during list dragging
|
||||||
|
$('.js-swimlane').each(function() {
|
||||||
|
$(this).removeClass('dragscroll');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeStop(evt, ui) {
|
||||||
|
// Clean up visual feedback
|
||||||
|
ui.item.removeClass('ui-sortable-helper');
|
||||||
},
|
},
|
||||||
stop(evt, ui) {
|
stop(evt, ui) {
|
||||||
// To attribute the new index number, we need to get the DOM element
|
// To attribute the new index number, we need to get the DOM element
|
||||||
|
|
@ -83,15 +148,37 @@ function initSortable(boardComponent, $listsDom) {
|
||||||
const sortIndex = calculateIndex(prevListDom, nextListDom, 1);
|
const sortIndex = calculateIndex(prevListDom, nextListDom, 1);
|
||||||
|
|
||||||
const listDomElement = ui.item.get(0);
|
const listDomElement = ui.item.get(0);
|
||||||
const list = Blaze.getData(listDomElement);
|
if (!listDomElement) {
|
||||||
|
console.error('List DOM element not found during drag stop');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let list;
|
||||||
|
try {
|
||||||
|
list = Blaze.getData(listDomElement);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting list data:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!list) {
|
||||||
|
console.error('List data not found for element:', listDomElement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Detect if the list was dropped in a different swimlane
|
// Detect if the list was dropped in a different swimlane
|
||||||
const targetSwimlaneDom = ui.item.closest('.js-swimlane');
|
const targetSwimlaneDom = ui.item.closest('.js-swimlane');
|
||||||
let targetSwimlaneId = null;
|
let targetSwimlaneId = null;
|
||||||
|
|
||||||
|
|
||||||
if (targetSwimlaneDom.length > 0) {
|
if (targetSwimlaneDom.length > 0) {
|
||||||
// List was dropped in a swimlane
|
// List was dropped in a swimlane
|
||||||
targetSwimlaneId = targetSwimlaneDom.attr('id').replace('swimlane-', '');
|
try {
|
||||||
|
targetSwimlaneId = targetSwimlaneDom.attr('id').replace('swimlane-', '');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting target swimlane ID:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// List was dropped in lists view (not swimlanes view)
|
// List was dropped in lists view (not swimlanes view)
|
||||||
// In this case, assign to the default swimlane
|
// In this case, assign to the default swimlane
|
||||||
|
|
@ -127,9 +214,6 @@ function initSortable(boardComponent, $listsDom) {
|
||||||
// If the list was dropped in a different swimlane, update the swimlaneId
|
// If the list was dropped in a different swimlane, update the swimlaneId
|
||||||
if (isDifferentSwimlane) {
|
if (isDifferentSwimlane) {
|
||||||
updateData.swimlaneId = targetSwimlaneId;
|
updateData.swimlaneId = targetSwimlaneId;
|
||||||
if (process.env.DEBUG === 'true') {
|
|
||||||
console.log(`Moving list "${list.title}" from swimlane ${originalSwimlaneId} to swimlane ${targetSwimlaneId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move all cards in the list to the new swimlane
|
// Move all cards in the list to the new swimlane
|
||||||
const cardsInList = ReactiveCache.getCards({
|
const cardsInList = ReactiveCache.getCards({
|
||||||
|
|
@ -141,59 +225,103 @@ function initSortable(boardComponent, $listsDom) {
|
||||||
card.move(list.boardId, targetSwimlaneId, list._id);
|
card.move(list.boardId, targetSwimlaneId, list._id);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (process.env.DEBUG === 'true') {
|
|
||||||
console.log(`Moved ${cardsInList.length} cards to swimlane ${targetSwimlaneId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't cancel the sortable when moving to a different swimlane
|
// Don't cancel the sortable when moving to a different swimlane
|
||||||
// The DOM move should be allowed to complete
|
// The DOM move should be allowed to complete
|
||||||
} else {
|
}
|
||||||
// If staying in the same swimlane, cancel the sortable to prevent DOM manipulation issues
|
// Allow reordering within the same swimlane by not canceling the sortable
|
||||||
$listsDom.sortable('cancel');
|
|
||||||
|
try {
|
||||||
|
Lists.update(list._id, {
|
||||||
|
$set: updateData,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating list:', error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Lists.update(list._id, {
|
|
||||||
$set: updateData,
|
|
||||||
});
|
|
||||||
|
|
||||||
boardComponent.setIsDragging(false);
|
boardComponent.setIsDragging(false);
|
||||||
|
|
||||||
|
// Re-enable dragscroll after list dragging is complete
|
||||||
|
try {
|
||||||
|
dragscroll.reset();
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-enable dragscroll on all swimlanes
|
||||||
|
$('.js-swimlane').each(function() {
|
||||||
|
$(this).addClass('dragscroll');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing list sortable:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Check if drag handles exist
|
||||||
|
const dragHandles = $listsDom.find('.js-list-handle');
|
||||||
|
|
||||||
|
// Check if lists exist
|
||||||
|
const lists = $listsDom.find('.js-list');
|
||||||
|
|
||||||
boardComponent.autorun(() => {
|
// Skip the complex autorun and options for now
|
||||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
|
||||||
$listsDom.sortable({
|
|
||||||
handle: '.js-list-handle',
|
|
||||||
connectWith: '.js-swimlane, .js-lists',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$listsDom.sortable({
|
|
||||||
handle: '.js-list-header',
|
|
||||||
connectWith: '.js-swimlane, .js-lists',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const $listDom = $listsDom;
|
|
||||||
if ($listDom.data('uiSortable') || $listDom.data('sortable')) {
|
|
||||||
$listsDom.sortable(
|
|
||||||
'option',
|
|
||||||
'disabled',
|
|
||||||
!ReactiveCache.getCurrentUser()?.isBoardAdmin(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BlazeComponent.extendComponent({
|
BlazeComponent.extendComponent({
|
||||||
onRendered() {
|
onRendered() {
|
||||||
const boardComponent = this.parentComponent();
|
const boardComponent = this.parentComponent();
|
||||||
const $listsDom = this.$('.js-lists');
|
const $listsDom = this.$('.js-lists');
|
||||||
|
|
||||||
|
|
||||||
if (!Utils.getCurrentCardId()) {
|
if (!Utils.getCurrentCardId()) {
|
||||||
boardComponent.scrollLeft();
|
boardComponent.scrollLeft();
|
||||||
}
|
}
|
||||||
|
|
||||||
initSortable(boardComponent, $listsDom);
|
// Try a simpler approach - initialize sortable directly like cards do
|
||||||
|
|
||||||
|
// Wait for DOM to be ready
|
||||||
|
setTimeout(() => {
|
||||||
|
const $lists = this.$('.js-list');
|
||||||
|
|
||||||
|
const $parent = $lists.parent();
|
||||||
|
|
||||||
|
if ($lists.length > 0) {
|
||||||
|
|
||||||
|
// Check for drag handles
|
||||||
|
const $handles = $parent.find('.js-list-handle');
|
||||||
|
|
||||||
|
// Test if drag handles are clickable
|
||||||
|
$handles.on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
$parent.sortable({
|
||||||
|
connectWith: '.js-swimlane, .js-lists',
|
||||||
|
tolerance: 'pointer',
|
||||||
|
appendTo: '.board-canvas',
|
||||||
|
helper: 'clone',
|
||||||
|
items: '.js-list:not(.js-list-composer)',
|
||||||
|
placeholder: 'list placeholder',
|
||||||
|
distance: 7,
|
||||||
|
handle: '.js-list-handle',
|
||||||
|
disabled: !Utils.canModifyBoard(),
|
||||||
|
start(evt, ui) {
|
||||||
|
ui.helper.css('z-index', 1000);
|
||||||
|
ui.placeholder.height(ui.helper.height());
|
||||||
|
ui.placeholder.width(ui.helper.width());
|
||||||
|
EscapeActions.executeUpTo('popup-close');
|
||||||
|
boardComponent.setIsDragging(true);
|
||||||
|
},
|
||||||
|
stop(evt, ui) {
|
||||||
|
boardComponent.setIsDragging(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
},
|
},
|
||||||
onCreated() {
|
onCreated() {
|
||||||
this.draggingActive = new ReactiveVar(false);
|
this.draggingActive = new ReactiveVar(false);
|
||||||
|
|
@ -256,7 +384,6 @@ BlazeComponent.extendComponent({
|
||||||
const isInNoDragArea = $(evt.target).closest(noDragInside.join(',')).length > 0;
|
const isInNoDragArea = $(evt.target).closest(noDragInside.join(',')).length > 0;
|
||||||
|
|
||||||
if (isResizeHandle) {
|
if (isResizeHandle) {
|
||||||
console.log('Board drag prevented - resize handle clicked');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -294,7 +421,31 @@ BlazeComponent.extendComponent({
|
||||||
swimlaneHeight() {
|
swimlaneHeight() {
|
||||||
const user = ReactiveCache.getCurrentUser();
|
const user = ReactiveCache.getCurrentUser();
|
||||||
const swimlane = Template.currentData();
|
const swimlane = Template.currentData();
|
||||||
const height = user.getSwimlaneHeightFromStorage(swimlane.boardId, swimlane._id);
|
|
||||||
|
let height;
|
||||||
|
if (user) {
|
||||||
|
// For logged-in users, get from user profile
|
||||||
|
height = user.getSwimlaneHeightFromStorage(swimlane.boardId, swimlane._id);
|
||||||
|
} else {
|
||||||
|
// For non-logged-in users, get from localStorage
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('wekan-swimlane-heights');
|
||||||
|
if (stored) {
|
||||||
|
const heights = JSON.parse(stored);
|
||||||
|
if (heights[swimlane.boardId] && heights[swimlane.boardId][swimlane._id]) {
|
||||||
|
height = heights[swimlane.boardId][swimlane._id];
|
||||||
|
} else {
|
||||||
|
height = -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
height = -1;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error reading swimlane height from localStorage:', e);
|
||||||
|
height = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return height == -1 ? "auto" : (height + 5 + "px");
|
return height == -1 ? "auto" : (height + 5 + "px");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -408,15 +559,36 @@ BlazeComponent.extendComponent({
|
||||||
if (process.env.DEBUG === 'true') {
|
if (process.env.DEBUG === 'true') {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the new storage method that handles both logged-in and non-logged-in users
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
Meteor.call('applySwimlaneHeightToStorage', boardId, swimlaneId, finalHeight, (error, result) => {
|
if (currentUser) {
|
||||||
if (error) {
|
// For logged-in users, use server method
|
||||||
console.error('Error saving swimlane height:', error);
|
Meteor.call('applySwimlaneHeightToStorage', boardId, swimlaneId, finalHeight, (error, result) => {
|
||||||
} else {
|
if (error) {
|
||||||
|
console.error('Error saving swimlane height:', error);
|
||||||
|
} else {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// For non-logged-in users, save to localStorage directly
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('wekan-swimlane-heights');
|
||||||
|
let heights = stored ? JSON.parse(stored) : {};
|
||||||
|
|
||||||
|
if (!heights[boardId]) {
|
||||||
|
heights[boardId] = {};
|
||||||
|
}
|
||||||
|
heights[boardId][swimlaneId] = finalHeight;
|
||||||
|
|
||||||
|
localStorage.setItem('wekan-swimlane-heights', JSON.stringify(heights));
|
||||||
|
|
||||||
if (process.env.DEBUG === 'true') {
|
if (process.env.DEBUG === 'true') {
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error saving swimlane height to localStorage:', e);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
@ -440,6 +612,7 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
}).register('swimlane');
|
}).register('swimlane');
|
||||||
|
|
||||||
|
|
||||||
BlazeComponent.extendComponent({
|
BlazeComponent.extendComponent({
|
||||||
onCreated() {
|
onCreated() {
|
||||||
this.currentBoard = Utils.getCurrentBoard();
|
this.currentBoard = Utils.getCurrentBoard();
|
||||||
|
|
@ -507,8 +680,305 @@ Template.swimlane.helpers({
|
||||||
canSeeAddList() {
|
canSeeAddList() {
|
||||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
lists() {
|
||||||
|
// Return per-swimlane lists for this swimlane
|
||||||
|
return this.myLists();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize sortable on DOM elements
|
||||||
|
setTimeout(() => {
|
||||||
|
const $swimlaneElements = $('.swimlane');
|
||||||
|
const $listsGroupElements = $('.list-group');
|
||||||
|
|
||||||
|
// Initialize sortable on ALL swimlane elements (even empty ones)
|
||||||
|
$swimlaneElements.each(function(index) {
|
||||||
|
const $swimlane = $(this);
|
||||||
|
const $lists = $swimlane.find('.js-list');
|
||||||
|
|
||||||
|
// Only initialize on swimlanes that have the .js-lists class (the container for lists)
|
||||||
|
if ($swimlane.hasClass('js-lists')) {
|
||||||
|
$swimlane.sortable({
|
||||||
|
connectWith: '.js-swimlane, .js-lists',
|
||||||
|
tolerance: 'pointer',
|
||||||
|
appendTo: '.board-canvas',
|
||||||
|
helper: 'clone',
|
||||||
|
items: '.js-list:not(.js-list-composer)',
|
||||||
|
placeholder: 'list placeholder',
|
||||||
|
distance: 7,
|
||||||
|
handle: '.js-list-handle',
|
||||||
|
disabled: !Utils.canModifyBoard(),
|
||||||
|
start(evt, ui) {
|
||||||
|
ui.helper.css('z-index', 1000);
|
||||||
|
ui.placeholder.height(ui.helper.height());
|
||||||
|
ui.placeholder.width(ui.helper.width());
|
||||||
|
EscapeActions.executeUpTo('popup-close');
|
||||||
|
// Try to get board component
|
||||||
|
try {
|
||||||
|
const boardComponent = BlazeComponent.getComponentForElement(ui.item[0]);
|
||||||
|
if (boardComponent && boardComponent.setIsDragging) {
|
||||||
|
boardComponent.setIsDragging(true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Silent fail
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stop(evt, ui) {
|
||||||
|
// To attribute the new index number, we need to get the DOM element
|
||||||
|
// of the previous and the following list -- if any.
|
||||||
|
const prevListDom = ui.item.prev('.js-list').get(0);
|
||||||
|
const nextListDom = ui.item.next('.js-list').get(0);
|
||||||
|
const sortIndex = calculateIndex(prevListDom, nextListDom, 1);
|
||||||
|
|
||||||
|
const listDomElement = ui.item.get(0);
|
||||||
|
if (!listDomElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let list;
|
||||||
|
try {
|
||||||
|
list = Blaze.getData(listDomElement);
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect if the list was dropped in a different swimlane
|
||||||
|
const targetSwimlaneDom = ui.item.closest('.js-swimlane');
|
||||||
|
let targetSwimlaneId = null;
|
||||||
|
|
||||||
|
if (targetSwimlaneDom.length > 0) {
|
||||||
|
// List was dropped in a swimlane
|
||||||
|
try {
|
||||||
|
targetSwimlaneId = targetSwimlaneDom.attr('id').replace('swimlane-', '');
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// List was dropped in lists view (not swimlanes view)
|
||||||
|
// In this case, assign to the default swimlane
|
||||||
|
const currentBoard = ReactiveCache.getBoard(Session.get('currentBoard'));
|
||||||
|
if (currentBoard) {
|
||||||
|
const defaultSwimlane = currentBoard.getDefaultSwimline();
|
||||||
|
if (defaultSwimlane) {
|
||||||
|
targetSwimlaneId = defaultSwimlane._id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the original swimlane ID of the list (handle backward compatibility)
|
||||||
|
const originalSwimlaneId = list.getEffectiveSwimlaneId ? list.getEffectiveSwimlaneId() : (list.swimlaneId || null);
|
||||||
|
|
||||||
|
// Prepare update object
|
||||||
|
const updateData = {
|
||||||
|
sort: sortIndex.base,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the list was dropped in a different swimlane
|
||||||
|
const isDifferentSwimlane = targetSwimlaneId && targetSwimlaneId !== originalSwimlaneId;
|
||||||
|
|
||||||
|
// If the list was dropped in a different swimlane, update the swimlaneId
|
||||||
|
if (isDifferentSwimlane) {
|
||||||
|
updateData.swimlaneId = targetSwimlaneId;
|
||||||
|
|
||||||
|
// Move all cards in the list to the new swimlane
|
||||||
|
const cardsInList = ReactiveCache.getCards({
|
||||||
|
listId: list._id,
|
||||||
|
archived: false
|
||||||
|
});
|
||||||
|
|
||||||
|
cardsInList.forEach(card => {
|
||||||
|
card.move(list.boardId, targetSwimlaneId, list._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't cancel the sortable when moving to a different swimlane
|
||||||
|
// The DOM move should be allowed to complete
|
||||||
|
}
|
||||||
|
// Allow reordering within the same swimlane by not canceling the sortable
|
||||||
|
|
||||||
|
try {
|
||||||
|
Lists.update(list._id, {
|
||||||
|
$set: updateData,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get board component
|
||||||
|
try {
|
||||||
|
const boardComponent = BlazeComponent.getComponentForElement(ui.item[0]);
|
||||||
|
if (boardComponent && boardComponent.setIsDragging) {
|
||||||
|
boardComponent.setIsDragging(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Silent fail
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-enable dragscroll after list dragging is complete
|
||||||
|
try {
|
||||||
|
dragscroll.reset();
|
||||||
|
} catch (e) {
|
||||||
|
// Silent fail
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-enable dragscroll on all swimlanes
|
||||||
|
$('.js-swimlane').each(function() {
|
||||||
|
$(this).addClass('dragscroll');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize sortable on ALL listsGroup elements (even empty ones)
|
||||||
|
$listsGroupElements.each(function(index) {
|
||||||
|
const $listsGroup = $(this);
|
||||||
|
const $lists = $listsGroup.find('.js-list');
|
||||||
|
|
||||||
|
// Only initialize on listsGroup elements that have the .js-lists class
|
||||||
|
if ($listsGroup.hasClass('js-lists')) {
|
||||||
|
$listsGroup.sortable({
|
||||||
|
connectWith: '.js-swimlane, .js-lists',
|
||||||
|
tolerance: 'pointer',
|
||||||
|
appendTo: '.board-canvas',
|
||||||
|
helper: 'clone',
|
||||||
|
items: '.js-list:not(.js-list-composer)',
|
||||||
|
placeholder: 'list placeholder',
|
||||||
|
distance: 7,
|
||||||
|
handle: '.js-list-handle',
|
||||||
|
disabled: !Utils.canModifyBoard(),
|
||||||
|
start(evt, ui) {
|
||||||
|
ui.helper.css('z-index', 1000);
|
||||||
|
ui.placeholder.height(ui.helper.height());
|
||||||
|
ui.placeholder.width(ui.helper.width());
|
||||||
|
EscapeActions.executeUpTo('popup-close');
|
||||||
|
// Try to get board component
|
||||||
|
try {
|
||||||
|
const boardComponent = BlazeComponent.getComponentForElement(ui.item[0]);
|
||||||
|
if (boardComponent && boardComponent.setIsDragging) {
|
||||||
|
boardComponent.setIsDragging(true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Silent fail
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stop(evt, ui) {
|
||||||
|
// To attribute the new index number, we need to get the DOM element
|
||||||
|
// of the previous and the following list -- if any.
|
||||||
|
const prevListDom = ui.item.prev('.js-list').get(0);
|
||||||
|
const nextListDom = ui.item.next('.js-list').get(0);
|
||||||
|
const sortIndex = calculateIndex(prevListDom, nextListDom, 1);
|
||||||
|
|
||||||
|
const listDomElement = ui.item.get(0);
|
||||||
|
if (!listDomElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let list;
|
||||||
|
try {
|
||||||
|
list = Blaze.getData(listDomElement);
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect if the list was dropped in a different swimlane
|
||||||
|
const targetSwimlaneDom = ui.item.closest('.js-swimlane');
|
||||||
|
let targetSwimlaneId = null;
|
||||||
|
|
||||||
|
if (targetSwimlaneDom.length > 0) {
|
||||||
|
// List was dropped in a swimlane
|
||||||
|
try {
|
||||||
|
targetSwimlaneId = targetSwimlaneDom.attr('id').replace('swimlane-', '');
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// List was dropped in lists view (not swimlanes view)
|
||||||
|
// In this case, assign to the default swimlane
|
||||||
|
const currentBoard = ReactiveCache.getBoard(Session.get('currentBoard'));
|
||||||
|
if (currentBoard) {
|
||||||
|
const defaultSwimlane = currentBoard.getDefaultSwimline();
|
||||||
|
if (defaultSwimlane) {
|
||||||
|
targetSwimlaneId = defaultSwimlane._id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the original swimlane ID of the list (handle backward compatibility)
|
||||||
|
const originalSwimlaneId = list.getEffectiveSwimlaneId ? list.getEffectiveSwimlaneId() : (list.swimlaneId || null);
|
||||||
|
|
||||||
|
// Prepare update object
|
||||||
|
const updateData = {
|
||||||
|
sort: sortIndex.base,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the list was dropped in a different swimlane
|
||||||
|
const isDifferentSwimlane = targetSwimlaneId && targetSwimlaneId !== originalSwimlaneId;
|
||||||
|
|
||||||
|
// If the list was dropped in a different swimlane, update the swimlaneId
|
||||||
|
if (isDifferentSwimlane) {
|
||||||
|
updateData.swimlaneId = targetSwimlaneId;
|
||||||
|
|
||||||
|
// Move all cards in the list to the new swimlane
|
||||||
|
const cardsInList = ReactiveCache.getCards({
|
||||||
|
listId: list._id,
|
||||||
|
archived: false
|
||||||
|
});
|
||||||
|
|
||||||
|
cardsInList.forEach(card => {
|
||||||
|
card.move(list.boardId, targetSwimlaneId, list._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't cancel the sortable when moving to a different swimlane
|
||||||
|
// The DOM move should be allowed to complete
|
||||||
|
}
|
||||||
|
// Allow reordering within the same swimlane by not canceling the sortable
|
||||||
|
|
||||||
|
try {
|
||||||
|
Lists.update(list._id, {
|
||||||
|
$set: updateData,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get board component
|
||||||
|
try {
|
||||||
|
const boardComponent = BlazeComponent.getComponentForElement(ui.item[0]);
|
||||||
|
if (boardComponent && boardComponent.setIsDragging) {
|
||||||
|
boardComponent.setIsDragging(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Silent fail
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-enable dragscroll after list dragging is complete
|
||||||
|
try {
|
||||||
|
dragscroll.reset();
|
||||||
|
} catch (e) {
|
||||||
|
// Silent fail
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-enable dragscroll on all swimlanes
|
||||||
|
$('.js-swimlane').each(function() {
|
||||||
|
$(this).addClass('dragscroll');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
BlazeComponent.extendComponent({
|
BlazeComponent.extendComponent({
|
||||||
currentCardIsInThisList(listId, swimlaneId) {
|
currentCardIsInThisList(listId, swimlaneId) {
|
||||||
return currentCardIsInThisList(listId, swimlaneId);
|
return currentCardIsInThisList(listId, swimlaneId);
|
||||||
|
|
@ -538,15 +1008,59 @@ BlazeComponent.extendComponent({
|
||||||
onRendered() {
|
onRendered() {
|
||||||
const boardComponent = this.parentComponent();
|
const boardComponent = this.parentComponent();
|
||||||
const $listsDom = this.$('.js-lists');
|
const $listsDom = this.$('.js-lists');
|
||||||
|
|
||||||
|
|
||||||
if (!Utils.getCurrentCardId()) {
|
if (!Utils.getCurrentCardId()) {
|
||||||
boardComponent.scrollLeft();
|
boardComponent.scrollLeft();
|
||||||
}
|
}
|
||||||
|
|
||||||
initSortable(boardComponent, $listsDom);
|
// Try a simpler approach for listsGroup too
|
||||||
|
|
||||||
|
// Wait for DOM to be ready
|
||||||
|
setTimeout(() => {
|
||||||
|
const $lists = this.$('.js-list');
|
||||||
|
|
||||||
|
const $parent = $lists.parent();
|
||||||
|
|
||||||
|
if ($lists.length > 0) {
|
||||||
|
|
||||||
|
// Check for drag handles
|
||||||
|
const $handles = $parent.find('.js-list-handle');
|
||||||
|
|
||||||
|
// Test if drag handles are clickable
|
||||||
|
$handles.on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
$parent.sortable({
|
||||||
|
connectWith: '.js-swimlane, .js-lists',
|
||||||
|
tolerance: 'pointer',
|
||||||
|
appendTo: '.board-canvas',
|
||||||
|
helper: 'clone',
|
||||||
|
items: '.js-list:not(.js-list-composer)',
|
||||||
|
placeholder: 'list placeholder',
|
||||||
|
distance: 7,
|
||||||
|
handle: '.js-list-handle',
|
||||||
|
disabled: !Utils.canModifyBoard(),
|
||||||
|
start(evt, ui) {
|
||||||
|
ui.helper.css('z-index', 1000);
|
||||||
|
ui.placeholder.height(ui.helper.height());
|
||||||
|
ui.placeholder.width(ui.helper.width());
|
||||||
|
EscapeActions.executeUpTo('popup-close');
|
||||||
|
boardComponent.setIsDragging(true);
|
||||||
|
},
|
||||||
|
stop(evt, ui) {
|
||||||
|
boardComponent.setIsDragging(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
},
|
},
|
||||||
}).register('listsGroup');
|
}).register('listsGroup');
|
||||||
|
|
||||||
|
|
||||||
class MoveSwimlaneComponent extends BlazeComponent {
|
class MoveSwimlaneComponent extends BlazeComponent {
|
||||||
serverMethod = 'moveSwimlane';
|
serverMethod = 'moveSwimlane';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
template(name="userAvatar")
|
template(name="userAvatar")
|
||||||
a.member(class="js-{{#if assignee}}assignee{{else}}member{{/if}}" title="{{userData.profile.fullname}} ({{userData.username}}) {{_ memberType}}")
|
a.member(class="js-{{#if assignee}}assignee{{else}}member{{/if}}" title="{{userData.profile.fullname}} ({{userData.username}}) {{_ memberType}}")
|
||||||
if userData.profile.avatarUrl
|
if userData.profile.avatarUrl
|
||||||
img.avatar.avatar-image(src="{{userData.profile.avatarUrl}}")
|
img.avatar.avatar-image(src="{{avatarUrl}}")
|
||||||
else
|
else
|
||||||
+userAvatarInitials(userId=userData._id)
|
+userAvatarInitials(userId=userData._id)
|
||||||
|
|
||||||
|
|
@ -87,7 +87,7 @@ template(name="changeAvatarPopup")
|
||||||
each uploadedAvatars
|
each uploadedAvatars
|
||||||
li: a.js-select-avatar
|
li: a.js-select-avatar
|
||||||
.member
|
.member
|
||||||
img.avatar.avatar-image(src="{{link}}?auth=false&brokenIsFine=true")
|
img.avatar.avatar-image(src="{{link}}")
|
||||||
| {{_ 'uploaded-avatar'}}
|
| {{_ 'uploaded-avatar'}}
|
||||||
if isSelected
|
if isSelected
|
||||||
| ✅
|
| ✅
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,21 @@ Template.userAvatar.helpers({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
avatarUrl() {
|
||||||
|
const user = ReactiveCache.getUser(this.userId, { fields: { profile: 1 } });
|
||||||
|
const base = (user && user.profile && user.profile.avatarUrl) || '';
|
||||||
|
if (!base) return '';
|
||||||
|
// Append current boardId when available so public viewers can access avatars on public boards
|
||||||
|
try {
|
||||||
|
const boardId = Utils.getCurrentBoardId && Utils.getCurrentBoardId();
|
||||||
|
if (boardId) {
|
||||||
|
const sep = base.includes('?') ? '&' : '?';
|
||||||
|
return `${base}${sep}boardId=${encodeURIComponent(boardId)}`;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return base;
|
||||||
|
},
|
||||||
|
|
||||||
memberType() {
|
memberType() {
|
||||||
const user = ReactiveCache.getUser(this.userId);
|
const user = ReactiveCache.getUser(this.userId);
|
||||||
return user && user.isBoardAdmin() ? 'admin' : 'normal';
|
return user && user.isBoardAdmin() ? 'admin' : 'normal';
|
||||||
|
|
@ -179,7 +194,7 @@ BlazeComponent.extendComponent({
|
||||||
isSelected() {
|
isSelected() {
|
||||||
const userProfile = ReactiveCache.getCurrentUser().profile;
|
const userProfile = ReactiveCache.getCurrentUser().profile;
|
||||||
const avatarUrl = userProfile && userProfile.avatarUrl;
|
const avatarUrl = userProfile && userProfile.avatarUrl;
|
||||||
const currentAvatarUrl = `${this.currentData().link()}?auth=false&brokenIsFine=true`;
|
const currentAvatarUrl = this.currentData().link();
|
||||||
return avatarUrl === currentAvatarUrl;
|
return avatarUrl === currentAvatarUrl;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -220,7 +235,7 @@ BlazeComponent.extendComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'click .js-select-avatar'() {
|
'click .js-select-avatar'() {
|
||||||
const avatarUrl = `${this.currentData().link()}?auth=false&brokenIsFine=true`;
|
const avatarUrl = this.currentData().link();
|
||||||
this.setAvatar(avatarUrl);
|
this.setAvatar(avatarUrl);
|
||||||
},
|
},
|
||||||
'click .js-select-initials'() {
|
'click .js-select-initials'() {
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,6 @@ template(name="headerUserBar")
|
||||||
|
|
||||||
template(name="memberMenuPopup")
|
template(name="memberMenuPopup")
|
||||||
ul.pop-over-list
|
ul.pop-over-list
|
||||||
// Bookmarks at the very top
|
|
||||||
li
|
|
||||||
a.js-open-bookmarks
|
|
||||||
| 🔖
|
|
||||||
| {{_ 'bookmarks'}}
|
|
||||||
with currentUser
|
with currentUser
|
||||||
li
|
li
|
||||||
a.js-my-cards(href="{{pathFor 'my-cards'}}")
|
a.js-my-cards(href="{{pathFor 'my-cards'}}")
|
||||||
|
|
@ -32,6 +27,7 @@ template(name="memberMenuPopup")
|
||||||
| {{_ 'globalSearch-title'}}
|
| {{_ 'globalSearch-title'}}
|
||||||
li
|
li
|
||||||
a(href="{{pathFor 'home'}}")
|
a(href="{{pathFor 'home'}}")
|
||||||
|
| 🏠
|
||||||
| 🏠
|
| 🏠
|
||||||
| {{_ 'all-boards'}}
|
| {{_ 'all-boards'}}
|
||||||
li
|
li
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,10 @@ Blaze.registerHelper('canModifyCard', () =>
|
||||||
Utils.canModifyCard(),
|
Utils.canModifyCard(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Blaze.registerHelper('canMoveCard', () =>
|
||||||
|
Utils.canMoveCard(),
|
||||||
|
);
|
||||||
|
|
||||||
Blaze.registerHelper('canModifyBoard', () =>
|
Blaze.registerHelper('canModifyBoard', () =>
|
||||||
Utils.canModifyBoard(),
|
Utils.canModifyBoard(),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
73
client/lib/boardMultiSelection.js
Normal file
73
client/lib/boardMultiSelection.js
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { ReactiveCache } from '/imports/reactiveCache';
|
||||||
|
|
||||||
|
BoardMultiSelection = {
|
||||||
|
_selectedBoards: new ReactiveVar([]),
|
||||||
|
|
||||||
|
_isActive: new ReactiveVar(false),
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this._selectedBoards.set([]);
|
||||||
|
},
|
||||||
|
|
||||||
|
isActive() {
|
||||||
|
return this._isActive.get();
|
||||||
|
},
|
||||||
|
|
||||||
|
count() {
|
||||||
|
return this._selectedBoards.get().length;
|
||||||
|
},
|
||||||
|
|
||||||
|
isEmpty() {
|
||||||
|
return this.count() === 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSelectedBoardIds() {
|
||||||
|
return this._selectedBoards.get();
|
||||||
|
},
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
if (!this.isActive()) {
|
||||||
|
this._isActive.set(true);
|
||||||
|
Tracker.flush();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this.isActive()) {
|
||||||
|
this._isActive.set(false);
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
add(boardIds) {
|
||||||
|
return this.toggle(boardIds, { add: true, remove: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(boardIds) {
|
||||||
|
return this.toggle(boardIds, { add: false, remove: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
toogle(boardIds) {
|
||||||
|
return this.toggle(boardIds, { add: true, remove: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
toggle(boardIds, { add, remove } = {}) {
|
||||||
|
boardIds = _.isString(boardIds) ? [boardIds] : boardIds;
|
||||||
|
let selectedBoards = this._selectedBoards.get();
|
||||||
|
|
||||||
|
boardIds.forEach(boardId => {
|
||||||
|
const index = selectedBoards.indexOf(boardId);
|
||||||
|
if (index > -1 && remove) {
|
||||||
|
selectedBoards = selectedBoards.filter(id => id !== boardId);
|
||||||
|
} else if (index === -1 && add) {
|
||||||
|
selectedBoards.push(boardId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._selectedBoards.set(selectedBoards);
|
||||||
|
},
|
||||||
|
|
||||||
|
isSelected(boardId) {
|
||||||
|
return this._selectedBoards.get().includes(boardId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -29,21 +29,86 @@ export class CardSearchPagedComponent extends BlazeComponent {
|
||||||
const that = this;
|
const that = this;
|
||||||
this.subscriptionCallbacks = {
|
this.subscriptionCallbacks = {
|
||||||
onReady() {
|
onReady() {
|
||||||
that.getResults();
|
if (process.env.DEBUG === 'true') {
|
||||||
that.searching.set(false);
|
console.log('Subscription ready, getting results...');
|
||||||
that.hasResults.set(true);
|
console.log('Subscription ready - sessionId:', that.sessionId);
|
||||||
that.serverError.set(false);
|
}
|
||||||
|
|
||||||
|
// Wait for session data to be available (with timeout)
|
||||||
|
let waitCount = 0;
|
||||||
|
const maxWaitCount = 50; // 10 seconds max wait
|
||||||
|
|
||||||
|
const waitForSessionData = () => {
|
||||||
|
waitCount++;
|
||||||
|
const sessionData = that.getSessionData();
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('waitForSessionData - attempt', waitCount, 'session data:', sessionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionData) {
|
||||||
|
const results = that.getResults();
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('Search results count:', results ? results.length : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no results and this is a due cards search, try to retry
|
||||||
|
if ((!results || results.length === 0) && that.searchRetryCount !== undefined && that.searchRetryCount < that.maxRetries) {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('No results found, retrying search...');
|
||||||
|
}
|
||||||
|
that.searchRetryCount++;
|
||||||
|
Meteor.setTimeout(() => {
|
||||||
|
if (that.performSearch) {
|
||||||
|
that.performSearch();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
that.searching.set(false);
|
||||||
|
that.hasResults.set(true);
|
||||||
|
that.serverError.set(false);
|
||||||
|
} else if (waitCount < maxWaitCount) {
|
||||||
|
// Session data not available yet, wait a bit more
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('Session data not available yet, waiting... (attempt', waitCount, 'of', maxWaitCount, ')');
|
||||||
|
}
|
||||||
|
Meteor.setTimeout(waitForSessionData, 200);
|
||||||
|
} else {
|
||||||
|
// Timeout reached, try fallback search
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('Timeout reached waiting for session data, trying fallback search');
|
||||||
|
}
|
||||||
|
const results = that.getResults();
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('Fallback search results count:', results ? results.length : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results && results.length > 0) {
|
||||||
|
that.searching.set(false);
|
||||||
|
that.hasResults.set(true);
|
||||||
|
that.serverError.set(false);
|
||||||
|
} else {
|
||||||
|
that.searching.set(false);
|
||||||
|
that.hasResults.set(false);
|
||||||
|
that.serverError.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start waiting for session data
|
||||||
|
Meteor.setTimeout(waitForSessionData, 100);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('Subscription error:', error);
|
||||||
|
console.log('Error.reason:', error.reason);
|
||||||
|
console.log('Error.message:', error.message);
|
||||||
|
console.log('Error.stack:', error.stack);
|
||||||
|
}
|
||||||
that.searching.set(false);
|
that.searching.set(false);
|
||||||
that.hasResults.set(false);
|
that.hasResults.set(false);
|
||||||
that.serverError.set(true);
|
that.serverError.set(true);
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
//console.log('Error.reason:', error.reason);
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
//console.log('Error.message:', error.message);
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
//console.log('Error.stack:', error.stack);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -62,9 +127,28 @@ export class CardSearchPagedComponent extends BlazeComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
getSessionData(sessionId) {
|
getSessionData(sessionId) {
|
||||||
return ReactiveCache.getSessionData({
|
const sessionIdToUse = sessionId || SessionData.getSessionId();
|
||||||
sessionId: sessionId || SessionData.getSessionId(),
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('getSessionData - looking for sessionId:', sessionIdToUse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try using the raw SessionData collection instead of ReactiveCache
|
||||||
|
const sessionData = SessionData.findOne({
|
||||||
|
sessionId: sessionIdToUse,
|
||||||
});
|
});
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('getSessionData - found session data (raw):', sessionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also try ReactiveCache for comparison
|
||||||
|
const reactiveSessionData = ReactiveCache.getSessionData({
|
||||||
|
sessionId: sessionIdToUse,
|
||||||
|
});
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('getSessionData - found session data (reactive):', reactiveSessionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionData || reactiveSessionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
getResults() {
|
getResults() {
|
||||||
|
|
@ -72,33 +156,87 @@ export class CardSearchPagedComponent extends BlazeComponent {
|
||||||
// console.log('getting results');
|
// console.log('getting results');
|
||||||
this.sessionData = this.getSessionData();
|
this.sessionData = this.getSessionData();
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('session data:', this.sessionData);
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('getResults - sessionId:', this.sessionId);
|
||||||
|
console.log('getResults - session data:', this.sessionData);
|
||||||
|
}
|
||||||
const cards = [];
|
const cards = [];
|
||||||
this.sessionData.cards.forEach(cardId => {
|
|
||||||
cards.push(ReactiveCache.getCard(cardId));
|
if (this.sessionData && this.sessionData.cards) {
|
||||||
});
|
if (process.env.DEBUG === 'true') {
|
||||||
this.queryErrors = this.sessionData.errors;
|
console.log('getResults - cards array length:', this.sessionData.cards.length);
|
||||||
|
}
|
||||||
|
this.sessionData.cards.forEach(cardId => {
|
||||||
|
const card = ReactiveCache.getCard(cardId);
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('getResults - card:', cardId, card);
|
||||||
|
}
|
||||||
|
cards.push(card);
|
||||||
|
});
|
||||||
|
this.queryErrors = this.sessionData.errors || [];
|
||||||
|
} else {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('getResults - no sessionData or no cards array, trying direct card search');
|
||||||
|
}
|
||||||
|
// Fallback: try to get cards directly from the client-side collection
|
||||||
|
// Use a more efficient query with limit and sort
|
||||||
|
const selector = {
|
||||||
|
type: 'cardType-card',
|
||||||
|
dueAt: { $exists: true, $nin: [null, ''] }
|
||||||
|
};
|
||||||
|
const options = {
|
||||||
|
sort: { dueAt: 1 }, // Sort by due date ascending (oldest first)
|
||||||
|
limit: 100 // Limit to 100 cards for performance
|
||||||
|
};
|
||||||
|
const allCards = Cards.find(selector, options).fetch();
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('getResults - direct card search found:', allCards ? allCards.length : 0, 'cards');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allCards && allCards.length > 0) {
|
||||||
|
allCards.forEach(card => {
|
||||||
|
if (card && card._id) {
|
||||||
|
cards.push(card);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.queryErrors = [];
|
||||||
|
}
|
||||||
if (this.queryErrors.length) {
|
if (this.queryErrors.length) {
|
||||||
// console.log('queryErrors:', this.queryErrorMessages());
|
// console.log('queryErrors:', this.queryErrorMessages());
|
||||||
this.hasQueryErrors.set(true);
|
this.hasQueryErrors.set(true);
|
||||||
// return null;
|
// return null;
|
||||||
}
|
}
|
||||||
this.debug.set(new QueryDebug(this.sessionData.debug));
|
this.debug.set(new QueryDebug(this.sessionData ? this.sessionData.debug : null));
|
||||||
console.log('debug:', this.debug.get().get());
|
if (process.env.DEBUG === 'true') {
|
||||||
console.log('debug.show():', this.debug.get().show());
|
console.log('debug:', this.debug.get().get());
|
||||||
console.log('debug.showSelector():', this.debug.get().showSelector());
|
console.log('debug.show():', this.debug.get().show());
|
||||||
|
console.log('debug.showSelector():', this.debug.get().showSelector());
|
||||||
|
}
|
||||||
|
|
||||||
if (cards) {
|
if (cards) {
|
||||||
this.totalHits = this.sessionData.totalHits;
|
if (this.sessionData) {
|
||||||
this.resultsCount = cards.length;
|
this.totalHits = this.sessionData.totalHits || 0;
|
||||||
this.resultsStart = this.sessionData.lastHit - this.resultsCount + 1;
|
this.resultsCount = cards.length;
|
||||||
this.resultsEnd = this.sessionData.lastHit;
|
this.resultsStart = this.sessionData.lastHit - this.resultsCount + 1;
|
||||||
this.resultsHeading.set(this.getResultsHeading());
|
this.resultsEnd = this.sessionData.lastHit;
|
||||||
this.results.set(cards);
|
this.resultsHeading.set(this.getResultsHeading());
|
||||||
this.hasNextPage.set(this.sessionData.lastHit < this.sessionData.totalHits);
|
this.results.set(cards);
|
||||||
this.hasPreviousPage.set(
|
this.hasNextPage.set(this.sessionData.lastHit < this.sessionData.totalHits);
|
||||||
this.sessionData.lastHit - this.sessionData.resultsCount > 0,
|
this.hasPreviousPage.set(
|
||||||
);
|
this.sessionData.lastHit - this.sessionData.resultsCount > 0,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.totalHits = cards.length;
|
||||||
|
this.resultsCount = cards.length;
|
||||||
|
this.resultsStart = 1;
|
||||||
|
this.resultsEnd = cards.length;
|
||||||
|
this.resultsHeading.set(this.getResultsHeading());
|
||||||
|
this.results.set(cards);
|
||||||
|
this.hasNextPage.set(false);
|
||||||
|
this.hasPreviousPage.set(false);
|
||||||
|
}
|
||||||
return cards;
|
return cards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,13 +251,29 @@ export class CardSearchPagedComponent extends BlazeComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubscription(queryParams) {
|
getSubscription(queryParams) {
|
||||||
return Meteor.subscribe(
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('Subscribing to globalSearch with:', {
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
params: queryParams.params,
|
||||||
|
text: queryParams.text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to both globalSearch and sessionData
|
||||||
|
const globalSearchHandle = Meteor.subscribe(
|
||||||
'globalSearch',
|
'globalSearch',
|
||||||
this.sessionId,
|
this.sessionId,
|
||||||
queryParams.params,
|
queryParams.params,
|
||||||
queryParams.text,
|
queryParams.text,
|
||||||
this.subscriptionCallbacks,
|
this.subscriptionCallbacks,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sessionDataHandle = Meteor.subscribe('sessionData', this.sessionId);
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.log('Subscribed to sessionData with sessionId:', this.sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalSearchHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
runGlobalSearch(queryParams) {
|
runGlobalSearch(queryParams) {
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,27 @@
|
||||||
import { ReactiveCache } from '/imports/reactiveCache';
|
import { ReactiveCache } from '/imports/reactiveCache';
|
||||||
import { TAPi18n } from '/imports/i18n';
|
import { TAPi18n } from '/imports/i18n';
|
||||||
import {
|
|
||||||
formatDateTime,
|
|
||||||
formatDate,
|
|
||||||
formatTime,
|
|
||||||
getISOWeek,
|
|
||||||
isValidDate,
|
|
||||||
isBefore,
|
|
||||||
isAfter,
|
|
||||||
isSame,
|
|
||||||
add,
|
|
||||||
subtract,
|
|
||||||
startOf,
|
|
||||||
endOf,
|
|
||||||
format,
|
|
||||||
parseDate,
|
|
||||||
now,
|
|
||||||
createDate,
|
|
||||||
fromNow,
|
|
||||||
calendar
|
|
||||||
} from '/imports/lib/dateUtils';
|
|
||||||
|
|
||||||
// Helper function to get time format for 24 hours
|
// Helper to check if a date is valid
|
||||||
function adjustedTimeFormat() {
|
function isValidDate(date) {
|
||||||
return 'HH:mm';
|
return date instanceof Date && !isNaN(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
// .replace(/HH/i, 'H');
|
// Format date as YYYY-MM-DD
|
||||||
|
function formatDate(date) {
|
||||||
|
if (!isValidDate(date)) return '';
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format time as HH:mm
|
||||||
|
function formatTime(date) {
|
||||||
|
if (!isValidDate(date)) return '';
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
export class DatePicker extends BlazeComponent {
|
export class DatePicker extends BlazeComponent {
|
||||||
template() {
|
template() {
|
||||||
|
|
@ -76,10 +71,10 @@ export class DatePicker extends BlazeComponent {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return 'L';
|
return 'YYYY-MM-DD';
|
||||||
}
|
}
|
||||||
timeFormat() {
|
timeFormat() {
|
||||||
return 'LT';
|
return 'HH:mm';
|
||||||
}
|
}
|
||||||
|
|
||||||
events() {
|
events() {
|
||||||
|
|
@ -89,7 +84,8 @@ export class DatePicker extends BlazeComponent {
|
||||||
// Native HTML date input validation
|
// Native HTML date input validation
|
||||||
const dateValue = this.find('#date').value;
|
const dateValue = this.find('#date').value;
|
||||||
if (dateValue) {
|
if (dateValue) {
|
||||||
const dateObj = new Date(dateValue);
|
// HTML date input format is always YYYY-MM-DD
|
||||||
|
const dateObj = new Date(dateValue + 'T12:00:00');
|
||||||
if (isValidDate(dateObj)) {
|
if (isValidDate(dateObj)) {
|
||||||
this.error.set('');
|
this.error.set('');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -101,7 +97,8 @@ export class DatePicker extends BlazeComponent {
|
||||||
// Native HTML time input validation
|
// Native HTML time input validation
|
||||||
const timeValue = this.find('#time').value;
|
const timeValue = this.find('#time').value;
|
||||||
if (timeValue) {
|
if (timeValue) {
|
||||||
const timeObj = new Date(`1970-01-01T${timeValue}`);
|
// HTML time input format is always HH:mm
|
||||||
|
const timeObj = new Date(`1970-01-01T${timeValue}:00`);
|
||||||
if (isValidDate(timeObj)) {
|
if (isValidDate(timeObj)) {
|
||||||
this.error.set('');
|
this.error.set('');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -121,7 +118,9 @@ export class DatePicker extends BlazeComponent {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCompleteDate = new Date(`${dateValue}T${timeValue}`);
|
// Combine date and time: HTML date input is YYYY-MM-DD, time input is HH:mm
|
||||||
|
const dateTimeString = `${dateValue}T${timeValue}:00`;
|
||||||
|
const newCompleteDate = new Date(dateTimeString);
|
||||||
|
|
||||||
if (!isValidDate(newCompleteDate)) {
|
if (!isValidDate(newCompleteDate)) {
|
||||||
this.error.set('invalid');
|
this.error.set('invalid');
|
||||||
|
|
|
||||||
94
client/lib/fixDuplicateLists.js
Normal file
94
client/lib/fixDuplicateLists.js
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client-side interface for fixing duplicate lists
|
||||||
|
*/
|
||||||
|
export const fixDuplicateLists = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a report of all boards with duplicate lists/swimlanes
|
||||||
|
*/
|
||||||
|
async getReport() {
|
||||||
|
try {
|
||||||
|
const result = await Meteor.callAsync('fixDuplicateLists.getReport');
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting duplicate lists report:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix duplicate lists for a specific board
|
||||||
|
*/
|
||||||
|
async fixBoard(boardId) {
|
||||||
|
try {
|
||||||
|
const result = await Meteor.callAsync('fixDuplicateLists.fixBoard', boardId);
|
||||||
|
console.log(`Fixed duplicate lists for board ${boardId}:`, result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fixing board ${boardId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix duplicate lists for all boards
|
||||||
|
*/
|
||||||
|
async fixAllBoards() {
|
||||||
|
try {
|
||||||
|
console.log('Starting fix for all boards...');
|
||||||
|
const result = await Meteor.callAsync('fixDuplicateLists.fixAllBoards');
|
||||||
|
console.log('Fix completed:', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fixing all boards:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interactive fix with user confirmation
|
||||||
|
*/
|
||||||
|
async interactiveFix() {
|
||||||
|
try {
|
||||||
|
// Get report first
|
||||||
|
console.log('Getting duplicate lists report...');
|
||||||
|
const report = await this.getReport();
|
||||||
|
|
||||||
|
if (report.boardsWithDuplicates === 0) {
|
||||||
|
console.log('No duplicate lists found!');
|
||||||
|
return { message: 'No duplicate lists found!' };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${report.boardsWithDuplicates} boards with duplicate lists:`);
|
||||||
|
report.report.forEach(board => {
|
||||||
|
console.log(`- Board "${board.boardTitle}" (${board.boardId}): ${board.duplicateSwimlanes} duplicate swimlanes, ${board.duplicateLists} duplicate lists`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ask for confirmation
|
||||||
|
const confirmed = confirm(
|
||||||
|
`Found ${report.boardsWithDuplicates} boards with duplicate lists. ` +
|
||||||
|
`This will fix ${report.report.reduce((sum, board) => sum + board.duplicateSwimlanes + board.duplicateLists, 0)} duplicates. ` +
|
||||||
|
'Continue?'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return { message: 'Fix cancelled by user' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the fix
|
||||||
|
const result = await this.fixAllBoards();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in interactive fix:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make it available globally for console access
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.fixDuplicateLists = fixDuplicateLists;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -646,6 +646,23 @@ class MigrationManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix boards that are stuck in migration loop
|
||||||
|
*/
|
||||||
|
fixStuckBoards() {
|
||||||
|
try {
|
||||||
|
Meteor.call('boardMigration.fixStuckBoards', (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fix stuck boards:', error);
|
||||||
|
} else {
|
||||||
|
console.log('Fix stuck boards result:', result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fixing stuck boards:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start migration process using cron system
|
* Start migration process using cron system
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,10 @@ window.Popup = new (class {
|
||||||
// our internal dependency, and since we just changed the top element of
|
// our internal dependency, and since we just changed the top element of
|
||||||
// our internal stack, the popup will be updated with the new data.
|
// our internal stack, the popup will be updated with the new data.
|
||||||
if (!self.isOpen()) {
|
if (!self.isOpen()) {
|
||||||
|
if (!Template[popupName]) {
|
||||||
|
console.error('Template not found:', popupName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.current = Blaze.renderWithData(
|
self.current = Blaze.renderWithData(
|
||||||
self.template,
|
self.template,
|
||||||
() => {
|
() => {
|
||||||
|
|
@ -214,6 +218,22 @@ window.Popup = new (class {
|
||||||
const viewportHeight = $(window).height();
|
const viewportHeight = $(window).height();
|
||||||
const popupWidth = Math.min(380, viewportWidth * 0.55) + 15; // Add 15px for margin
|
const popupWidth = Math.min(380, viewportWidth * 0.55) + 15; // Add 15px for margin
|
||||||
|
|
||||||
|
// Check if this is an admin panel edit popup
|
||||||
|
const isAdminEditPopup = $element.hasClass('edit-user') ||
|
||||||
|
$element.hasClass('edit-org') ||
|
||||||
|
$element.hasClass('edit-team');
|
||||||
|
|
||||||
|
if (isAdminEditPopup) {
|
||||||
|
// Center the popup horizontally and use full height
|
||||||
|
const centeredLeft = (viewportWidth - popupWidth) / 2;
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: Math.max(10, centeredLeft), // Ensure popup doesn't go off screen
|
||||||
|
top: 10, // Start from top with small margin
|
||||||
|
maxHeight: viewportHeight - 20, // Use full height minus small margins
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate available height for popup
|
// Calculate available height for popup
|
||||||
const popupTop = offset.top + $element.outerHeight();
|
const popupTop = offset.top + $element.outerHeight();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,43 +3,25 @@ import DOMPurify from 'dompurify';
|
||||||
// Centralized secure DOMPurify configuration to prevent XSS and CSS injection attacks
|
// Centralized secure DOMPurify configuration to prevent XSS and CSS injection attacks
|
||||||
export function getSecureDOMPurifyConfig() {
|
export function getSecureDOMPurifyConfig() {
|
||||||
return {
|
return {
|
||||||
// Block dangerous elements that can cause XSS and CSS injection
|
// Allow common markdown elements including anchor tags
|
||||||
FORBID_TAGS: [
|
ALLOWED_TAGS: ['a', 'p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'pre', 'code', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr', 'div', 'span', 's'],
|
||||||
'svg', 'defs', 'use', 'g', 'symbol', 'marker', 'pattern', 'mask', 'clipPath',
|
// Allow safe attributes including href for anchor tags
|
||||||
'linearGradient', 'radialGradient', 'stop', 'animate', 'animateTransform',
|
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'width', 'height', 'target', 'rel'],
|
||||||
'animateMotion', 'set', 'switch', 'foreignObject', 'script', 'style', 'link',
|
// Allow safe protocols for links
|
||||||
'meta', 'iframe', 'object', 'embed', 'applet', 'form', 'input', 'textarea',
|
|
||||||
'select', 'option', 'button', 'label', 'fieldset', 'legend', 'frameset',
|
|
||||||
'frame', 'noframes', 'base', 'basefont', 'isindex', 'dir', 'menu', 'menuitem'
|
|
||||||
],
|
|
||||||
// Block dangerous attributes that can cause XSS and CSS injection
|
|
||||||
FORBID_ATTR: [
|
|
||||||
'xlink:href', 'href', 'onload', 'onerror', 'onclick', 'onmouseover',
|
|
||||||
'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect',
|
|
||||||
'onunload', 'onresize', 'onscroll', 'onkeydown', 'onkeyup', 'onkeypress',
|
|
||||||
'onmousedown', 'onmouseup', 'onmouseover', 'onmouseout', 'onmousemove',
|
|
||||||
'ondblclick', 'oncontextmenu', 'onwheel', 'ontouchstart', 'ontouchend',
|
|
||||||
'ontouchmove', 'ontouchcancel', 'onabort', 'oncanplay', 'oncanplaythrough',
|
|
||||||
'ondurationchange', 'onemptied', 'onended', 'onerror', 'onloadeddata',
|
|
||||||
'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying',
|
|
||||||
'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled',
|
|
||||||
'onsuspend', 'ontimeupdate', 'onvolumechange', 'onwaiting', 'onbeforeunload',
|
|
||||||
'onhashchange', 'onpagehide', 'onpageshow', 'onpopstate', 'onstorage',
|
|
||||||
'onunload', 'style', 'class', 'id', 'data-*', 'aria-*'
|
|
||||||
],
|
|
||||||
// Allow only safe image formats and protocols
|
|
||||||
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
|
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
|
||||||
// Remove dangerous protocols
|
// Allow unknown protocols but be cautious
|
||||||
ALLOW_UNKNOWN_PROTOCOLS: false,
|
ALLOW_UNKNOWN_PROTOCOLS: false,
|
||||||
// Sanitize URLs to prevent malicious content loading
|
// Sanitize DOM for security
|
||||||
SANITIZE_DOM: true,
|
SANITIZE_DOM: true,
|
||||||
// Remove dangerous elements completely
|
// Keep content but sanitize it
|
||||||
KEEP_CONTENT: false,
|
KEEP_CONTENT: true,
|
||||||
// Additional security measures
|
// Block dangerous elements that can cause XSS
|
||||||
ADD_ATTR: [],
|
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'applet', 'svg', 'defs', 'use', 'g', 'symbol', 'marker', 'pattern', 'mask', 'clipPath', 'linearGradient', 'radialGradient', 'stop', 'animate', 'animateTransform', 'animateMotion', 'set', 'switch', 'foreignObject', 'link', 'meta', 'form', 'input', 'textarea', 'select', 'option', 'button', 'label', 'fieldset', 'legend', 'frameset', 'frame', 'noframes', 'base', 'basefont', 'isindex', 'dir', 'menu', 'menuitem'],
|
||||||
|
// Block dangerous attributes but allow safe href
|
||||||
|
FORBID_ATTR: ['xlink:href', 'onload', 'onerror', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect', 'onunload', 'onresize', 'onscroll', 'onkeydown', 'onkeyup', 'onkeypress', 'onmousedown', 'onmouseup', 'onmouseover', 'onmouseout', 'onmousemove', 'ondblclick', 'oncontextmenu', 'onwheel', 'ontouchstart', 'ontouchend', 'ontouchmove', 'ontouchcancel', 'onabort', 'oncanplay', 'oncanplaythrough', 'ondurationchange', 'onemptied', 'onended', 'onerror', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying', 'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled', 'onsuspend', 'ontimeupdate', 'onvolumechange', 'onwaiting', 'onbeforeunload', 'onhashchange', 'onpagehide', 'onpageshow', 'onpopstate', 'onstorage', 'onunload', 'style', 'class', 'id', 'data-*', 'aria-*'],
|
||||||
// Block data URIs that could contain malicious content
|
// Block data URIs that could contain malicious content
|
||||||
ALLOW_DATA_ATTR: false,
|
ALLOW_DATA_ATTR: false,
|
||||||
// Custom hook to further sanitize content
|
// Custom hooks for additional security
|
||||||
HOOKS: {
|
HOOKS: {
|
||||||
uponSanitizeElement: function(node, data) {
|
uponSanitizeElement: function(node, data) {
|
||||||
// Block any remaining dangerous elements
|
// Block any remaining dangerous elements
|
||||||
|
|
@ -51,14 +33,37 @@ export function getSecureDOMPurifyConfig() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block img tags with SVG data URIs
|
// Block img tags with SVG data URIs that could contain malicious JavaScript
|
||||||
if (node.tagName && node.tagName.toLowerCase() === 'img') {
|
if (node.tagName && node.tagName.toLowerCase() === 'img') {
|
||||||
const src = node.getAttribute('src');
|
const src = node.getAttribute('src');
|
||||||
if (src && (src.startsWith('data:image/svg') || src.endsWith('.svg'))) {
|
if (src) {
|
||||||
if (process.env.DEBUG === 'true') {
|
// Block all SVG data URIs to prevent XSS via embedded JavaScript
|
||||||
console.warn('Blocked potentially malicious SVG image:', src);
|
if (src.startsWith('data:image/svg') || src.endsWith('.svg')) {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.warn('Blocked potentially malicious SVG image:', src);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional check for base64 encoded SVG with script tags
|
||||||
|
if (src.startsWith('data:image/svg+xml;base64,')) {
|
||||||
|
try {
|
||||||
|
const base64Content = src.split(',')[1];
|
||||||
|
const decodedContent = atob(base64Content);
|
||||||
|
if (decodedContent.includes('<script') || decodedContent.includes('javascript:')) {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.warn('Blocked SVG with embedded JavaScript:', src.substring(0, 100) + '...');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If decoding fails, block it as a safety measure
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.warn('Blocked malformed SVG data URI:', src);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,6 +105,19 @@ export function getSecureDOMPurifyConfig() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow href attribute for anchor tags only
|
||||||
|
if (data.attrName === 'href') {
|
||||||
|
// Only allow href on anchor tags
|
||||||
|
if (node.tagName && node.tagName.toLowerCase() === 'a') {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.warn('Blocked href attribute on non-anchor element:', node.tagName);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,15 @@ Utils = {
|
||||||
);
|
);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
|
canMoveCard() {
|
||||||
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
const ret = (
|
||||||
|
currentUser &&
|
||||||
|
currentUser.isBoardMember() &&
|
||||||
|
!currentUser.isCommentOnly()
|
||||||
|
);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
canModifyBoard() {
|
canModifyBoard() {
|
||||||
const currentUser = ReactiveCache.getCurrentUser();
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
const ret = (
|
const ret = (
|
||||||
|
|
@ -231,9 +240,21 @@ Utils = {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
},
|
},
|
||||||
setBoardView(view) {
|
setBoardView(view) {
|
||||||
currentUser = ReactiveCache.getCurrentUser();
|
const currentUser = ReactiveCache.getCurrentUser();
|
||||||
|
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
ReactiveCache.getCurrentUser().setBoardView(view);
|
// Update localStorage first
|
||||||
|
window.localStorage.setItem('boardView', view);
|
||||||
|
|
||||||
|
// Update user profile via Meteor method
|
||||||
|
Meteor.call('setBoardView', view, (error) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('[setBoardView] Update failed:', error);
|
||||||
|
} else {
|
||||||
|
// Reload to apply the view change
|
||||||
|
Utils.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
} else if (view === 'board-view-swimlanes') {
|
} else if (view === 'board-view-swimlanes') {
|
||||||
window.localStorage.setItem('boardView', 'board-view-swimlanes'); //true
|
window.localStorage.setItem('boardView', 'board-view-swimlanes'); //true
|
||||||
Utils.reload();
|
Utils.reload();
|
||||||
|
|
@ -433,10 +454,14 @@ Utils = {
|
||||||
// we can easily debug with a small window of desktop browser. :-)
|
// we can easily debug with a small window of desktop browser. :-)
|
||||||
isMiniScreen() {
|
isMiniScreen() {
|
||||||
this.windowResizeDep.depend();
|
this.windowResizeDep.depend();
|
||||||
|
// Also depend on mobile mode changes to make this reactive
|
||||||
|
Session.get('wekan-mobile-mode');
|
||||||
|
|
||||||
// Show mobile view when:
|
// Show mobile view when:
|
||||||
// 1. Screen width is 800px or less (matches CSS media queries)
|
// 1. Screen width is 800px or less (matches CSS media queries)
|
||||||
// 2. Mobile phones in portrait mode
|
// 2. Mobile phones in portrait mode
|
||||||
// 3. iPad in very small screens (≤ 600px)
|
// 3. iPad in very small screens (≤ 600px)
|
||||||
|
// 4. All iPhone models by default (including largest models), but respect user preference
|
||||||
const isSmallScreen = window.innerWidth <= 800;
|
const isSmallScreen = window.innerWidth <= 800;
|
||||||
const isVerySmallScreen = window.innerWidth <= 600;
|
const isVerySmallScreen = window.innerWidth <= 600;
|
||||||
const isPortrait = window.innerWidth < window.innerHeight || window.matchMedia("(orientation: portrait)").matches;
|
const isPortrait = window.innerWidth < window.innerHeight || window.matchMedia("(orientation: portrait)").matches;
|
||||||
|
|
@ -445,13 +470,19 @@ Utils = {
|
||||||
const isIPad = /iPad/i.test(navigator.userAgent);
|
const isIPad = /iPad/i.test(navigator.userAgent);
|
||||||
const isUbuntuTouch = /Ubuntu/i.test(navigator.userAgent);
|
const isUbuntuTouch = /Ubuntu/i.test(navigator.userAgent);
|
||||||
|
|
||||||
// For iPhone: always show mobile view regardless of orientation
|
// Check if user has explicitly set mobile mode preference
|
||||||
// For other mobile phones: show mobile view in portrait, desktop view in landscape
|
const userMobileMode = this.getMobileMode();
|
||||||
// For iPad: show mobile view only in very small screens (≤ 600px)
|
|
||||||
// For Ubuntu Touch: smartphones behave like mobile phones, tablets like iPad
|
// For iPhone: default to mobile view, but respect user's mobile mode toggle preference
|
||||||
// For desktop: show mobile view when screen width <= 800px
|
// This ensures all iPhone models (including iPhone 15 Pro Max, 14 Pro Max, etc.) start with mobile view
|
||||||
|
// but users can still switch to desktop mode if they prefer
|
||||||
if (isIPhone) {
|
if (isIPhone) {
|
||||||
return true; // iPhone: always mobile view
|
// If user has explicitly set a preference, respect it
|
||||||
|
if (userMobileMode !== null && userMobileMode !== undefined) {
|
||||||
|
return userMobileMode;
|
||||||
|
}
|
||||||
|
// Otherwise, default to mobile view for iPhones
|
||||||
|
return true;
|
||||||
} else if (isMobilePhone) {
|
} else if (isMobilePhone) {
|
||||||
return isPortrait; // Other mobile phones: portrait = mobile, landscape = desktop
|
return isPortrait; // Other mobile phones: portrait = mobile, landscape = desktop
|
||||||
} else if (isIPad) {
|
} else if (isIPad) {
|
||||||
|
|
@ -496,30 +527,14 @@ Utils = {
|
||||||
|
|
||||||
// returns if desktop drag handles are enabled
|
// returns if desktop drag handles are enabled
|
||||||
isShowDesktopDragHandles() {
|
isShowDesktopDragHandles() {
|
||||||
if (this.isTouchScreen()) {
|
// Always show drag handles on all displays
|
||||||
return true;
|
return true;
|
||||||
/*
|
|
||||||
const currentUser = ReactiveCache.getCurrentUser();
|
|
||||||
if (currentUser) {
|
|
||||||
return (currentUser.profile || {}).showDesktopDragHandles;
|
|
||||||
} else if (window.localStorage.getItem('showDesktopDragHandles')) {
|
|
||||||
//
|
|
||||||
if (window.localStorage.getItem('showDesktopDragHandles')) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
*/
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// returns if mini screen or desktop drag handles
|
// returns if mini screen or desktop drag handles
|
||||||
isTouchScreenOrShowDesktopDragHandles() {
|
isTouchScreenOrShowDesktopDragHandles() {
|
||||||
// Always enable drag handles for mobile screens (touch devices)
|
// Always enable drag handles for all displays
|
||||||
return this.isTouchScreen() || this.isMiniScreen();
|
return true;
|
||||||
//return this.isTouchScreen() || this.isShowDesktopDragHandles();
|
|
||||||
//return this.isShowDesktopDragHandles();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
calculateIndexData(prevData, nextData, nItems = 1) {
|
calculateIndexData(prevData, nextData, nItems = 1) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { TAPi18n } from '/imports/i18n';
|
import { TAPi18n } from '/imports/i18n';
|
||||||
|
|
||||||
const passwordField = AccountsTemplates.removeField('password');
|
const passwordField = AccountsTemplates.removeField('password');
|
||||||
|
passwordField.autocomplete = 'current-password';
|
||||||
const emailField = AccountsTemplates.removeField('email');
|
const emailField = AccountsTemplates.removeField('email');
|
||||||
let disableRegistration = false;
|
let disableRegistration = false;
|
||||||
let disableForgotPassword = false;
|
let disableForgotPassword = false;
|
||||||
|
|
@ -61,6 +62,14 @@ AccountsTemplates.addFields([
|
||||||
},
|
},
|
||||||
emailField,
|
emailField,
|
||||||
passwordField,
|
passwordField,
|
||||||
|
{
|
||||||
|
_id: 'password_again',
|
||||||
|
type: 'password',
|
||||||
|
displayName: 'Password (again)',
|
||||||
|
required: true,
|
||||||
|
minLength: 6,
|
||||||
|
autocomplete: 'new-password',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
_id: 'invitationcode',
|
_id: 'invitationcode',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|
|
||||||
103
docs/Platforms/FOSS/Snap/Snap-build.md
Normal file
103
docs/Platforms/FOSS/Snap/Snap-build.md
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
# Building the Wekan snap without timeouts
|
||||||
|
|
||||||
|
This guide focuses on macOS hosts (Multipass VM) and common timeout fixes. It also applies to Linux hosts with LXD.
|
||||||
|
|
||||||
|
## Quick options
|
||||||
|
|
||||||
|
- Fastest: use Canonical builders (no local VM)
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
# One-time: login to the store (required for remote-build)
|
||||||
|
snapcraft login
|
||||||
|
|
||||||
|
# Build for amd64 on Canonical builders
|
||||||
|
snapcraft remote-build --build-for=amd64
|
||||||
|
```
|
||||||
|
|
||||||
|
- Local VM (macOS + Multipass): increase resources and build verbosely
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
# Give the builder more CPU/RAM/disk to avoid sluggish downloads
|
||||||
|
export SNAPCRAFT_BUILD_ENVIRONMENT=hosted-multipass
|
||||||
|
export SNAPCRAFT_BUILD_ENVIRONMENT_CPU=4
|
||||||
|
export SNAPCRAFT_BUILD_ENVIRONMENT_MEMORY=8G
|
||||||
|
export SNAPCRAFT_BUILD_ENVIRONMENT_DISK=40G
|
||||||
|
|
||||||
|
# Clean previous state and build
|
||||||
|
snapcraft clean
|
||||||
|
snapcraft --verbose --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## What changed to reduce timeouts
|
||||||
|
|
||||||
|
- Downloads in `wekan` part now retry with exponential backoff.
|
||||||
|
- `caddy` part now attempts APT with retries and falls back to a static binary from the official GitHub release if APT is slow or unreachable.
|
||||||
|
|
||||||
|
These changes make the build resilient to transient network issues and slow mirrors.
|
||||||
|
|
||||||
|
## Diagnosing where it stalls
|
||||||
|
|
||||||
|
- Run a single step for a part to reproduce:
|
||||||
|
```zsh
|
||||||
|
snapcraft pull wekan -v
|
||||||
|
snapcraft build wekan -v
|
||||||
|
```
|
||||||
|
- Drop into the build environment when it fails:
|
||||||
|
```zsh
|
||||||
|
snapcraft --debug
|
||||||
|
# Then run the failing commands manually
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tips for macOS + Multipass
|
||||||
|
|
||||||
|
- Check networking:
|
||||||
|
```zsh
|
||||||
|
multipass list
|
||||||
|
multipass exec snapcraft-*-wekan -- ping -c2 github.com
|
||||||
|
```
|
||||||
|
- If the instance looks wedged, recreate it:
|
||||||
|
```zsh
|
||||||
|
snapcraft clean --use-lxd || true # harmless on macOS
|
||||||
|
snapcraft clean --step pull
|
||||||
|
multipass delete --purge $(multipass list | awk '/snapcraft-/{print $1}')
|
||||||
|
snapcraft --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
## Linux hosts (optional)
|
||||||
|
|
||||||
|
On Linux, using LXD is often faster and more reliable than Multipass:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo snap install lxd --channel=5.21/stable
|
||||||
|
newgrp lxd
|
||||||
|
snapcraft --use-lxd -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common environment knobs
|
||||||
|
|
||||||
|
- Proxy/mirror environments inside the build VM if needed:
|
||||||
|
```zsh
|
||||||
|
export http_proxy=http://proxy.example:3128
|
||||||
|
export https_proxy=$http_proxy
|
||||||
|
export SNAPCRAFT_PROXY_HTTP=$http_proxy
|
||||||
|
export SNAPCRAFT_PROXY_HTTPS=$https_proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
- Speed up apt by pinning retries (already set in the recipe) or switching to a closer mirror by customizing sources in an override if needed.
|
||||||
|
|
||||||
|
## Cleaning up caches
|
||||||
|
|
||||||
|
If repeated attempts keep hitting corrupt downloads, clean Snapcraft caches:
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
snapcraft clean --destructive-mode || true
|
||||||
|
rm -rf ~/.cache/snapcraft/*
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reporting
|
||||||
|
|
||||||
|
If you still hit timeouts, capture and share:
|
||||||
|
- The exact step (pull/build/stage/prime) and part name
|
||||||
|
- Output of `snapcraft --verbose --debug`
|
||||||
|
- Host OS and Snapcraft version: `snapcraft --version`
|
||||||
|
- Multipass resources: `multipass list`
|
||||||
|
|
@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
|
||||||
|
|
||||||
Right click and download files 1-4:
|
Right click and download files 1-4:
|
||||||
|
|
||||||
1. [wekan-8.05-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.05/wekan-8.05-amd64-windows.zip)
|
1. [wekan-8.17-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.17/wekan-8.17-amd64-windows.zip)
|
||||||
|
|
||||||
2. [node.exe](https://nodejs.org/dist/latest-v14.x/win-x64/node.exe)
|
2. [node.exe](https://nodejs.org/dist/latest-v14.x/win-x64/node.exe)
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ Right click and download files 1-4:
|
||||||
|
|
||||||
6. Double click `mongodb-windows-x86_64-7.0.25-signed.msi` . In installer, uncheck downloading MongoDB compass.
|
6. Double click `mongodb-windows-x86_64-7.0.25-signed.msi` . In installer, uncheck downloading MongoDB compass.
|
||||||
|
|
||||||
7. Unzip `wekan-8.05-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files:
|
7. Unzip `wekan-8.17-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files:
|
||||||
|
|
||||||
```
|
```
|
||||||
bundle (directory)
|
bundle (directory)
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "deleted comment %s",
|
"activity-deleteComment": "deleted comment %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Templates",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Add Attachment",
|
"add-attachment": "Add Attachment",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Collapse",
|
"board-view-collapse": "Collapse",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lists",
|
"board-view-lists": "Lists",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"card-archived": "This card is moved to Archive.",
|
"card-archived": "This card is moved to Archive.",
|
||||||
"board-archived": "This board is moved to Archive.",
|
"board-archived": "This board is moved to Archive.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"createBoardPopup-title": "Create Board",
|
"createBoardPopup-title": "Create Board",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Import board",
|
"chooseBoardSourcePopup-title": "Import board",
|
||||||
"createLabelPopup-title": "Create Label",
|
"createLabelPopup-title": "Create Label",
|
||||||
"createCustomField": "Create Field",
|
"createCustomField": "Create Field",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "Default avatar",
|
"default-avatar": "Default avatar",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edit Notification",
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "deleted comment %s",
|
"activity-deleteComment": "deleted comment %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Templates",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Add Attachment",
|
"add-attachment": "Add Attachment",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Collapse",
|
"board-view-collapse": "Collapse",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lists",
|
"board-view-lists": "Lists",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"card-archived": "This card is moved to Archive.",
|
"card-archived": "This card is moved to Archive.",
|
||||||
"board-archived": "This board is moved to Archive.",
|
"board-archived": "This board is moved to Archive.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"createBoardPopup-title": "Create Board",
|
"createBoardPopup-title": "Create Board",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Import board",
|
"chooseBoardSourcePopup-title": "Import board",
|
||||||
"createLabelPopup-title": "Create Label",
|
"createLabelPopup-title": "Create Label",
|
||||||
"createCustomField": "Create Field",
|
"createCustomField": "Create Field",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "Default avatar",
|
"default-avatar": "Default avatar",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edit Notification",
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "deleted comment %s",
|
"activity-deleteComment": "deleted comment %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Templates",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Add Attachment",
|
"add-attachment": "Add Attachment",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Collapse",
|
"board-view-collapse": "Collapse",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lists",
|
"board-view-lists": "Lists",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"card-archived": "This card is moved to Archive.",
|
"card-archived": "This card is moved to Archive.",
|
||||||
"board-archived": "This board is moved to Archive.",
|
"board-archived": "This board is moved to Archive.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"createBoardPopup-title": "Create Board",
|
"createBoardPopup-title": "Create Board",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Import board",
|
"chooseBoardSourcePopup-title": "Import board",
|
||||||
"createLabelPopup-title": "Create Label",
|
"createLabelPopup-title": "Create Label",
|
||||||
"createCustomField": "Create Field",
|
"createCustomField": "Create Field",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "Default avatar",
|
"default-avatar": "Default avatar",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edit Notification",
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "deleted comment %s",
|
"activity-deleteComment": "deleted comment %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Templates",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Add Attachment",
|
"add-attachment": "Add Attachment",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Collapse",
|
"board-view-collapse": "Collapse",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lists",
|
"board-view-lists": "Lists",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"card-archived": "This card is moved to Archive.",
|
"card-archived": "This card is moved to Archive.",
|
||||||
"board-archived": "This board is moved to Archive.",
|
"board-archived": "This board is moved to Archive.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"createBoardPopup-title": "Create Board",
|
"createBoardPopup-title": "Create Board",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Import board",
|
"chooseBoardSourcePopup-title": "Import board",
|
||||||
"createLabelPopup-title": "Create Label",
|
"createLabelPopup-title": "Create Label",
|
||||||
"createCustomField": "Create Field",
|
"createCustomField": "Create Field",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "Default avatar",
|
"default-avatar": "Default avatar",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edit Notification",
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "تعليق محذوف %s",
|
"activity-deleteComment": "تعليق محذوف %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "نماذج",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "إضافة مرفق",
|
"add-attachment": "إضافة مرفق",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "انهيار",
|
"board-view-collapse": "انهيار",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "القائمات",
|
"board-view-lists": "القائمات",
|
||||||
"bucket-example": "مثل « todo list » على سبيل المثال",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "إلغاء",
|
"cancel": "إلغاء",
|
||||||
"card-archived": "البطاقة منقولة الى الارشيف",
|
"card-archived": "البطاقة منقولة الى الارشيف",
|
||||||
"board-archived": "اللوحات منقولة الى الارشيف",
|
"board-archived": "اللوحات منقولة الى الارشيف",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "إنشاء",
|
"create": "إنشاء",
|
||||||
"createBoardPopup-title": "إنشاء لوحة",
|
"createBoardPopup-title": "إنشاء لوحة",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "استيراد لوحة",
|
"chooseBoardSourcePopup-title": "استيراد لوحة",
|
||||||
"createLabelPopup-title": "إنشاء علامة",
|
"createLabelPopup-title": "إنشاء علامة",
|
||||||
"createCustomField": "انشاء حقل",
|
"createCustomField": "انشاء حقل",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "نص",
|
"custom-field-text": "نص",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "تاريخ",
|
"date": "تاريخ",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "صورة شخصية افتراضية",
|
"default-avatar": "صورة شخصية افتراضية",
|
||||||
"delete": "حذف",
|
"delete": "حذف",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "تصحيح الإشعار",
|
"editNotificationPopup-title": "تصحيح الإشعار",
|
||||||
"editProfilePopup-title": "تعديل الملف الشخصي",
|
"editProfilePopup-title": "تعديل الملف الشخصي",
|
||||||
"email": "البريد الإلكتروني",
|
"email": "البريد الإلكتروني",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "انا",
|
"dueCardsViewChange-choice-me": "انا",
|
||||||
"dueCardsViewChange-choice-all": "كل المستخدمين",
|
"dueCardsViewChange-choice-all": "كل المستخدمين",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "تفاصيل",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "deleted comment %s",
|
"activity-deleteComment": "deleted comment %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Templates",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Add Attachment",
|
"add-attachment": "Add Attachment",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Collapse",
|
"board-view-collapse": "Collapse",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lists",
|
"board-view-lists": "Lists",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"card-archived": "This card is moved to Archive.",
|
"card-archived": "This card is moved to Archive.",
|
||||||
"board-archived": "This board is moved to Archive.",
|
"board-archived": "This board is moved to Archive.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"createBoardPopup-title": "Create Board",
|
"createBoardPopup-title": "Create Board",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Import board",
|
"chooseBoardSourcePopup-title": "Import board",
|
||||||
"createLabelPopup-title": "Create Label",
|
"createLabelPopup-title": "Create Label",
|
||||||
"createCustomField": "Create Field",
|
"createCustomField": "Create Field",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "Default avatar",
|
"default-avatar": "Default avatar",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edit Notification",
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "deleted comment %s",
|
"activity-deleteComment": "deleted comment %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Templates",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Add Attachment",
|
"add-attachment": "Add Attachment",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Collapse",
|
"board-view-collapse": "Collapse",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lists",
|
"board-view-lists": "Lists",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"card-archived": "This card is moved to Archive.",
|
"card-archived": "This card is moved to Archive.",
|
||||||
"board-archived": "This board is moved to Archive.",
|
"board-archived": "This board is moved to Archive.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"createBoardPopup-title": "Create Board",
|
"createBoardPopup-title": "Create Board",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Import board",
|
"chooseBoardSourcePopup-title": "Import board",
|
||||||
"createLabelPopup-title": "Create Label",
|
"createLabelPopup-title": "Create Label",
|
||||||
"createCustomField": "Create Field",
|
"createCustomField": "Create Field",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "Default avatar",
|
"default-avatar": "Default avatar",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edit Notification",
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "deleted comment %s",
|
"activity-deleteComment": "deleted comment %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Templates",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Add Attachment",
|
"add-attachment": "Add Attachment",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Collapse",
|
"board-view-collapse": "Collapse",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lists",
|
"board-view-lists": "Lists",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"card-archived": "This card is moved to Archive.",
|
"card-archived": "This card is moved to Archive.",
|
||||||
"board-archived": "This board is moved to Archive.",
|
"board-archived": "This board is moved to Archive.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"createBoardPopup-title": "Create Board",
|
"createBoardPopup-title": "Create Board",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Import board",
|
"chooseBoardSourcePopup-title": "Import board",
|
||||||
"createLabelPopup-title": "Create Label",
|
"createLabelPopup-title": "Create Label",
|
||||||
"createCustomField": "Create Field",
|
"createCustomField": "Create Field",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "Default avatar",
|
"default-avatar": "Default avatar",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edit Notification",
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "deleted comment %s",
|
"activity-deleteComment": "deleted comment %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Templates",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Add Attachment",
|
"add-attachment": "Add Attachment",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Collapse",
|
"board-view-collapse": "Collapse",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lists",
|
"board-view-lists": "Lists",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"card-archived": "This card is moved to Archive.",
|
"card-archived": "This card is moved to Archive.",
|
||||||
"board-archived": "This board is moved to Archive.",
|
"board-archived": "This board is moved to Archive.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"createBoardPopup-title": "Create Board",
|
"createBoardPopup-title": "Create Board",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Import board",
|
"chooseBoardSourcePopup-title": "Import board",
|
||||||
"createLabelPopup-title": "Create Label",
|
"createLabelPopup-title": "Create Label",
|
||||||
"createCustomField": "Create Field",
|
"createCustomField": "Create Field",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "Default avatar",
|
"default-avatar": "Default avatar",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edit Notification",
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "deleted comment %s",
|
"activity-deleteComment": "deleted comment %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Templates",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Add Attachment",
|
"add-attachment": "Add Attachment",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Collapse",
|
"board-view-collapse": "Collapse",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lists",
|
"board-view-lists": "Lists",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"card-archived": "This card is moved to Archive.",
|
"card-archived": "This card is moved to Archive.",
|
||||||
"board-archived": "This board is moved to Archive.",
|
"board-archived": "This board is moved to Archive.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"createBoardPopup-title": "Create Board",
|
"createBoardPopup-title": "Create Board",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Import board",
|
"chooseBoardSourcePopup-title": "Import board",
|
||||||
"createLabelPopup-title": "Create Label",
|
"createLabelPopup-title": "Create Label",
|
||||||
"createCustomField": "Create Field",
|
"createCustomField": "Create Field",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "Default avatar",
|
"default-avatar": "Default avatar",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edit Notification",
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "изтрит коментар %s",
|
"activity-deleteComment": "изтрит коментар %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Шаблони",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Добави прикачен файл",
|
"add-attachment": "Добави прикачен файл",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Събери",
|
"board-view-collapse": "Събери",
|
||||||
"board-view-gantt": "План",
|
"board-view-gantt": "План",
|
||||||
"board-view-lists": "Списъци",
|
"board-view-lists": "Списъци",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Отмени",
|
"cancel": "Отмени",
|
||||||
"card-archived": "Тази карта е преместена в Архива.",
|
"card-archived": "Тази карта е преместена в Архива.",
|
||||||
"board-archived": "Това табло е преместено в Архива.",
|
"board-archived": "Това табло е преместено в Архива.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Създай",
|
"create": "Създай",
|
||||||
"createBoardPopup-title": "Създай Табло",
|
"createBoardPopup-title": "Създай Табло",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Импортирай Табло",
|
"chooseBoardSourcePopup-title": "Импортирай Табло",
|
||||||
"createLabelPopup-title": "Създай Табло",
|
"createLabelPopup-title": "Създай Табло",
|
||||||
"createCustomField": "Създай Поле",
|
"createCustomField": "Създай Поле",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Текст",
|
"custom-field-text": "Текст",
|
||||||
"custom-fields": "Собствени полета",
|
"custom-fields": "Собствени полета",
|
||||||
"date": "Дата",
|
"date": "Дата",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Отказ",
|
"decline": "Отказ",
|
||||||
"default-avatar": "Основен аватар",
|
"default-avatar": "Основен аватар",
|
||||||
"delete": "Изтрий",
|
"delete": "Изтрий",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Промени известията",
|
"editNotificationPopup-title": "Промени известията",
|
||||||
"editProfilePopup-title": "Промяна на профила",
|
"editProfilePopup-title": "Промяна на профила",
|
||||||
"email": "Имейл",
|
"email": "Имейл",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "Ваш профил беше създаден на __siteName__",
|
"email-enrollAccount-subject": "Ваш профил беше създаден на __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Неуспешно изпращане на имейла",
|
"email-fail": "Неуспешно изпращане на имейла",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "Всички списъци, карти, имена и действия ще бъдат изтрити и няма да можете да възстановите съдържанието на дъската. Няма връщане назад.",
|
"delete-board-confirm-popup": "Всички списъци, карти, имена и действия ще бъдат изтрити и няма да можете да възстановите съдържанието на дъската. Няма връщане назад.",
|
||||||
"boardDeletePopup-title": "Изтриване на Таблото?",
|
"boardDeletePopup-title": "Изтриване на Таблото?",
|
||||||
"delete-board": "Изтрий таблото",
|
"delete-board": "Изтрий таблото",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Подзадачи за табло __board__",
|
"default-subtasks-board": "Подзадачи за табло __board__",
|
||||||
"default": "по подразбиране",
|
"default": "по подразбиране",
|
||||||
"defaultdefault": "по подразбиране",
|
"defaultdefault": "по подразбиране",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Настройки на Подзадачите",
|
"subtask-settings": "Настройки на Подзадачите",
|
||||||
"card-settings": "Настройки на Карта",
|
"card-settings": "Настройки на Карта",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Настройки за Подзадачите за това Табло",
|
"boardSubtaskSettingsPopup-title": "Настройки на Подзадачите",
|
||||||
"boardCardSettingsPopup-title": "Настройки на Карта",
|
"boardCardSettingsPopup-title": "Настройки на Карта",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Вложете под-задачи към тази дъска:",
|
"deposit-subtasks-board": "Вложете под-задачи към тази дъска:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Състояние",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Завършено",
|
"completed": "Завършено",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "deleted comment %s",
|
"activity-deleteComment": "deleted comment %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Templates",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Add Attachment",
|
"add-attachment": "Add Attachment",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Collapse",
|
"board-view-collapse": "Collapse",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lists",
|
"board-view-lists": "Lists",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"card-archived": "This card is moved to Archive.",
|
"card-archived": "This card is moved to Archive.",
|
||||||
"board-archived": "This board is moved to Archive.",
|
"board-archived": "This board is moved to Archive.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Krouiñ",
|
"create": "Krouiñ",
|
||||||
"createBoardPopup-title": "Create Board",
|
"createBoardPopup-title": "Create Board",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Import board",
|
"chooseBoardSourcePopup-title": "Import board",
|
||||||
"createLabelPopup-title": "Create Label",
|
"createLabelPopup-title": "Create Label",
|
||||||
"createCustomField": "Create Field",
|
"createCustomField": "Create Field",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "Default avatar",
|
"default-avatar": "Default avatar",
|
||||||
"delete": "Diverkañ",
|
"delete": "Diverkañ",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edit Notification",
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "Ha esborrar el comentari %s",
|
"activity-deleteComment": "Ha esborrar el comentari %s",
|
||||||
"activity-receivedDate": "editat la data de recepció a %s de %s",
|
"activity-receivedDate": "editat la data de recepció a %s de %s",
|
||||||
"activity-startDate": "data d'inici editada a %s de %s",
|
"activity-startDate": "data d'inici editada a %s de %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Plantilles",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "data de venciment editada a %s de %s",
|
"activity-dueDate": "data de venciment editada a %s de %s",
|
||||||
"activity-endDate": "data de finalització editada a %s de %s",
|
"activity-endDate": "data de finalització editada a %s de %s",
|
||||||
"add-attachment": "Afegeix adjunt",
|
"add-attachment": "Afegeix adjunt",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Contraure",
|
"board-view-collapse": "Contraure",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Llistes",
|
"board-view-lists": "Llistes",
|
||||||
"bucket-example": "Igual que “Bucket List”, per exemple",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel·la",
|
"cancel": "Cancel·la",
|
||||||
"card-archived": "Aquesta fitxa ha estat moguda al Arxiu.",
|
"card-archived": "Aquesta fitxa ha estat moguda al Arxiu.",
|
||||||
"board-archived": "Aquest tauler s'ha mogut a l'arxiu",
|
"board-archived": "Aquest tauler s'ha mogut a l'arxiu",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"Títol de la primera fitxa\", \"description\":\"Descripció de la primera fitxa\"}, {\"title\":\"Títol de la segona fitxa\",\"description\":\"Descripció de la segona fitxa \"},{\"title\":\"Títol de l'última fitxa\",\"description\":\"Descripció de l'última fitxa\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"Títol de la primera fitxa\", \"description\":\"Descripció de la primera fitxa\"}, {\"title\":\"Títol de la segona fitxa\",\"description\":\"Descripció de la segona fitxa \"},{\"title\":\"Títol de l'última fitxa\",\"description\":\"Descripció de l'última fitxa\"} ]",
|
||||||
"create": "Crea",
|
"create": "Crea",
|
||||||
"createBoardPopup-title": "Crea tauler",
|
"createBoardPopup-title": "Crea tauler",
|
||||||
|
"createTemplateContainerPopup-title": "Afegeix un Contenidor de plantilles",
|
||||||
"chooseBoardSourcePopup-title": "Importa tauler",
|
"chooseBoardSourcePopup-title": "Importa tauler",
|
||||||
"createLabelPopup-title": "Crea etiqueta",
|
"createLabelPopup-title": "Crea etiqueta",
|
||||||
"createCustomField": "Crear campament",
|
"createCustomField": "Crear campament",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Camps Personalitzats",
|
"custom-fields": "Camps Personalitzats",
|
||||||
"date": "Dades",
|
"date": "Dades",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Declina",
|
"decline": "Declina",
|
||||||
"default-avatar": "Avatar per defecte",
|
"default-avatar": "Avatar per defecte",
|
||||||
"delete": "Esborra",
|
"delete": "Esborra",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edita la notificació",
|
"editNotificationPopup-title": "Edita la notificació",
|
||||||
"editProfilePopup-title": "Edita el teu Perfil",
|
"editProfilePopup-title": "Edita el teu Perfil",
|
||||||
"email": "Correu electrònic",
|
"email": "Correu electrònic",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "Un compte creat per a tu a __siteName__",
|
"email-enrollAccount-subject": "Un compte creat per a tu a __siteName__",
|
||||||
"email-enrollAccount-text": "Hola __user__,\n\nPer començar a utilitzar el servei, segueix l'enllaç següent.\n\n__url__\n\nGràcies.",
|
"email-enrollAccount-text": "Hola __user__,\n\nPer començar a utilitzar el servei, segueix l'enllaç següent.\n\n__url__\n\nGràcies.",
|
||||||
"email-fail": "Error enviant el correu",
|
"email-fail": "Error enviant el correu",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "Se suprimiran totes les llistes, fitxes, etiquetes i activitats i no podreu recuperar el contingut del tauler. No hi ha cap desfer.",
|
"delete-board-confirm-popup": "Se suprimiran totes les llistes, fitxes, etiquetes i activitats i no podreu recuperar el contingut del tauler. No hi ha cap desfer.",
|
||||||
"boardDeletePopup-title": "Vols suprimir el tauler?",
|
"boardDeletePopup-title": "Vols suprimir el tauler?",
|
||||||
"delete-board": "Suprimeix el tauler",
|
"delete-board": "Suprimeix el tauler",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasques per al tauler __board__",
|
"default-subtasks-board": "Subtasques per al tauler __board__",
|
||||||
"default": "Per defecte",
|
"default": "Per defecte",
|
||||||
"defaultdefault": "Per defecte",
|
"defaultdefault": "Per defecte",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Configuració de subtasques",
|
"subtask-settings": "Configuració de subtasques",
|
||||||
"card-settings": "Configuració de fitxa",
|
"card-settings": "Configuració de fitxa",
|
||||||
"minicard-settings": "Configuració de la minifitxa",
|
"minicard-settings": "Configuració de la minifitxa",
|
||||||
"boardSubtaskSettingsPopup-title": "Configuració de les subtasques del tauler",
|
"boardSubtaskSettingsPopup-title": "Configuració de subtasques",
|
||||||
"boardCardSettingsPopup-title": "Configuració de fitxa",
|
"boardCardSettingsPopup-title": "Configuració de fitxa",
|
||||||
"boardMinicardSettingsPopup-title": "Configuració de la minifitxa",
|
"boardMinicardSettingsPopup-title": "Configuració de la minifitxa",
|
||||||
"deposit-subtasks-board": "Diposita subtasques a aquest tauler:",
|
"deposit-subtasks-board": "Diposita subtasques a aquest tauler:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "jo",
|
"dueCardsViewChange-choice-me": "jo",
|
||||||
"dueCardsViewChange-choice-all": "Tots els usuaris",
|
"dueCardsViewChange-choice-all": "Tots els usuaris",
|
||||||
"dueCardsViewChange-choice-all-description": "Mostra totes les fitxes incompletes amb una *data de venciment* de taulers per als quals l'usuari té permís.",
|
"dueCardsViewChange-choice-all-description": "Mostra totes les fitxes incompletes amb una *data de venciment* de taulers per als quals l'usuari té permís.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Fitxes Trencades",
|
"broken-cards": "Fitxes Trencades",
|
||||||
"board-title-not-found": "No s'ha trobat el tauler '%s'.",
|
"board-title-not-found": "No s'ha trobat el tauler '%s'.",
|
||||||
"swimlane-title-not-found": "No s'ha trobat el carril '%s'.",
|
"swimlane-title-not-found": "No s'ha trobat el carril '%s'.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Estat",
|
||||||
|
"migration-progress-details": "Detalls",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completat",
|
"completed": "Completat",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "deleted comment %s",
|
"activity-deleteComment": "deleted comment %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Templates",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Add Attachment",
|
"add-attachment": "Add Attachment",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Collapse",
|
"board-view-collapse": "Collapse",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lists",
|
"board-view-lists": "Lists",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"card-archived": "This card is moved to Archive.",
|
"card-archived": "This card is moved to Archive.",
|
||||||
"board-archived": "This board is moved to Archive.",
|
"board-archived": "This board is moved to Archive.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"createBoardPopup-title": "Create Board",
|
"createBoardPopup-title": "Create Board",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Import board",
|
"chooseBoardSourcePopup-title": "Import board",
|
||||||
"createLabelPopup-title": "Create Label",
|
"createLabelPopup-title": "Create Label",
|
||||||
"createCustomField": "Create Field",
|
"createCustomField": "Create Field",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "Default avatar",
|
"default-avatar": "Default avatar",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edit Notification",
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "deleted comment %s",
|
"activity-deleteComment": "deleted comment %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Templates",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Add Attachment",
|
"add-attachment": "Add Attachment",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Collapse",
|
"board-view-collapse": "Collapse",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lists",
|
"board-view-lists": "Lists",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"card-archived": "This card is moved to Archive.",
|
"card-archived": "This card is moved to Archive.",
|
||||||
"board-archived": "This board is moved to Archive.",
|
"board-archived": "This board is moved to Archive.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"createBoardPopup-title": "Create Board",
|
"createBoardPopup-title": "Create Board",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Import board",
|
"chooseBoardSourcePopup-title": "Import board",
|
||||||
"createLabelPopup-title": "Create Label",
|
"createLabelPopup-title": "Create Label",
|
||||||
"createCustomField": "Create Field",
|
"createCustomField": "Create Field",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "Default avatar",
|
"default-avatar": "Default avatar",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edit Notification",
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "deleted comment %s",
|
"activity-deleteComment": "deleted comment %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Templates",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Add Attachment",
|
"add-attachment": "Add Attachment",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Collapse",
|
"board-view-collapse": "Collapse",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lists",
|
"board-view-lists": "Lists",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"card-archived": "This card is moved to Archive.",
|
"card-archived": "This card is moved to Archive.",
|
||||||
"board-archived": "This board is moved to Archive.",
|
"board-archived": "This board is moved to Archive.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"createBoardPopup-title": "Create Board",
|
"createBoardPopup-title": "Create Board",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Import board",
|
"chooseBoardSourcePopup-title": "Import board",
|
||||||
"createLabelPopup-title": "Create Label",
|
"createLabelPopup-title": "Create Label",
|
||||||
"createCustomField": "Create Field",
|
"createCustomField": "Create Field",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "Default avatar",
|
"default-avatar": "Default avatar",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edit Notification",
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "smazat komentář %s",
|
"activity-deleteComment": "smazat komentář %s",
|
||||||
"activity-receivedDate": "editoval(a) datum přijetí na %s z %s",
|
"activity-receivedDate": "editoval(a) datum přijetí na %s z %s",
|
||||||
"activity-startDate": "editoval(a) datum zahájení na %s z %s",
|
"activity-startDate": "editoval(a) datum zahájení na %s z %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Šablony",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "editoval(a) termín dokončení na %s z %s",
|
"activity-dueDate": "editoval(a) termín dokončení na %s z %s",
|
||||||
"activity-endDate": "editoval(a) datum ukončení na %s z %s",
|
"activity-endDate": "editoval(a) datum ukončení na %s z %s",
|
||||||
"add-attachment": "Přidat přílohu",
|
"add-attachment": "Přidat přílohu",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Sbalit",
|
"board-view-collapse": "Sbalit",
|
||||||
"board-view-gantt": "Gannt",
|
"board-view-gantt": "Gannt",
|
||||||
"board-view-lists": "Sloupce",
|
"board-view-lists": "Sloupce",
|
||||||
"bucket-example": "Například \"O čem sním\"",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Zrušit",
|
"cancel": "Zrušit",
|
||||||
"card-archived": "Karta byla přesunuta do archivu.",
|
"card-archived": "Karta byla přesunuta do archivu.",
|
||||||
"board-archived": "Toto tablo je přesunuto do archivu.",
|
"board-archived": "Toto tablo je přesunuto do archivu.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"Nadpis první karty\", \"description\":\"Popis první karty\"}, {\"title\":\"Nadpis druhé karty\",\"description\":\"Popis druhé karty\"},{\"title\":\"Nadpis poslední kary\",\"description\":\"Popis poslední karty\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"Nadpis první karty\", \"description\":\"Popis první karty\"}, {\"title\":\"Nadpis druhé karty\",\"description\":\"Popis druhé karty\"},{\"title\":\"Nadpis poslední kary\",\"description\":\"Popis poslední karty\"} ]",
|
||||||
"create": "Vytvořit",
|
"create": "Vytvořit",
|
||||||
"createBoardPopup-title": "Vytvořit tablo",
|
"createBoardPopup-title": "Vytvořit tablo",
|
||||||
|
"createTemplateContainerPopup-title": "Přidat kontejner šablony",
|
||||||
"chooseBoardSourcePopup-title": "Importovat tablo",
|
"chooseBoardSourcePopup-title": "Importovat tablo",
|
||||||
"createLabelPopup-title": "Vytvořit štítek",
|
"createLabelPopup-title": "Vytvořit štítek",
|
||||||
"createCustomField": "Vytvořit pole",
|
"createCustomField": "Vytvořit pole",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Vlastní pole",
|
"custom-fields": "Vlastní pole",
|
||||||
"date": "Datum",
|
"date": "Datum",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Zamítnout",
|
"decline": "Zamítnout",
|
||||||
"default-avatar": "Výchozí avatar",
|
"default-avatar": "Výchozí avatar",
|
||||||
"delete": "Smazat",
|
"delete": "Smazat",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Změnit notifikace",
|
"editNotificationPopup-title": "Změnit notifikace",
|
||||||
"editProfilePopup-title": "Upravit profil",
|
"editProfilePopup-title": "Upravit profil",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "Byl vytvořen účet na __siteName__",
|
"email-enrollAccount-subject": "Byl vytvořen účet na __siteName__",
|
||||||
"email-enrollAccount-text": "Ahoj __user__,\n\nMůžeš začít používat službu kliknutím na odkaz níže.\n\n__url__\n\nDěkujeme.",
|
"email-enrollAccount-text": "Ahoj __user__,\n\nMůžeš začít používat službu kliknutím na odkaz níže.\n\n__url__\n\nDěkujeme.",
|
||||||
"email-fail": "Odeslání emailu selhalo",
|
"email-fail": "Odeslání emailu selhalo",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "Všechny sloupce, štítky a aktivity budou smazány a obsah tabla nebude možné obnovit. Toto nelze vrátit zpět.",
|
"delete-board-confirm-popup": "Všechny sloupce, štítky a aktivity budou smazány a obsah tabla nebude možné obnovit. Toto nelze vrátit zpět.",
|
||||||
"boardDeletePopup-title": "Smazat tablo?",
|
"boardDeletePopup-title": "Smazat tablo?",
|
||||||
"delete-board": "Smazat tablo",
|
"delete-board": "Smazat tablo",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Podúkoly pro tablo __board__",
|
"default-subtasks-board": "Podúkoly pro tablo __board__",
|
||||||
"default": "Výchozí",
|
"default": "Výchozí",
|
||||||
"defaultdefault": "Výchozí",
|
"defaultdefault": "Výchozí",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Nastavení podúkolů",
|
"subtask-settings": "Nastavení podúkolů",
|
||||||
"card-settings": "Nastavení karet",
|
"card-settings": "Nastavení karet",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Nastavení podúkolů tabla",
|
"boardSubtaskSettingsPopup-title": "Nastavení podúkolů",
|
||||||
"boardCardSettingsPopup-title": "Nastavení karet",
|
"boardCardSettingsPopup-title": "Nastavení karet",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Vložit podúkoly do tohoto tabla",
|
"deposit-subtasks-board": "Vložit podúkoly do tohoto tabla",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Moje",
|
"dueCardsViewChange-choice-me": "Moje",
|
||||||
"dueCardsViewChange-choice-all": "Všechny",
|
"dueCardsViewChange-choice-all": "Všechny",
|
||||||
"dueCardsViewChange-choice-all-description": "Zobrazí všechny nedokončené karty s *Termínem dokončení* z každého tabla, ke kterému má uživatel oprávnění.",
|
"dueCardsViewChange-choice-all-description": "Zobrazí všechny nedokončené karty s *Termínem dokončení* z každého tabla, ke kterému má uživatel oprávnění.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Rozbité karty",
|
"broken-cards": "Rozbité karty",
|
||||||
"board-title-not-found": "Tablo '%s' nenalezeno.",
|
"board-title-not-found": "Tablo '%s' nenalezeno.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' nenalezena.",
|
"swimlane-title-not-found": "Swimlane '%s' nenalezena.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Stav",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Dokončeno",
|
"completed": "Dokončeno",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "smazat komentář %s",
|
"activity-deleteComment": "smazat komentář %s",
|
||||||
"activity-receivedDate": "editoval(a) datum přijetí na %s z %s",
|
"activity-receivedDate": "editoval(a) datum přijetí na %s z %s",
|
||||||
"activity-startDate": "editoval(a) datum zahájení na %s z %s",
|
"activity-startDate": "editoval(a) datum zahájení na %s z %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Šablony",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "editoval(a) termín dokončení na %s z %s",
|
"activity-dueDate": "editoval(a) termín dokončení na %s z %s",
|
||||||
"activity-endDate": "editoval(a) datum ukončení na %s z %s",
|
"activity-endDate": "editoval(a) datum ukončení na %s z %s",
|
||||||
"add-attachment": "Přidat přílohu",
|
"add-attachment": "Přidat přílohu",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Sbalit",
|
"board-view-collapse": "Sbalit",
|
||||||
"board-view-gantt": "Gannt",
|
"board-view-gantt": "Gannt",
|
||||||
"board-view-lists": "Sloupce",
|
"board-view-lists": "Sloupce",
|
||||||
"bucket-example": "Například \"O čem sním\"",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Zrušit",
|
"cancel": "Zrušit",
|
||||||
"card-archived": "Karta byla přesunuta do archivu.",
|
"card-archived": "Karta byla přesunuta do archivu.",
|
||||||
"board-archived": "Toto tablo je přesunuto do archivu.",
|
"board-archived": "Toto tablo je přesunuto do archivu.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"Nadpis první karty\", \"description\":\"Popis první karty\"}, {\"title\":\"Nadpis druhé karty\",\"description\":\"Popis druhé karty\"},{\"title\":\"Nadpis poslední kary\",\"description\":\"Popis poslední karty\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"Nadpis první karty\", \"description\":\"Popis první karty\"}, {\"title\":\"Nadpis druhé karty\",\"description\":\"Popis druhé karty\"},{\"title\":\"Nadpis poslední kary\",\"description\":\"Popis poslední karty\"} ]",
|
||||||
"create": "Vytvořit",
|
"create": "Vytvořit",
|
||||||
"createBoardPopup-title": "Vytvořit tablo",
|
"createBoardPopup-title": "Vytvořit tablo",
|
||||||
|
"createTemplateContainerPopup-title": "Přidat kontejner šablony",
|
||||||
"chooseBoardSourcePopup-title": "Importovat tablo",
|
"chooseBoardSourcePopup-title": "Importovat tablo",
|
||||||
"createLabelPopup-title": "Vytvořit štítek",
|
"createLabelPopup-title": "Vytvořit štítek",
|
||||||
"createCustomField": "Vytvořit pole",
|
"createCustomField": "Vytvořit pole",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Vlastní pole",
|
"custom-fields": "Vlastní pole",
|
||||||
"date": "Datum",
|
"date": "Datum",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Zamítnout",
|
"decline": "Zamítnout",
|
||||||
"default-avatar": "Výchozí avatar",
|
"default-avatar": "Výchozí avatar",
|
||||||
"delete": "Smazat",
|
"delete": "Smazat",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Změnit notifikace",
|
"editNotificationPopup-title": "Změnit notifikace",
|
||||||
"editProfilePopup-title": "Upravit profil",
|
"editProfilePopup-title": "Upravit profil",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "Byl vytvořen účet na __siteName__",
|
"email-enrollAccount-subject": "Byl vytvořen účet na __siteName__",
|
||||||
"email-enrollAccount-text": "Ahoj __user__,\n\nMůžeš začít používat službu kliknutím na odkaz níže.\n\n__url__\n\nDěkujeme.",
|
"email-enrollAccount-text": "Ahoj __user__,\n\nMůžeš začít používat službu kliknutím na odkaz níže.\n\n__url__\n\nDěkujeme.",
|
||||||
"email-fail": "Odeslání emailu selhalo",
|
"email-fail": "Odeslání emailu selhalo",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "Všechny sloupce, štítky a aktivity budou smazány a obsah tabla nebude možné obnovit. Toto nelze vrátit zpět.",
|
"delete-board-confirm-popup": "Všechny sloupce, štítky a aktivity budou smazány a obsah tabla nebude možné obnovit. Toto nelze vrátit zpět.",
|
||||||
"boardDeletePopup-title": "Smazat tablo?",
|
"boardDeletePopup-title": "Smazat tablo?",
|
||||||
"delete-board": "Smazat tablo",
|
"delete-board": "Smazat tablo",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Podúkoly pro tablo __board__",
|
"default-subtasks-board": "Podúkoly pro tablo __board__",
|
||||||
"default": "Výchozí",
|
"default": "Výchozí",
|
||||||
"defaultdefault": "Výchozí",
|
"defaultdefault": "Výchozí",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Nastavení podúkolů",
|
"subtask-settings": "Nastavení podúkolů",
|
||||||
"card-settings": "Nastavení karet",
|
"card-settings": "Nastavení karet",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Nastavení podúkolů tabla",
|
"boardSubtaskSettingsPopup-title": "Nastavení podúkolů",
|
||||||
"boardCardSettingsPopup-title": "Nastavení karet",
|
"boardCardSettingsPopup-title": "Nastavení karet",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Vložit podúkoly do tohoto tabla",
|
"deposit-subtasks-board": "Vložit podúkoly do tohoto tabla",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Moje",
|
"dueCardsViewChange-choice-me": "Moje",
|
||||||
"dueCardsViewChange-choice-all": "Všechny",
|
"dueCardsViewChange-choice-all": "Všechny",
|
||||||
"dueCardsViewChange-choice-all-description": "Zobrazí všechny nedokončené karty s *Termínem dokončení* z každého tabla, ke kterému má uživatel oprávnění.",
|
"dueCardsViewChange-choice-all-description": "Zobrazí všechny nedokončené karty s *Termínem dokončení* z každého tabla, ke kterému má uživatel oprávnění.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Rozbité karty",
|
"broken-cards": "Rozbité karty",
|
||||||
"board-title-not-found": "Tablo '%s' nenalezeno.",
|
"board-title-not-found": "Tablo '%s' nenalezeno.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' nenalezena.",
|
"swimlane-title-not-found": "Swimlane '%s' nenalezena.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Stav",
|
||||||
|
"migration-progress-details": "Podrobnosti",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Dokončeno",
|
"completed": "Dokončeno",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "deleted comment %s",
|
"activity-deleteComment": "deleted comment %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Templates",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Add Attachment",
|
"add-attachment": "Add Attachment",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Collapse",
|
"board-view-collapse": "Collapse",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lists",
|
"board-view-lists": "Lists",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"card-archived": "This card is moved to Archive.",
|
"card-archived": "This card is moved to Archive.",
|
||||||
"board-archived": "This board is moved to Archive.",
|
"board-archived": "This board is moved to Archive.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"createBoardPopup-title": "Create Board",
|
"createBoardPopup-title": "Create Board",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Import board",
|
"chooseBoardSourcePopup-title": "Import board",
|
||||||
"createLabelPopup-title": "Create Label",
|
"createLabelPopup-title": "Create Label",
|
||||||
"createCustomField": "Create Field",
|
"createCustomField": "Create Field",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "Default avatar",
|
"default-avatar": "Default avatar",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edit Notification",
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "deleted comment %s",
|
"activity-deleteComment": "deleted comment %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Templates",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Add Attachment",
|
"add-attachment": "Add Attachment",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Collapse",
|
"board-view-collapse": "Collapse",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lists",
|
"board-view-lists": "Lists",
|
||||||
"bucket-example": "Like “Bucket List” for example",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"card-archived": "This card is moved to Archive.",
|
"card-archived": "This card is moved to Archive.",
|
||||||
"board-archived": "This board is moved to Archive.",
|
"board-archived": "This board is moved to Archive.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"createBoardPopup-title": "Create Board",
|
"createBoardPopup-title": "Create Board",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Import board",
|
"chooseBoardSourcePopup-title": "Import board",
|
||||||
"createLabelPopup-title": "Create Label",
|
"createLabelPopup-title": "Create Label",
|
||||||
"createCustomField": "Create Field",
|
"createCustomField": "Create Field",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Custom Fields",
|
"custom-fields": "Custom Fields",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Decline",
|
"decline": "Decline",
|
||||||
"default-avatar": "Default avatar",
|
"default-avatar": "Default avatar",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Edit Notification",
|
"editNotificationPopup-title": "Edit Notification",
|
||||||
"editProfilePopup-title": "Edit Profile",
|
"editProfilePopup-title": "Edit Profile",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
"email-enrollAccount-subject": "An account created for you on __siteName__",
|
||||||
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
"email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.",
|
||||||
"email-fail": "Sending email failed",
|
"email-fail": "Sending email failed",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
"delete-board-confirm-popup": "All lists, cards, labels, and activities will be deleted and you won't be able to recover the board contents. There is no undo.",
|
||||||
"boardDeletePopup-title": "Delete Board?",
|
"boardDeletePopup-title": "Delete Board?",
|
||||||
"delete-board": "Delete Board",
|
"delete-board": "Delete Board",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Subtasks for __board__ board",
|
"default-subtasks-board": "Subtasks for __board__ board",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"defaultdefault": "Default",
|
"defaultdefault": "Default",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Subtasks Settings",
|
"subtask-settings": "Subtasks Settings",
|
||||||
"card-settings": "Card Settings",
|
"card-settings": "Card Settings",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
|
"boardSubtaskSettingsPopup-title": "Subtasks Settings",
|
||||||
"boardCardSettingsPopup-title": "Card Settings",
|
"boardCardSettingsPopup-title": "Card Settings",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
"deposit-subtasks-board": "Deposit subtasks to this board:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "slettede kommentar %s",
|
"activity-deleteComment": "slettede kommentar %s",
|
||||||
"activity-receivedDate": "edited received date to %s of %s",
|
"activity-receivedDate": "edited received date to %s of %s",
|
||||||
"activity-startDate": "edited start date to %s of %s",
|
"activity-startDate": "edited start date to %s of %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Skabeloner",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "edited due date to %s of %s",
|
"activity-dueDate": "edited due date to %s of %s",
|
||||||
"activity-endDate": "edited end date to %s of %s",
|
"activity-endDate": "edited end date to %s of %s",
|
||||||
"add-attachment": "Tilføj vedhæftning",
|
"add-attachment": "Tilføj vedhæftning",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Sammenfold",
|
"board-view-collapse": "Sammenfold",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Lister",
|
"board-view-lists": "Lister",
|
||||||
"bucket-example": "Eksempelvis \"Bucked-liste\"",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Annullér",
|
"cancel": "Annullér",
|
||||||
"card-archived": "Dette kort blev flyttet til arkivet.",
|
"card-archived": "Dette kort blev flyttet til arkivet.",
|
||||||
"board-archived": "Denne tavle blev flyttet til arkivet.",
|
"board-archived": "Denne tavle blev flyttet til arkivet.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]",
|
||||||
"create": "Opret",
|
"create": "Opret",
|
||||||
"createBoardPopup-title": "Opret tavle",
|
"createBoardPopup-title": "Opret tavle",
|
||||||
|
"createTemplateContainerPopup-title": "Add Template Container",
|
||||||
"chooseBoardSourcePopup-title": "Importér tavle",
|
"chooseBoardSourcePopup-title": "Importér tavle",
|
||||||
"createLabelPopup-title": "Opret etikette",
|
"createLabelPopup-title": "Opret etikette",
|
||||||
"createCustomField": "Opret felt",
|
"createCustomField": "Opret felt",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Tekst",
|
"custom-field-text": "Tekst",
|
||||||
"custom-fields": "Brugerdefinerede felter",
|
"custom-fields": "Brugerdefinerede felter",
|
||||||
"date": "Dato",
|
"date": "Dato",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Afslå",
|
"decline": "Afslå",
|
||||||
"default-avatar": "Standard-avatar",
|
"default-avatar": "Standard-avatar",
|
||||||
"delete": "Slet",
|
"delete": "Slet",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Redigér notifikation",
|
"editNotificationPopup-title": "Redigér notifikation",
|
||||||
"editProfilePopup-title": "Redigér profil",
|
"editProfilePopup-title": "Redigér profil",
|
||||||
"email": "E-mail",
|
"email": "E-mail",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "Der er oprettet konto til dig på __siteName__",
|
"email-enrollAccount-subject": "Der er oprettet konto til dig på __siteName__",
|
||||||
"email-enrollAccount-text": "Hej __user__,\n\nFor at begynde at benytte tjenesten, så klik linket nedenfor.\n\n__url__\n\nTak.",
|
"email-enrollAccount-text": "Hej __user__,\n\nFor at begynde at benytte tjenesten, så klik linket nedenfor.\n\n__url__\n\nTak.",
|
||||||
"email-fail": "Afsendelse af e-mail mislykkedes",
|
"email-fail": "Afsendelse af e-mail mislykkedes",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "Alle lister, kort, etiketter og aktiviteter vil blive slettet og du får ikke mulighed for at genskabe tavlens indhold. Dette kan ikke fortrydes.",
|
"delete-board-confirm-popup": "Alle lister, kort, etiketter og aktiviteter vil blive slettet og du får ikke mulighed for at genskabe tavlens indhold. Dette kan ikke fortrydes.",
|
||||||
"boardDeletePopup-title": "Slet tavle?",
|
"boardDeletePopup-title": "Slet tavle?",
|
||||||
"delete-board": "Slet tavle",
|
"delete-board": "Slet tavle",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Delopgaver for tavlen __board__",
|
"default-subtasks-board": "Delopgaver for tavlen __board__",
|
||||||
"default": "Standard",
|
"default": "Standard",
|
||||||
"defaultdefault": "Standard",
|
"defaultdefault": "Standard",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Indstillinger for delopgaver",
|
"subtask-settings": "Indstillinger for delopgaver",
|
||||||
"card-settings": "Indstillinger for kort",
|
"card-settings": "Indstillinger for kort",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Indstillinger for delopgaver i tavle",
|
"boardSubtaskSettingsPopup-title": "Indstillinger for delopgaver",
|
||||||
"boardCardSettingsPopup-title": "Indstillinger for kort",
|
"boardCardSettingsPopup-title": "Indstillinger for kort",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Indsæt delopgaver på denne tavle:",
|
"deposit-subtasks-board": "Indsæt delopgaver på denne tavle:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Me",
|
"dueCardsViewChange-choice-me": "Me",
|
||||||
"dueCardsViewChange-choice-all": "All Users",
|
"dueCardsViewChange-choice-all": "All Users",
|
||||||
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
"dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Broken Cards",
|
"broken-cards": "Broken Cards",
|
||||||
"board-title-not-found": "Board '%s' not found.",
|
"board-title-not-found": "Board '%s' not found.",
|
||||||
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
"swimlane-title-not-found": "Swimlane '%s' not found.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "Fuldført",
|
"completed": "Fuldført",
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,18 @@
|
||||||
"activity-deleteComment": "löschte Kommentar %s",
|
"activity-deleteComment": "löschte Kommentar %s",
|
||||||
"activity-receivedDate": "hat Empfangsdatum zu %s geändert auf %s",
|
"activity-receivedDate": "hat Empfangsdatum zu %s geändert auf %s",
|
||||||
"activity-startDate": "hat Startdatum zu %s geändert auf %s",
|
"activity-startDate": "hat Startdatum zu %s geändert auf %s",
|
||||||
|
"allboards.starred": "Starred",
|
||||||
|
"allboards.templates": "Vorlagen",
|
||||||
|
"allboards.remaining": "Remaining",
|
||||||
|
"allboards.workspaces": "Workspaces",
|
||||||
|
"allboards.add-workspace": "Add Workspace",
|
||||||
|
"allboards.add-workspace-prompt": "Workspace name",
|
||||||
|
"allboards.add-subworkspace": "Add Subworkspace",
|
||||||
|
"allboards.add-subworkspace-prompt": "Subworkspace name",
|
||||||
|
"allboards.edit-workspace": "Edit workspace",
|
||||||
|
"allboards.edit-workspace-name": "Workspace name",
|
||||||
|
"allboards.edit-workspace-icon": "Workspace icon (markdown)",
|
||||||
|
"multi-selection-active": "Click checkboxes to select boards",
|
||||||
"activity-dueDate": "hat Fälligkeitsdatum zu %s geändert auf %s",
|
"activity-dueDate": "hat Fälligkeitsdatum zu %s geändert auf %s",
|
||||||
"activity-endDate": "hat Enddatum zu %s geändert auf %s",
|
"activity-endDate": "hat Enddatum zu %s geändert auf %s",
|
||||||
"add-attachment": "Datei anhängen",
|
"add-attachment": "Datei anhängen",
|
||||||
|
|
@ -190,7 +202,9 @@
|
||||||
"board-view-collapse": "Einklappen",
|
"board-view-collapse": "Einklappen",
|
||||||
"board-view-gantt": "Gantt",
|
"board-view-gantt": "Gantt",
|
||||||
"board-view-lists": "Listen",
|
"board-view-lists": "Listen",
|
||||||
"bucket-example": "z.B. \"Löffelliste\"",
|
"bucket-example": "Like \"Bucket List\" for example",
|
||||||
|
"calendar-previous-month-label": "Previous Month",
|
||||||
|
"calendar-next-month-label": "Next Month",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"card-archived": "Diese Karte wurde ins Archiv verschoben",
|
"card-archived": "Diese Karte wurde ins Archiv verschoben",
|
||||||
"board-archived": "Dieses Board wurde ins Archiv verschoben.",
|
"board-archived": "Dieses Board wurde ins Archiv verschoben.",
|
||||||
|
|
@ -335,6 +349,7 @@
|
||||||
"copyManyCardsPopup-format": "[ {\"title\": \"Titel der ersten Karte\", \"description\":\"Beschreibung der ersten Karte\"}, {\"title\":\"Titel der zweiten Karte\",\"description\":\"Beschreibung der zweiten Karte\"},{\"title\":\"Titel der letzten Karte\",\"description\":\"Beschreibung der letzten Karte\"} ]",
|
"copyManyCardsPopup-format": "[ {\"title\": \"Titel der ersten Karte\", \"description\":\"Beschreibung der ersten Karte\"}, {\"title\":\"Titel der zweiten Karte\",\"description\":\"Beschreibung der zweiten Karte\"},{\"title\":\"Titel der letzten Karte\",\"description\":\"Beschreibung der letzten Karte\"} ]",
|
||||||
"create": "Erstellen",
|
"create": "Erstellen",
|
||||||
"createBoardPopup-title": "Board erstellen",
|
"createBoardPopup-title": "Board erstellen",
|
||||||
|
"createTemplateContainerPopup-title": "Vorlagen-Container hinzufügen",
|
||||||
"chooseBoardSourcePopup-title": "Board importieren",
|
"chooseBoardSourcePopup-title": "Board importieren",
|
||||||
"createLabelPopup-title": "Label erstellen",
|
"createLabelPopup-title": "Label erstellen",
|
||||||
"createCustomField": "Feld erstellen",
|
"createCustomField": "Feld erstellen",
|
||||||
|
|
@ -354,6 +369,10 @@
|
||||||
"custom-field-text": "Text",
|
"custom-field-text": "Text",
|
||||||
"custom-fields": "Benutzerdefinierte Felder",
|
"custom-fields": "Benutzerdefinierte Felder",
|
||||||
"date": "Datum",
|
"date": "Datum",
|
||||||
|
"date-format": "Date Format",
|
||||||
|
"date-format-yyyy-mm-dd": "YYYY-MM-DD",
|
||||||
|
"date-format-dd-mm-yyyy": "DD-MM-YYYY",
|
||||||
|
"date-format-mm-dd-yyyy": "MM-DD-YYYY",
|
||||||
"decline": "Ablehnen",
|
"decline": "Ablehnen",
|
||||||
"default-avatar": "Standard Profilbild",
|
"default-avatar": "Standard Profilbild",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
|
|
@ -379,6 +398,7 @@
|
||||||
"editNotificationPopup-title": "Benachrichtigung ändern",
|
"editNotificationPopup-title": "Benachrichtigung ändern",
|
||||||
"editProfilePopup-title": "Profil ändern",
|
"editProfilePopup-title": "Profil ändern",
|
||||||
"email": "E-Mail",
|
"email": "E-Mail",
|
||||||
|
"email-address": "Email Address",
|
||||||
"email-enrollAccount-subject": "Ihr Benutzerkonto auf __siteName__ wurde erstellt",
|
"email-enrollAccount-subject": "Ihr Benutzerkonto auf __siteName__ wurde erstellt",
|
||||||
"email-enrollAccount-text": "Hallo __user__,\n\num den Dienst nutzen zu können, klicken Sie bitte auf folgenden Link:\n\n__url__\n\nDanke.",
|
"email-enrollAccount-text": "Hallo __user__,\n\num den Dienst nutzen zu können, klicken Sie bitte auf folgenden Link:\n\n__url__\n\nDanke.",
|
||||||
"email-fail": "Senden der E-Mail fehlgeschlagen",
|
"email-fail": "Senden der E-Mail fehlgeschlagen",
|
||||||
|
|
@ -748,6 +768,8 @@
|
||||||
"delete-board-confirm-popup": "Alle Listen, Karten, Labels und Akivitäten werden gelöscht und Sie können die Inhalte des Boards nicht wiederherstellen! Die Aktion kann nicht rückgängig gemacht werden.",
|
"delete-board-confirm-popup": "Alle Listen, Karten, Labels und Akivitäten werden gelöscht und Sie können die Inhalte des Boards nicht wiederherstellen! Die Aktion kann nicht rückgängig gemacht werden.",
|
||||||
"boardDeletePopup-title": "Board löschen?",
|
"boardDeletePopup-title": "Board löschen?",
|
||||||
"delete-board": "Board löschen",
|
"delete-board": "Board löschen",
|
||||||
|
"delete-duplicate-lists": "Delete Duplicate Lists",
|
||||||
|
"delete-duplicate-lists-confirm": "Are you sure? This will delete all duplicate lists that have the same name and contain no cards.",
|
||||||
"default-subtasks-board": "Teilaufgabe für __board__ Board",
|
"default-subtasks-board": "Teilaufgabe für __board__ Board",
|
||||||
"default": "Standard",
|
"default": "Standard",
|
||||||
"defaultdefault": "Standard",
|
"defaultdefault": "Standard",
|
||||||
|
|
@ -755,7 +777,7 @@
|
||||||
"subtask-settings": "Einstellungen für Teilaufgaben",
|
"subtask-settings": "Einstellungen für Teilaufgaben",
|
||||||
"card-settings": "Karten-Einstellungen",
|
"card-settings": "Karten-Einstellungen",
|
||||||
"minicard-settings": "Minicard Settings",
|
"minicard-settings": "Minicard Settings",
|
||||||
"boardSubtaskSettingsPopup-title": "Boardeinstellungen für Teilaufgaben",
|
"boardSubtaskSettingsPopup-title": "Einstellungen für Teilaufgaben",
|
||||||
"boardCardSettingsPopup-title": "Karten-Einstellungen",
|
"boardCardSettingsPopup-title": "Karten-Einstellungen",
|
||||||
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
"boardMinicardSettingsPopup-title": "Minicard Settings",
|
||||||
"deposit-subtasks-board": "Teilaufgaben in diesem Board ablegen:",
|
"deposit-subtasks-board": "Teilaufgaben in diesem Board ablegen:",
|
||||||
|
|
@ -1009,6 +1031,8 @@
|
||||||
"dueCardsViewChange-choice-me": "Ich",
|
"dueCardsViewChange-choice-me": "Ich",
|
||||||
"dueCardsViewChange-choice-all": "alle Benutzer",
|
"dueCardsViewChange-choice-all": "alle Benutzer",
|
||||||
"dueCardsViewChange-choice-all-description": "Zeigt alle unvollständigen Karten mit einem *Fälligkeits*-Datum auf Boards, für die der Benutzer Berechtigungen hat.",
|
"dueCardsViewChange-choice-all-description": "Zeigt alle unvollständigen Karten mit einem *Fälligkeits*-Datum auf Boards, für die der Benutzer Berechtigungen hat.",
|
||||||
|
"dueCards-noResults-title": "No Due Cards Found",
|
||||||
|
"dueCards-noResults-description": "You don't have any cards with due dates at the moment.",
|
||||||
"broken-cards": "Fehlerhafte Karten",
|
"broken-cards": "Fehlerhafte Karten",
|
||||||
"board-title-not-found": "Board „%s“ nicht gefunden.",
|
"board-title-not-found": "Board „%s“ nicht gefunden.",
|
||||||
"swimlane-title-not-found": "Swimlane „%s“ nicht gefunden.",
|
"swimlane-title-not-found": "Swimlane „%s“ nicht gefunden.",
|
||||||
|
|
@ -1393,7 +1417,70 @@
|
||||||
"back-to-settings": "Back to Settings",
|
"back-to-settings": "Back to Settings",
|
||||||
"board-id": "Board ID",
|
"board-id": "Board ID",
|
||||||
"board-migration": "Board Migration",
|
"board-migration": "Board Migration",
|
||||||
|
"board-migrations": "Board Migrations",
|
||||||
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
"card-show-lists-on-minicard": "Show Lists on Minicard",
|
||||||
|
"comprehensive-board-migration": "Comprehensive Board Migration",
|
||||||
|
"comprehensive-board-migration-description": "Performs comprehensive checks and fixes for board data integrity, including list ordering, card positions, and swimlane structure.",
|
||||||
|
"delete-duplicate-empty-lists-migration": "Delete Duplicate Empty Lists",
|
||||||
|
"delete-duplicate-empty-lists-migration-description": "Safely deletes empty duplicate lists. Only removes lists that have no cards AND have another list with the same title that contains cards.",
|
||||||
|
"lost-cards": "Lost Cards",
|
||||||
|
"lost-cards-list": "Restored Items",
|
||||||
|
"restore-lost-cards-migration": "Restore Lost Cards",
|
||||||
|
"restore-lost-cards-migration-description": "Finds and restores cards and lists with missing swimlaneId or listId. Creates a 'Lost Cards' swimlane to make all lost items visible again.",
|
||||||
|
"restore-all-archived-migration": "Restore All Archived",
|
||||||
|
"restore-all-archived-migration-description": "Restores all archived swimlanes, lists, and cards. Automatically fixes any missing swimlaneId or listId to make items visible.",
|
||||||
|
"fix-missing-lists-migration": "Fix Missing Lists",
|
||||||
|
"fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.",
|
||||||
|
"fix-avatar-urls-migration": "Fix Avatar URLs",
|
||||||
|
"fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.",
|
||||||
|
"fix-all-file-urls-migration": "Fix All File URLs",
|
||||||
|
"fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.",
|
||||||
|
"migration-needed": "Migration Needed",
|
||||||
|
"migration-complete": "Complete",
|
||||||
|
"migration-running": "Running...",
|
||||||
|
"migration-successful": "Migration completed successfully",
|
||||||
|
"migration-failed": "Migration failed",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"migrations-admin-only": "Only board administrators can run migrations",
|
||||||
|
"migrations-description": "Run data integrity checks and repairs for this board. Each migration can be executed individually.",
|
||||||
|
"no-issues-found": "No issues found",
|
||||||
|
"run-migration": "Run Migration",
|
||||||
|
"run-comprehensive-migration-confirm": "This will perform a comprehensive migration to check and fix board data integrity. This may take a few moments. Continue?",
|
||||||
|
"run-delete-duplicate-empty-lists-migration-confirm": "This will first convert any shared lists to per-swimlane lists, then delete empty lists that have a duplicate list with the same title containing cards. Only truly redundant empty lists will be removed. Continue?",
|
||||||
|
"run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?",
|
||||||
|
"run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?",
|
||||||
|
"run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?",
|
||||||
|
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
|
||||||
|
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
|
||||||
|
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
|
||||||
|
|
||||||
|
"migration-progress-title": "Board Migration in Progress",
|
||||||
|
"migration-progress-overall": "Overall Progress",
|
||||||
|
"migration-progress-current-step": "Current Step",
|
||||||
|
"migration-progress-status": "Status",
|
||||||
|
"migration-progress-details": "Details",
|
||||||
|
"migration-progress-note": "Please wait while we migrate your board to the latest structure...",
|
||||||
|
|
||||||
|
"step-analyze-board-structure": "Analyze Board Structure",
|
||||||
|
"step-fix-orphaned-cards": "Fix Orphaned Cards",
|
||||||
|
"step-convert-shared-lists": "Convert Shared Lists",
|
||||||
|
"step-ensure-per-swimlane-lists": "Ensure Per-Swimlane Lists",
|
||||||
|
"step-validate-migration": "Validate Migration",
|
||||||
|
"step-fix-avatar-urls": "Fix Avatar URLs",
|
||||||
|
"step-fix-attachment-urls": "Fix Attachment URLs",
|
||||||
|
"step-analyze-lists": "Analyze Lists",
|
||||||
|
"step-create-missing-lists": "Create Missing Lists",
|
||||||
|
"step-update-cards": "Update Cards",
|
||||||
|
"step-finalize": "Finalize",
|
||||||
|
"step-delete-duplicate-empty-lists": "Delete Duplicate Empty Lists",
|
||||||
|
"step-ensure-lost-cards-swimlane": "Ensure Lost Cards Swimlane",
|
||||||
|
"step-restore-lists": "Restore Lists",
|
||||||
|
"step-restore-cards": "Restore Cards",
|
||||||
|
"step-restore-swimlanes": "Restore Swimlanes",
|
||||||
|
"step-fix-missing-ids": "Fix Missing IDs",
|
||||||
|
"step-scan-users": "Checking board member avatars",
|
||||||
|
"step-scan-files": "Checking board file attachments",
|
||||||
|
"step-fix-file-urls": "Fixing file URLs",
|
||||||
"cleanup": "Cleanup",
|
"cleanup": "Cleanup",
|
||||||
"cleanup-old-jobs": "Cleanup Old Jobs",
|
"cleanup-old-jobs": "Cleanup Old Jobs",
|
||||||
"completed": "abgeschlossen",
|
"completed": "abgeschlossen",
|
||||||
|
|
|
||||||
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