mirror of
https://github.com/wekan/wekan.git
synced 2026-11-08 11:25:19 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37c5436087 | ||
|
|
6592102e8f | ||
|
|
06a5a8f70d | ||
|
|
ef54ebada6 | ||
|
|
d4f13de1d9 | ||
|
|
4fcedde529 | ||
|
|
a4518bbefc | ||
|
|
95f771aa26 | ||
|
|
da98942cce | ||
|
|
f3efaf59e1 | ||
|
|
d64032f2a3 | ||
|
|
abad8cc4d5 | ||
|
|
0d9536e2f9 | ||
|
|
67b078b805 | ||
|
|
6f02eeae53 | ||
|
|
5bc03b23ea | ||
|
|
3d4acd8c8f | ||
|
|
448bec8181 | ||
|
|
0a34ee1b64 | ||
|
|
34e8e4d4c3 | ||
|
|
32627c03f4 | ||
|
|
63c314ca18 | ||
|
|
e8453783da | ||
|
|
96522ec3a3 | ||
|
|
17dedab391 | ||
|
|
283d2ee09c | ||
|
|
289ff0127e | ||
|
|
931d7217b1 | ||
|
|
3149a3927e | ||
|
|
c57bced7b1 | ||
|
|
8d794a59dc | ||
|
|
7bb1e24bda | ||
|
|
e0013b9b63 | ||
|
|
7d81aab900 | ||
|
|
cc99da5357 | ||
|
|
9bd21e1d1b | ||
|
|
2148aeea42 | ||
|
|
6a7a5505f9 | ||
|
|
5a6faafa30 | ||
|
|
e2f3dad779 | ||
|
|
ae2aa1f5cd | ||
|
|
0e3a17d922 | ||
|
|
033919a270 | ||
|
|
f5d40a0a12 | ||
|
|
0fd781e80a | ||
|
|
a8f6170fdf | ||
|
|
bd8c565415 | ||
|
|
ffb02fe0ec | ||
|
|
114520302c | ||
|
|
317138ab72 | ||
|
|
a990109f43 | ||
|
|
da68b01502 | ||
|
|
e90bc744d9 | ||
|
|
2b5c56484a | ||
|
|
ea30612013 | ||
|
|
4445612bb8 | ||
|
|
da56393078 | ||
|
|
a86ff1e8d0 | ||
|
|
7fce5650b8 | ||
|
|
b949357e66 | ||
|
|
8e71b68dbf | ||
|
|
b77eed221f | ||
|
|
1fa91474d0 | ||
|
|
fc32a89292 | ||
|
|
ea24010298 | ||
|
|
ae1f80a52c | ||
|
|
2f95431c9b | ||
|
|
d59683eff1 | ||
|
|
de77776cd0 | ||
|
|
74ccfea570 | ||
|
|
1f5a549589 | ||
|
|
3ccdc2e307 | ||
|
|
688b725807 | ||
|
|
ae01ea576c | ||
|
|
cd0cd64849 | ||
|
|
aab671398c | ||
|
|
d64aeb25d5 | ||
|
|
aa4fa83127 | ||
|
|
81c3dc1d95 | ||
|
|
746eecf3d8 | ||
|
|
cd948fb576 | ||
|
|
4ec4e19e63 | ||
|
|
d88d197de9 | ||
|
|
2a24918a9c | ||
|
|
d2a85b41e4 | ||
|
|
9bcd991a61 | ||
|
|
1a7bd65e59 | ||
|
|
f8ee929cf7 | ||
|
|
734165f3c7 | ||
|
|
8f9ef1bde8 | ||
|
|
2119c6ab0c | ||
|
|
a85479f73c | ||
|
|
21ba0a9606 | ||
|
|
82ba0f9593 | ||
|
|
7769124401 | ||
|
|
d9c978e7fc | ||
|
|
90899f0928 | ||
|
|
f0421da517 | ||
|
|
573d4bf2cb | ||
|
|
f1e1fd3593 | ||
|
|
c387c5bc34 | ||
|
|
0549bc0b0c | ||
|
|
c2a3e11324 | ||
|
|
107e2ac900 | ||
|
|
3b60bdea14 | ||
|
|
f6591d7820 | ||
|
|
6bbd622066 | ||
|
|
ee79cab7b2 | ||
|
|
a0b94065c5 | ||
|
|
e1fa607f87 | ||
|
|
2e91a359f5 | ||
|
|
9720e703fd | ||
|
|
f88898d5b8 | ||
|
|
30c1597b65 | ||
|
|
5bc5171220 | ||
|
|
d0f118e7af | ||
|
|
c481443667 | ||
|
|
b87cff1289 | ||
|
|
0c080830bc | ||
|
|
cdd7d69c66 | ||
|
|
c1cbcdcc72 | ||
|
|
1c84b19f24 | ||
|
|
2c73dae019 | ||
|
|
d285850a04 | ||
|
|
719ef87efc | ||
|
|
039b9abf5e | ||
|
|
39daf56811 | ||
|
|
a8de2f224f | ||
|
|
dda013844c | ||
|
|
3e9481c5bd | ||
|
|
85ac03a892 | ||
|
|
752699d1c2 | ||
|
|
339ca581ab | ||
|
|
20e9cf1144 | ||
|
|
9737884c4b | ||
|
|
2f5670e830 | ||
|
|
108b18cebf | ||
|
|
814ac22a0d | ||
|
|
3814a218c2 | ||
|
|
3fda90612d | ||
|
|
05762aa50c | ||
|
|
9a31371de0 | ||
|
|
b9b7143185 | ||
|
|
972721bdb5 | ||
|
|
3a48982916 | ||
|
|
6b3a6f9f7d | ||
|
|
48fddecb62 | ||
|
|
7f0d1cc5b3 | ||
|
|
70e82be0b4 | ||
|
|
5fc0809844 | ||
|
|
c79990bcd9 | ||
|
|
cd9e7412ea | ||
|
|
3e472606b5 | ||
|
|
2966bc3204 | ||
|
|
5dc6093eec | ||
|
|
86fc60f5ba | ||
|
|
8189a3f9ab | ||
|
|
76a65e2533 | ||
|
|
ad2914934e | ||
|
|
2769ba2874 | ||
|
|
294f1e178e | ||
|
|
c994d34bb8 | ||
|
|
42b5dd0751 | ||
|
|
f884d6d471 | ||
|
|
abe915a8d6 | ||
|
|
a2e8de2fd8 | ||
|
|
e34ca6a8a5 | ||
|
|
596c9de603 | ||
|
|
4170361a1c | ||
|
|
978a36ea77 | ||
|
|
8745ef7de8 | ||
|
|
2ce929b7b3 | ||
|
|
2dcdfa2862 | ||
|
|
b18daac9c9 | ||
|
|
606041a00d | ||
|
|
4ed5952a81 | ||
|
|
37bfce2eb7 | ||
|
|
59541699ce | ||
|
|
af73c16e37 | ||
|
|
673b09b84b | ||
|
|
09f8edfead | ||
|
|
e1ebdeca4f | ||
|
|
31769aa4af | ||
|
|
b32ad86ce7 | ||
|
|
7350473a65 | ||
|
|
1c87e337d6 | ||
|
|
dec038e7ed | ||
|
|
ebe6f7813e | ||
|
|
706dbfacac | ||
|
|
62ad588aa9 | ||
|
|
3c776e2ab6 | ||
|
|
ab4e2d1ed6 | ||
|
|
5e90c4a142 | ||
|
|
9a7fa25ca3 | ||
|
|
65cb0641f3 | ||
|
|
6f2b891214 | ||
|
|
44d7ab83ec | ||
|
|
b328cc3f6a | ||
|
|
836226a8d3 | ||
|
|
2083f6630a | ||
|
|
25e1e9028f | ||
|
|
a5651c686f | ||
|
|
93f0e0b125 | ||
|
|
730bb6d166 | ||
|
|
855bac59bf | ||
|
|
870900b92a | ||
|
|
a1d5802d36 | ||
|
|
6e16fa3992 | ||
|
|
ee3b49ce59 | ||
|
|
3253dd5010 | ||
|
|
a10d8e0438 | ||
|
|
5151814e98 | ||
|
|
2e2a0f01f4 | ||
|
|
dec9b82da0 | ||
|
|
33f4db5871 | ||
|
|
9a086c0397 | ||
|
|
013ddb8978 | ||
|
|
c4161c5ce6 | ||
|
|
392b8dd5b2 | ||
|
|
d19eb801f3 | ||
|
|
ed416ca555 | ||
|
|
106009316f | ||
|
|
cb10ad3a20 | ||
|
|
2fad0099e4 | ||
|
|
454416b0bd | ||
|
|
dd80b7308b | ||
|
|
fe5c5f1bdc | ||
|
|
79074deb34 | ||
|
|
db0294c95e | ||
|
|
8f0fa88eac | ||
|
|
946f7045e9 | ||
|
|
dda8c90d44 | ||
|
|
701485d76a | ||
|
|
b25198b87e | ||
|
|
ad7e3774c2 | ||
|
|
ee67dea96c | ||
|
|
b3d3bf1593 | ||
|
|
53726fed24 | ||
|
|
7d2ca2c811 | ||
|
|
c0d10a0ec3 | ||
|
|
8e01e3c762 | ||
|
|
04be599893 | ||
|
|
f6cb7fddf1 | ||
|
|
a662283b7d | ||
|
|
f34490fe28 | ||
|
|
1a98c86a37 | ||
|
|
97b5931a4f | ||
|
|
2a4dd9b693 | ||
|
|
ecf31c3675 | ||
|
|
9b0d0a2b7c | ||
|
|
e2ffca2e39 | ||
|
|
a8435879cf | ||
|
|
e16695585a | ||
|
|
a7bcfc6db7 | ||
|
|
21bed98d69 | ||
|
|
044793cc44 | ||
|
|
083a7050b9 | ||
|
|
cc78e19740 | ||
|
|
ee01a07b0a | ||
|
|
ce528b1826 | ||
|
|
92c0543614 | ||
|
|
b6638f4a29 | ||
|
|
96be56a620 | ||
|
|
2e555e420a | ||
|
|
a849951c53 | ||
|
|
435cb04717 | ||
|
|
25a375261e | ||
|
|
1a1a44616f | ||
|
|
e6347a305c | ||
|
|
b137108ae1 | ||
|
|
4ce7ff7cef | ||
|
|
184415a026 | ||
|
|
3977adcb8f | ||
|
|
1c47f98f59 | ||
|
|
9f0c37352e | ||
|
|
7a20fa4805 | ||
|
|
6a713d9e8d | ||
|
|
51b0544752 | ||
|
|
1022a7aa3a | ||
|
|
c048678844 | ||
|
|
f9fb02457c | ||
|
|
ce419ce64e | ||
|
|
ecb6ff57fc | ||
|
|
663866562f | ||
|
|
dfdd0b6cbb | ||
|
|
c81ca82822 | ||
|
|
66b067eb49 | ||
|
|
4e9f2dc46a | ||
|
|
f02d8b61b5 | ||
|
|
088de1877c | ||
|
|
4611abffed | ||
|
|
f206f2f45c | ||
|
|
9c5f5757be | ||
|
|
ec2e0f6fed | ||
|
|
ae0d059b6f | ||
|
|
0132b801b2 | ||
|
|
06ca564ec2 | ||
|
|
d83ce5e633 | ||
|
|
5287319fde | ||
|
|
c75a23e693 | ||
|
|
180bf930c1 | ||
|
|
c16e99ba1e | ||
|
|
b9887fc917 | ||
|
|
96b36bd8ef | ||
|
|
0e32f666eb | ||
|
|
1341fc6a98 | ||
|
|
81c9e96bf3 | ||
|
|
20f91ef893 | ||
|
|
1187254573 | ||
|
|
f22fb4f4a9 | ||
|
|
a14762e005 | ||
|
|
9a91214043 | ||
|
|
30795a46cf | ||
|
|
b0865c8d06 | ||
|
|
c2aabbc6db | ||
|
|
4aad3dafd4 | ||
|
|
62269a9902 | ||
|
|
4bc2fd4a7f | ||
|
|
0e43a08689 | ||
|
|
77b1416413 | ||
|
|
200545822c | ||
|
|
ac33f2164f |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
## Issue
|
||||
|
||||
UPGRADE OR MIGRATE: https://github.com/wekan/wekan/blob/main/docs/Upgrade/Upgrade.md
|
||||
UPGRADE: https://wekan.fi/upgrade/
|
||||
|
||||
Pull requests welcome to fix any broken links at docs directory, and organizing docs/Features and their screenshots to subdirectories of each feature.
|
||||
|
||||
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
@@ -32,13 +32,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
|
||||
@@ -15,6 +15,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Build the Docker image
|
||||
run: docker build . --file Dockerfile --tag wekan:$(date +%s)
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v4
|
||||
# uses: actions/checkout@v5
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
# needs: [lintcode]
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v4
|
||||
# uses: actions/checkout@v5
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
# needs: [lintcode,lintstyle]
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v4
|
||||
# uses: actions/checkout@v5
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
|
||||
# CHECKOUTS
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# CACHING
|
||||
- name: Install Meteor
|
||||
@@ -147,10 +147,10 @@ jobs:
|
||||
needs: [tests]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Download coverage
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: coverage-folder
|
||||
path: .coverage/
|
||||
|
||||
Vendored
+12
-11
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
"editor.rulers": [80],
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"files.insertFinalNewline": true,
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.DS_Store": true,
|
||||
"node_modules": true,
|
||||
".meteor/local": true
|
||||
}
|
||||
}
|
||||
"terminal.integrated.env.linux": {
|
||||
"TERM": "xterm-256color"
|
||||
},
|
||||
"terminal.integrated.env.osx": {
|
||||
"TERM": "xterm-256color"
|
||||
},
|
||||
"terminal.integrated.env.windows": {
|
||||
"TERM": "xterm-256color"
|
||||
},
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"terminal.integrated.shellArgs.linux": ["-l"]
|
||||
}
|
||||
+316
-9
@@ -1,19 +1,326 @@
|
||||
[Mac ChangeLog](https://github.com/wekan/wekan/wiki/Mac)
|
||||
|
||||
Newest WeKan at these amd64 platforms:
|
||||
Newest WeKan at these platforms:
|
||||
|
||||
- Windows and Linux bundle
|
||||
- Snap Candidate
|
||||
- Docker
|
||||
- Kubernetes
|
||||
- [bundle zip at releases](https://github.com/wekan/wekan/releases), docs at
|
||||
- [Linux amd64, arm64, s390x](https://github.com/wekan/wekan/blob/main/docs/Platforms/FOSS/RaspberryPi/Raspberry-Pi.md)
|
||||
- [Windows amd64, works also at Windows arm64](https://github.com/wekan/wekan/blob/main/docs/Platforms/Propietary/Windows/Offline.md)
|
||||
- [Mac amd64, works also with Rosetta2 at Apple Silicon](https://github.com/wekan/wekan/blob/main/docs/Platforms/Propietary/Mac.md)
|
||||
- https://wekan.fi/install/
|
||||
- Snap Candidate amd64
|
||||
- Docker amd64
|
||||
- Kubernetes Docker amd64
|
||||
- Bitnami MongoDB Docker images do not exist anymore. [MongoDump/MongoRestore to groundhog2k MongoDB images](https://github.com/wekan/charts/issues/45)
|
||||
|
||||
Fixing other platforms In Progress.
|
||||
|
||||
- Install info at Server part of webpage https://wekan.github.io
|
||||
- Newest Node.js is at https://github.com/wekan/node-v14-esm/releases/tag/v14.21.4
|
||||
- MongoDB 6.x
|
||||
- Node.js 14.x at https://github.com/wekan/node-v14-esm/releases/tag/v14.21.4 and https://nodejs.org/dist/latest-v14.x/
|
||||
- MongoDB 6.x and 7.x, or FerretDB/PostgreSQL https://blog.ferretdb.io/building-project-management-stack-wekan-ferretdb/
|
||||
|
||||
[How to upgrade WeKan](https://github.com/wekan/wekan/issues/4585)
|
||||
[Upgrade WeKan](https://wekan.fi/upgrade/)
|
||||
|
||||
# v8.02 2025-10-14 WeKan ® release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Run database migrations when opening board. Not when upgrading WeKan](https://github.com/wekan/wekan/commit/2b5c56484a4dd559f062ef892fd5248a903b2a10).
|
||||
Thanks to xet7.
|
||||
- [Added Cron Manager to Admin Panel for long running jobs, like running migrations when opening board, copying or moving boards swimlanes lists cards etc](https://github.com/wekan/wekan/commit/da68b01502afc9d5d9ea1267bee9fc98bb08b611).
|
||||
Thanks to xet7.
|
||||
- [If there is no cron jobs running, run migrations for boards that have not been opened yet](https://github.com/wekan/wekan/commit/317138ab7209a41715336ea8251df45f11a6d173).
|
||||
Thanks to xet7.
|
||||
- [Accessibility improvements](https://github.com/wekan/wekan/commit/67b078b8056ec9851caaf6ef855719de1e6d966d).
|
||||
Thanks to xet7.
|
||||
- [Change list width by dragging between lists](https://github.com/wekan/wekan/commit/abad8cc4d5dded0f5e1a80892a3b29aa71404a5c).
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Updated dependencies](https://github.com/wekan/wekan/commit/5bc03b23ea34816d8e1135cbe9ed5f18a2573854).
|
||||
Thanks to developers of dependencies.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fixes to make board showing correctly](https://github.com/wekan/wekan/commit/bd8c565415998c9aaded821988d591105258b378).
|
||||
Thanks to xet7.
|
||||
- [Fix opening sidebar](https://github.com/wekan/wekan/commit/0fd781e80aaf841c26ce59caffc579b9c391330f).
|
||||
Thanks to xet7.
|
||||
- [Fix Admin Panel menus "Attachment Settings" and "Cron Settings" and make them translateable](https://github.com/wekan/wekan/commit/033919a2702fa6959b8f8c87f076d3f255ace6ba).
|
||||
Thanks to xet7.
|
||||
- Change Admin Panel "Attachment Settings" and "Cron Settings" options to be tabs, not submenu.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/ae2aa1f5cd2511e80e12a91426eb91bb968dff98),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/5a6faafa30fefcd5dd0af7cc52b847a54d538065),
|
||||
[Part 3](https://github.com/wekan/wekan/commit/2148aeea42f69fa367bf8c451d7f1c3a63b52880).
|
||||
Thanks to xet7.
|
||||
- [Fixed Error in migrate-lists-to-per-swimlane migration](https://github.com/wekan/wekan/commit/cc99da5357fb1fc00e3b5aece20c57917f88301b).
|
||||
Thanks to xet7.
|
||||
- Fix Admin Panel Settings menu to show Attachments and Cron options correctly.
|
||||
[Part 1](https://github.com/wekan/wekan/e0013b9b631eb16861b1cfdb25386bf8e9099b4e),
|
||||
[Part 2](https://github.com/wekan/wekan/7bb1e24bda2ed9db0bad0fafcf256680c2c05e8a).
|
||||
- [Fixed migrations](https://github.com/wekan/wekan/commit/63c314ca185aeda650c01b4a67fcde1067320d22).
|
||||
Thanks to xet7.
|
||||
- [Removed not needed console log message](https://github.com/wekan/wekan/commit/0a34ee1b6437dcfd65e31d9bbc9f3ccfa5718ba9).
|
||||
Thanks to xet7.
|
||||
- [Updated mobile Bookmarks/Starred boards. Part 1. In Progress](https://github.com/wekan/wekan/commit/da98942cce37363d6062695d3c4cf7e2df796cac).
|
||||
Thanks to xet7.
|
||||
- [Fix drag drop reorder swimlanes](https://github.com/wekan/wekan/commit/a4518bbefc99be74f7ccfdbb9fdf902007ca90f3).
|
||||
Thanks to xet7.
|
||||
- [Try to fix swimlane hamburger menu popup positioning. In progress](https://github.com/wekan/wekan/commit/d4f13de1d978b271d05e1d67d40e3c1c14761578).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.01 2025-10-11 WeKan ® release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Made possible to start WeKan immediately without running any database migrations](https://github.com/wekan/wekan/commit/3ccdc2e3070978a79bc68941375122504318677d).
|
||||
Thanks to xet7.
|
||||
- [Add support for MongoDB 3-8 client, detecting which one is in use](https://github.com/wekan/wekan/commit/74ccfea5703b08b2166593d1bb8e1d981970cec0).
|
||||
Thanks to xet7.
|
||||
- [Add support for Snap MongoDB Server 3-8 support. Part 1](https://github.com/wekan/wekan/commit/b949357e665d97604cbecb04a9211319ac4e13c2).
|
||||
Thanks to xet7.
|
||||
- [Added attachments API and admin panel attachment management for file storage backends settings. Fixed drag drop upload attachments from file manager to minicard or opened card](https://github.com/wekan/wekan/commit/ae1f80a52cde09689dddb1209708630b4949b7ee).
|
||||
Thanks to xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fixed attachments migrations at Admin Panel to not use too much CPU while migrating attachments](https://github.com/wekan/wekan/commit/d59683eff1267ff87a6aef9ae36c7aebbe10eaa1).
|
||||
Thanks to xet7.
|
||||
- [Fixed per-card and per-board settings of showing checkist at minicard](https://github.com/wekan/wekan/commit/fc32a89292e4d314bd3868739f6e01bc2bc17018).
|
||||
Thanks to xet7.
|
||||
- [Fix count of Orgs Teams People at Admin Panel](https://github.com/wekan/wekan/commit/b77eed221f9e5fc657234a6de3e11c27f4de3d38).
|
||||
Thanks to xet7.
|
||||
- [Fixed attachments and minicard related bugs that prevented WeKan starting](https://github.com/wekan/wekan/commit/a86ff1e8d03d330a7ea0702cf3677859ed552509).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.00 2025-10-11 WeKan ® release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Snap: Migrate MongoDB from 3 to 7 only when "snap set wekan migrate-mongodb='true'". Not automatically](https://github.com/wekan/wekan/commit/ae01ea576c3073acbc181a5b19008ddaea4700e1).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v7.99 2025-10-11 WeKan ® release
|
||||
|
||||
This release fixed the following bugs:
|
||||
|
||||
- [Improve automatic Snap upgrades](https://github.com/wekan/wekan/commit/aab671398c7ee3d7ea4934c6c9c977ad630fa74f).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v7.98 2025-10-11 WeKan ® release
|
||||
|
||||
This release adds the following CRITICAL SECURITY FIXES:
|
||||
|
||||
- [Security Fix: IDOR CWE-639 that affected WeKan 7.80-7.93](https://github.com/wekan/wekan/commit/b87cff1289d2c98ef84d69e171f112ecedb4d843).
|
||||
Thanks to Romain Korpas at apitech.fr and xet7.
|
||||
- [Security Fix: Computational Resource Abuse in Export endpoints](https://github.com/wekan/wekan/commit/d0f118e7af0b2ede517d6d051226c38fa8e557b6).
|
||||
Thanks to Anynymous Security Researcher and xet7.
|
||||
- [Security Fix FG-VD-22-078: Prevent SVG Billion Laughs Attack](https://github.com/wekan/wekan/commit/30c1597b658b0ef50fd2efc56786e8b0f08ac72c).
|
||||
Thanks to Nguyen Thanh Nguyen of Fortinet's FortiGuard Labs and xet7.
|
||||
- [Security Fix usd-2022-0041: CWE-284 Improper Access Control](https://github.com/wekan/wekan/commit/f6591d7820e01075cba93612a5fdbf692fbe49dc).
|
||||
Thanks to Christian Pöschl of usd AG and xet7.
|
||||
- [Security Fix JVN#14269684: Broken access control](https://github.com/wekan/wekan/commit/9720e703fd9432bf0e1bfea2358f8c7ea078f1b1).
|
||||
Thanks to Ryoya Koyama of Mitsui Bussan Secure Directions, Inc and xet7.
|
||||
- [Security Fix JVN#74210258: Stored XSS](https://github.com/wekan/wekan/commit/e1fa607f87d821accb846f2deef1f388003848d1).
|
||||
Thanks to Ryoya Koyama of Mitsui Bussan Secure Directions, Inc and xet7.
|
||||
- [Security Fix JVN#86586539: Stored XSS](https://github.com/wekan/wekan/commit/ee79cab7b27f73fab62a00ec49add73fd6f7bcaa).
|
||||
Thanks to Ryoya Koyama of Mitsui Bussan Secure Directions, Inc and xet7.
|
||||
- [Security Fix JVN#15385465: CWE-79 XSS, that affected WeKan 7.94](https://github.com/wekan/wekan/commit/81c3dc1d956cd7040655940f6569653d7b98fa9a).
|
||||
Thanks to Sho Sugiyama and xet7.
|
||||
- Security Fix JVN#80785288: CWE-79 XSS, that affected WeKan 3.94 and v3.95. This was already previously fixed.
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following new features:
|
||||
|
||||
- [Mobile one board per row. Board zoom size percent. Board toggle mobile/desktop mode. In Progress](https://github.com/wekan/wekan/commit/752699d1c2fb8ea9ff0f3ec9ae0b2b776443d826).
|
||||
Thanks to xet7.
|
||||
- [Drag any files from file manager to minicard or opened card.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/3e9481c5bd2c02ba501bd0a6ef1d1e6ce82bb1d9),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/cdd7d69c660d0b6ac06b7b75d4f59985b8a9322a).
|
||||
Thanks to xet7.
|
||||
- [Use attachments from old CollectionFS database structure, when not yet migrated to Meteor-Files/ostrio-files, without needing to migrate database structure](https://github.com/wekan/wekan/commit/a8de2f224f61d0e5c7061fefb1cdd4f45f3bb020).
|
||||
Thanks to xet7.
|
||||
- [Show console.log 'Legacy attachments route loaded' only when environment variable DEBUG=true](https://github.com/wekan/wekan/commit/1c84b19f246ebe6d3cad4f56726013147756d9c3).
|
||||
Thanks to xet7.
|
||||
- [Make possible for lists to have different names at different swimlanes. Make possible to drag list from one swimlane to another swimlane](https://github.com/wekan/wekan/commit/719ef87efceacfe91461a8eeca7cf74d11f4cc0a).
|
||||
Thanks to xet7.
|
||||
- [Add support for Docker/Compose Secrets for passwords to Docker/Snap/Bundle platforms](https://github.com/wekan/wekan/commit/107e2ac90043138ea8aa09aadd92f4af355289c0).
|
||||
Thanks to Roemer and xet7.
|
||||
- Add Snap automatic upgrades.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/0549bc0b0c7dd4a5db53d645662dd4362ef4d1f3),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/f1e1fd359364dbe6366501ac4f6a150d73509f17).
|
||||
Thanks to xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- Fix DOMPurify paths.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/90899f0928274cbc30ed3d6df0227664efcd2584),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/77691244018f3a1e44c28973b40a363bf77f99b2),
|
||||
[Part 3](https://github.com/wekan/wekan/commit/21ba0a96066a495e885fcf186f026cf1c7ddf45d),
|
||||
[Part 4](https://github.com/wekan/wekan/commit/2119c6ab0c2d76c8a9b22fa712b35b38345fc040).
|
||||
Thanks to xet7.
|
||||
- [Fix sizes of drag handles at desktop mode](https://github.com/wekan/wekan/commit/734165f3c78e9ba9643826da62f78844273f18f6).
|
||||
Thanks to xet7.
|
||||
- [Fixed showing translations always, regardsless of is ROOT_URL set correctly or not](https://github.com/wekan/wekan/commit/1a7bd65e596a4a1030e2686a787186b6d1c61abe).
|
||||
Thanks to xet7.
|
||||
- [Snap: Removed double mongo3 that's already at migratemongo](https://github.com/wekan/wekan/commit/2a24918a9c31e541f4d6424b77cfd76a8d9f2b06).
|
||||
Thanks to xet7.
|
||||
- [Updated release scripts of snapcraft pack command syntax](https://github.com/wekan/wekan/commit/d88d197de964fd71e61ea6c932521f1e5abe7630).
|
||||
Thanks to xet7.
|
||||
- [Try to fix Snap automatic upgrade](https://github.com/wekan/wekan/commit/4ec4e19e637b86b05b11afd076fae09d3698b8de).
|
||||
Thanks to xet7.
|
||||
- [Added missing metadata fields to snapcraft.yaml](https://github.com/wekan/wekan/commit/cd948fb5765efdf5a7d792180a25ffa26bab2730).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v7.97 2025-10-08 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Fixed Collapse button not there when using clearlight board theme](https://github.com/wekan/wekan/pull/5900).
|
||||
Thanks to seve12.
|
||||
- [Fixed translation of "Change Language" at login page](https://github.com/wekan/wekan/commit/9a31371de0e155d3144769f45a1fa216872935eb).
|
||||
Thanks to xet7.
|
||||
- [Some mobile and tablet fixes. Show drag handles at touch screens only. No toggle of drag handles anymore](https://github.com/wekan/wekan/commit/3fda90612d88fc5a8f1e17ae5c399745de06aada).
|
||||
Thanks to xet7.
|
||||
- [Removed white box that appeared when clicking something](https://github.com/wekan/wekan/commit/3814a218c2dab3ed9e8908c3a2de996ffebf6d9d).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v7.96 2025-10-07 WeKan ® release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- [Updated release script for new command of uploading snap package to Snap Store](https://github.com/wekan/wekan/commit/dec9b82da0d700aafd7e2b2063b5d5b07dd383c3).
|
||||
Thanks to xet7.
|
||||
- [Updated to MongoDB 7.0.25 at Snap Candidate](https://github.com/wekan/wekan/commit/dec038e7edce11e2ce59de974aa870d7266af33b).
|
||||
Thanks to MongoDB developers.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Added missing mobile drag drop image to docs from wekan.github.io git history](https://github.com/wekan/wekan/commit/5151814e98599c22fff98dc7c42fbf634004ab20).
|
||||
Thanks to xet7.
|
||||
- [Fix links in docker-compose.yml to point to doc in repo](https://github.com/wekan/wekan/pull/5882).
|
||||
Thanks to Skylark13.
|
||||
- [Fixed find.sh script errors about unmatched single quotes and binary files that are now ignored](https://github.com/wekan/wekan/commit/730bb6d166baa5530cc75b4c94ae46c8077884b0).
|
||||
Thanks to xet7.
|
||||
- [Fixed links at docs](https://github.com/wekan/wekan/commit/6f2b89121402df8ecba88295ad09f5c4179ab0ed).
|
||||
Thanks to xet7.
|
||||
- [Added missing screenshots from wekan.github.io history to wekan/docs/Features/](https://github.com/wekan/wekan/commit/9a7fa25ca33d4267e193f0f9bb97b09c490e44fe).
|
||||
Thanks to xet7.
|
||||
- [Some accessibility, saving, downloading and editor fixes](https://github.com/wekan/wekan/pull/5895).
|
||||
Thanks to seve12.
|
||||
- [Hide extra keyboard shortcuts toggle](https://github.com/wekan/wekan/commit/62ad588aa982cd4305a8817cf21f0818234f4354).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v7.95 2025-09-18 WeKan ® release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- [Updated upgrade link at repo readme](https://github.com/wekan/wekan/commit/b3d3bf159376d56048361a385f0b295667e2319c).
|
||||
Thanks to xet7.
|
||||
- [Fixed image link at docs](https://github.com/wekan/wekan/commit/ad7e3774c2b29265a8fbefdde7b9b83822af6def).
|
||||
Thanks to xet7.
|
||||
- [More info about ToroDB, FerretDB, Turso, TigerBeetle, PostgreSQL, SQLite etc](https://github.com/wekan/wekan/commit/701485d76aded5ee4fa17967d94918132a821af6).
|
||||
Thanks to xet7.
|
||||
- [Added missing image to WeKan Multiverse page](https://github.com/wekan/wekan/commit/946f7045e92e26bbe8d182634844ee5eebf73564).
|
||||
Thanks to xet7.
|
||||
- [Added more info to Windows Offline docs about WRITABLE_PATH and ATTACHMENTS_STORE_PATH](https://github.com/wekan/wekan/pull/5872).
|
||||
Thanks to sowwos.
|
||||
- [Added PGP public key for sending encrypted email](https://github.com/wekan/wekan/commit/dd80b7308bb6c66113c8e6009c442a44a29e477a).
|
||||
Thanks to xet7.
|
||||
- [Updated dependencies](https://github.com/wekan/wekan/commit/c4161c5ce69a35c76364b13bcc456905f6335162).
|
||||
Thanks to developers of dependencies.
|
||||
- [Replace bitnami mongodb Helm chart by groundhog2k one](https://github.com/wekan/charts/pull/44).
|
||||
Thanks to ariep.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v7.94 2025-08-18 WeKan ® release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Feature: Accessibility page at /accessibility. Settings at Admin Panel. When enabled, link at right sidebar](https://github.com/wekan/wekan/commit/d83ce5e6335c175f2f4a384f6090c18c7f860555).
|
||||
Thanks to xet7.
|
||||
- [Feature: Added brute force login protection settings to Admin Panel/People/Locked Users](https://github.com/wekan/wekan/commit/ae0d059b6feb38a438ca21aa154c63b410641763).
|
||||
Added filtering of Admin Panel/People/People: All Users/Locked Users Only/Active/Not Active.
|
||||
Added visual indicators: red lock icon for locked users, green check for active users, and red X for inactive users.
|
||||
Added "Unlock All" button to quickly unlock all brute force locked users.
|
||||
Added ability to toggle user active status directly from the People page.
|
||||
Moved lockout settings from environment variables to database so admins can configure the lockout thresholds directly in the UI.
|
||||
Thanks to xet7.
|
||||
- Added docs for Windows SSL/TLS.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/92c0543614fb92cf8217d8cc24a367296f3ade37),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/ce528b1826255ebf3ac9533dd8ed91bec8bbe8b1),
|
||||
[Part 3](https://github.com/wekan/wekan/commit/ee01a07b0ac7c6b06054b3d7bfac8c38161adf19),
|
||||
[Part 4](https://github.com/wekan/wekan/commit/cc78e19740ae1bf42f2bbb17f801fe2d4dac135d),
|
||||
[Part 5](https://github.com/wekan/wekan/commit/044793cc4473bebc8eefa64a0df9e9a20115d049),
|
||||
[Part 6](https://github.com/wekan/wekan/commit/a7bcfc6db7cc78892ed20cdf0d03f84094853687).
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- Update CHANGELOG.md platforms info at top.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/0e43a08689c0d47c816ce781e93650f0c37efe28),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/4bc2fd4a7fc166041a5c483aad73bf8079c3ae45),
|
||||
[Part 3](https://github.com/wekan/wekan/commit/62269a99024744c789ac931921c96d5edae9d79e),
|
||||
[Part 4](https://github.com/wekan/wekan/commit/4aad3dafd4530cc7779072fc9a550d8732f32558),
|
||||
[Part 5](https://github.com/wekan/wekan/commit/c2aabbc6db9ffbb73d9f06edb73a15a9f4838df4),
|
||||
[Part 6](https://github.com/wekan/wekan/commit/b0865c8d06a31d07b5b5df719871c298e1cf4ad8),
|
||||
[Part 7](https://github.com/wekan/wekan/commit/30795a46cfb5f37837206a6de653913bfb631595),
|
||||
[Part 8](https://github.com/wekan/wekan/commit/9a91214043d93307074cf33e54d40cb43c14ad62).
|
||||
- [Updated security email address](https://github.com/wekan/wekan/commit/11872545734db82229e873b48f122d826457fec3).
|
||||
Thanks to xet7.
|
||||
- Updated GitHub Docker Actions.
|
||||
[Part 1](https://github.com/wekan/wekan/pull/5855),
|
||||
[Part 2](https://github.com/wekan/wekan/pull/5856),
|
||||
[Part 3](https://github.com/wekan/wekan/pull/5864),
|
||||
[Part 4](https://github.com/wekan/wekan/pull/5865).
|
||||
Thanks to Dependabot.
|
||||
- [Added info about Sandstorm Radicale Calendar and Contacts](https://github.com/wekan/wekan/commit/dfdd0b6cbbbb60bf6dba7a61f34dc52e74f98b41).
|
||||
Thanks to xet7.
|
||||
- [Updated dependencies](https://github.com/wekan/wekan/commit/97b5931a4f717541631dde55f2b834d037e1d82d).
|
||||
Thanks to developers of dependencies.
|
||||
- Reorganized Design Docs.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/f34490fe2820352c8303f0bac47ad7158592e8f0),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/a662283b7de91c0ef63195691e06b5cbc6cdff6c).
|
||||
Thanks to xet7.
|
||||
- [Added related link to design docs](https://github.com/wekan/wekan/commit/a662283b7de91c0ef63195691e06b5cbc6cdff6c).
|
||||
Thanks to xet7.
|
||||
- [Upgraded to MongoDB 7.0.23 at Snap Candidate](https://github.com/wekan/wekan/commit/8e01e3c762748269c2e60f9f349dac30f30971d7).
|
||||
Thanks to MongoDB developers.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fixes for building at macOS](https://github.com/wekan/wekan/commit/200545822c5c778521f76525938fe777ed9666f0).
|
||||
Thanks to xet7.
|
||||
- [Improves accessibility by disabling custom Tab handling](https://github.com/wekan/wekan/pull/5853).
|
||||
Thanks to seve12, co-maintainer of WeKan.
|
||||
- [Comment out not used Tab code at accessibility related changes](https://github.com/wekan/wekan/commit/0e32f666eb30cc254177119907e6eae3c09ce0ca).
|
||||
Thanks to xet7.
|
||||
- [Fixed link at OpenShift docs](https://github.com/wekan/wekan/commit/b9887fc91758edb31259a8df7770a1d992bc9251).
|
||||
Thanks to xet7.
|
||||
- [Added FerretDB/PostgreSQL to changelog](https://github.com/wekan/wekan/commit/c16e99ba1ec7bc25a4e0449ea463ea8f4b02da11).
|
||||
Thanks to xet7.
|
||||
- [Accessibility](https://github.com/wekan/wekan/pull/5857):
|
||||
Added product name to page titles, settings and global search.
|
||||
More accessible header and layout templates, modal dialogs, DOM structure, color contrast.
|
||||
Thanks to seve12.
|
||||
- [Fixed uppercase K of WeKan](https://github.com/wekan/wekan/commit/b137108ae15a9c5685cca5f45cfa9480ec7d3510).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v7.93 2025-07-18 WeKan ® release
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ For all code at WeKan GitHub Organization https://github.com/wekan
|
||||
|
||||
## Private reports
|
||||
|
||||
- Email support@wekan.team
|
||||
- Email support@wekan.fi
|
||||
- Security issues: [SECURITY.md](SECURITY.md)
|
||||
- License violations
|
||||
- Anything private, sensitive or negative
|
||||
|
||||
+9
-3
@@ -66,6 +66,7 @@ ENV \
|
||||
OAUTH2_LOGIN_STYLE=redirect \
|
||||
OAUTH2_CLIENT_ID="" \
|
||||
OAUTH2_SECRET="" \
|
||||
OAUTH2_SECRET_FILE="" \
|
||||
OAUTH2_SERVER_URL="" \
|
||||
OAUTH2_AUTH_ENDPOINT="" \
|
||||
OAUTH2_USERINFO_ENDPOINT="" \
|
||||
@@ -91,6 +92,7 @@ ENV \
|
||||
LDAP_AUTHENTIFICATION=false \
|
||||
LDAP_AUTHENTIFICATION_USERDN="" \
|
||||
LDAP_AUTHENTIFICATION_PASSWORD="" \
|
||||
LDAP_AUTHENTIFICATION_PASSWORD_FILE="" \
|
||||
LDAP_LOG_ENABLED=false \
|
||||
LDAP_BACKGROUND_SYNC=false \
|
||||
LDAP_BACKGROUND_SYNC_INTERVAL="" \
|
||||
@@ -156,7 +158,10 @@ ENV \
|
||||
ORACLE_OIM_ENABLED=false \
|
||||
WAIT_SPINNER="" \
|
||||
WRITABLE_PATH=/data \
|
||||
S3=""
|
||||
S3="" \
|
||||
MAIL_SERVICE_PASSWORD_FILE="" \
|
||||
MONGO_PASSWORD_FILE="" \
|
||||
S3_SECRET_FILE=""
|
||||
|
||||
# NODE_OPTIONS="--max_old_space_size=4096"
|
||||
|
||||
@@ -244,8 +249,9 @@ cd /home/wekan/app
|
||||
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
||||
#rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
|
||||
#mv /home/wekan/app_build/bundle /build
|
||||
wget "https://github.com/wekan/wekan/releases/download/v7.93/wekan-7.93-amd64.zip"
|
||||
unzip wekan-7.93-amd64.zip
|
||||
wget "https://github.com/wekan/wekan/releases/download/v8.02/wekan-8.02-amd64.zip"
|
||||
unzip wekan-8.02-amd64.zip
|
||||
rm wekan-8.02-amd64.zip
|
||||
mv /home/wekan/app/bundle /build
|
||||
|
||||
# Put back the original tar
|
||||
|
||||
+6
-1
@@ -53,7 +53,12 @@ ENV QEMU_ARCHITECTURE=aarch64 \
|
||||
WITH_API=true \
|
||||
PORT=8080 \
|
||||
ROOT_URL=http://localhost \
|
||||
MONGO_URL=mongodb://127.0.0.1:27017/wekan
|
||||
MONGO_URL=mongodb://127.0.0.1:27017/wekan \
|
||||
LDAP_AUTHENTIFICATION_PASSWORD_FILE="" \
|
||||
OAUTH2_SECRET_FILE="" \
|
||||
MAIL_SERVICE_PASSWORD_FILE="" \
|
||||
MONGO_PASSWORD_FILE="" \
|
||||
S3_SECRET_FILE=""
|
||||
|
||||
# Copy qemu-static to image
|
||||
COPY --from=builder qemu-${QEMU_ARCHITECTURE}-static /usr/bin
|
||||
|
||||
+6
-1
@@ -56,7 +56,12 @@ ENV QEMU_ARCHITECTURE=s390x \
|
||||
WITH_API=true \
|
||||
PORT=8080 \
|
||||
ROOT_URL=http://localhost \
|
||||
MONGO_URL=mongodb://127.0.0.1:27017/wekan
|
||||
MONGO_URL=mongodb://127.0.0.1:27017/wekan \
|
||||
LDAP_AUTHENTIFICATION_PASSWORD_FILE="" \
|
||||
OAUTH2_SECRET_FILE="" \
|
||||
MAIL_SERVICE_PASSWORD_FILE="" \
|
||||
MONGO_PASSWORD_FILE="" \
|
||||
S3_SECRET_FILE=""
|
||||
|
||||
# Copy qemu-static to image
|
||||
COPY --from=builder qemu-${QEMU_ARCHITECTURE}-static /usr/bin
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
[Gitpod Ready-to-Code](https://gitpod.io/#https://github.com/wekan/wekan)
|
||||
|
||||
# WeKan ® - Open Source kanban
|
||||
|
||||
## Downloads
|
||||
@@ -70,14 +68,11 @@ that by providing one-click installation on various platforms.
|
||||
- WeKan ® largest user has 30k users using WeKan ® in their company.
|
||||
- WeKan ® has been [translated](https://app.transifex.com/wekan/) to about 105 languages.
|
||||
- [Features][features]: WeKan ® has real-time user interface.
|
||||
- [Platforms][platforms]: WeKan ® supports many platforms.
|
||||
- [Platforms](https://wekan.fi/install/): WeKan ® supports many platforms.
|
||||
WeKan ® is critical part of new platforms Wekan is currently being integrated to.
|
||||
|
||||
## Requirements
|
||||
|
||||
- 64bit: Linux [Snap](https://github.com/wekan/wekan/blob/main/docs/Platforms/FOSS/Snap/Install.md) or [Sandstorm](https://sandstorm.io) /
|
||||
[Mac](https://github.com/wekan/wekan/wiki/Mac) / [Windows](https://github.com/wekan/wekan/blob/main/docs/Platforms/Propietary/Windows/Offline.md).
|
||||
[More Platforms](https://github.com/wekan/wekan/tree/main/docs/Platforms), bundle for RasPi3 ARM and other CPUs where Node.js and MongoDB exists.
|
||||
- 1 GB RAM minimum free for WeKan ®. Production server should have minimum total 4 GB RAM.
|
||||
For thousands of users, for example with [Docker](https://github.com/wekan/wekan/blob/main/docker-compose.yml): 3 frontend servers,
|
||||
each having 2 CPU and 2 wekan-app containers. One backend wekan-db server with many CPUs.
|
||||
@@ -124,7 +119,6 @@ Please refer to the [developer documentation](https://github.com/wekan/wekan/blo
|
||||
WeKan ® is released under the very permissive [MIT license](LICENSE), and made
|
||||
with [Meteor](https://www.meteor.com).
|
||||
|
||||
[platforms]: https://github.com/wekan/wekan/tree/main/docs/Platforms
|
||||
[dev_docs]: https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Developer-Documentation.md
|
||||
[screenshot_wekan]: https://wekan.fi/wekan-dark-mode.png
|
||||
[features]: https://github.com/wekan/wekan/wiki/Features
|
||||
|
||||
+5
-4
@@ -1,11 +1,12 @@
|
||||
About money, see [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
Security is very important to us. If you discover any issue regarding security, please disclose
|
||||
the information responsibly by sending an email to security@wekan.team and not by
|
||||
creating a GitHub issue. We will respond swiftly to fix verifiable security issues.
|
||||
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.github.io/hall-of-fame
|
||||
We thank you with a place at our hall of fame page, that is at https://wekan.fi/hall-of-fame
|
||||
|
||||
## How should reports be formatted?
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v7.93.0"
|
||||
appVersion: "v8.02.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -39,6 +39,12 @@ If *nix: chmod +x api.py => ./api.py users
|
||||
python3 api.py addcustomfieldtoboard AUTHORID BOARDID NAME TYPE SETTINGS SHOWONCARD AUTOMATICALLYONCARD SHOWLABELONMINICARD SHOWSUMATTOPOFLIST # Add Custom Field to Board
|
||||
python3 api.py editcustomfield BOARDID LISTID CARDID CUSTOMFIELDID NEWCUSTOMFIELDVALUE # Edit Custom Field
|
||||
python3 api.py listattachments BOARDID # List attachments
|
||||
python3 api.py uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND] # Upload attachment to card
|
||||
python3 api.py downloadattachment ATTACHMENTID OUTPUTPATH # Download attachment to local file
|
||||
python3 api.py attachmentinfo ATTACHMENTID # Get attachment information
|
||||
python3 api.py listcardattachments BOARDID SWIMLANEID LISTID CARDID # List attachments for specific card
|
||||
python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move] # Copy or move attachment
|
||||
python3 api.py deleteattachment ATTACHMENTID # Delete attachment
|
||||
python3 api.py cardsbyswimlane SWIMLANEID LISTID # Retrieve cards list on a swimlane
|
||||
python3 api.py getcard BOARDID LISTID CARDID # Get card info
|
||||
python3 api.py addlabel BOARDID LISTID CARDID LABELID # Add label to a card
|
||||
@@ -750,3 +756,273 @@ if arguments == 1:
|
||||
data2 = body.text.replace('}',"}\n")
|
||||
print(data2)
|
||||
# ------- LIST OF PUBLIC BOARDS END -----------
|
||||
|
||||
# ------- NEW ATTACHMENT API ENDPOINTS START -----------
|
||||
|
||||
if sys.argv[1] == 'uploadattachment':
|
||||
# ------- UPLOAD ATTACHMENT START -----------
|
||||
if arguments < 5:
|
||||
print("Usage: python3 api.py uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND]")
|
||||
print("Storage backends: fs, gridfs, s3")
|
||||
exit(1)
|
||||
|
||||
boardid = sys.argv[2]
|
||||
swimlaneid = sys.argv[3]
|
||||
listid = sys.argv[4]
|
||||
cardid = sys.argv[5]
|
||||
filepath = sys.argv[6]
|
||||
storage_backend = sys.argv[7] if arguments > 6 else None
|
||||
|
||||
# Read file and convert to base64
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
file_data = f.read()
|
||||
import base64
|
||||
base64_data = base64.b64encode(file_data).decode('utf-8')
|
||||
except FileNotFoundError:
|
||||
print(f"Error: File '{filepath}' not found")
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error reading file: {e}")
|
||||
exit(1)
|
||||
|
||||
# Get file info
|
||||
import os
|
||||
filename = os.path.basename(filepath)
|
||||
import mimetypes
|
||||
file_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
|
||||
|
||||
# Prepare request data
|
||||
upload_data = {
|
||||
'boardId': boardid,
|
||||
'swimlaneId': swimlaneid,
|
||||
'listId': listid,
|
||||
'cardId': cardid,
|
||||
'fileData': base64_data,
|
||||
'fileName': filename,
|
||||
'fileType': file_type
|
||||
}
|
||||
|
||||
if storage_backend:
|
||||
upload_data['storageBackend'] = storage_backend
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
upload_url = wekanurl + 'api/attachment/upload'
|
||||
|
||||
try:
|
||||
response = requests.post(upload_url, headers=headers, json=upload_data)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
print(f"Upload successful!")
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"File: {result.get('fileName')}")
|
||||
print(f"Size: {result.get('fileSize')} bytes")
|
||||
print(f"Storage: {result.get('storageBackend')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Upload failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- UPLOAD ATTACHMENT END -----------
|
||||
|
||||
if sys.argv[1] == 'downloadattachment':
|
||||
# ------- DOWNLOAD ATTACHMENT START -----------
|
||||
if arguments < 3:
|
||||
print("Usage: python3 api.py downloadattachment ATTACHMENTID OUTPUTPATH")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
outputpath = sys.argv[3]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
download_url = wekanurl + f'api/attachment/download/{attachmentid}'
|
||||
|
||||
try:
|
||||
response = requests.get(download_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
# Decode base64 data and save to file
|
||||
import base64
|
||||
file_data = base64.b64decode(result.get('base64Data'))
|
||||
|
||||
with open(outputpath, 'wb') as f:
|
||||
f.write(file_data)
|
||||
|
||||
print(f"Download successful!")
|
||||
print(f"File saved to: {outputpath}")
|
||||
print(f"Original filename: {result.get('fileName')}")
|
||||
print(f"Size: {result.get('fileSize')} bytes")
|
||||
print(f"Storage: {result.get('storageBackend')}")
|
||||
else:
|
||||
print(f"Download failed: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Download failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- DOWNLOAD ATTACHMENT END -----------
|
||||
|
||||
if sys.argv[1] == 'attachmentinfo':
|
||||
# ------- ATTACHMENT INFO START -----------
|
||||
if arguments < 2:
|
||||
print("Usage: python3 api.py attachmentinfo ATTACHMENTID")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
info_url = wekanurl + f'api/attachment/info/{attachmentid}'
|
||||
|
||||
try:
|
||||
response = requests.get(info_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
print("=== ATTACHMENT INFO ===")
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"File Name: {result.get('fileName')}")
|
||||
print(f"File Size: {result.get('fileSize')} bytes")
|
||||
print(f"File Type: {result.get('fileType')}")
|
||||
print(f"Storage Backend: {result.get('storageBackend')}")
|
||||
print(f"Board ID: {result.get('boardId')}")
|
||||
print(f"Swimlane ID: {result.get('swimlaneId')}")
|
||||
print(f"List ID: {result.get('listId')}")
|
||||
print(f"Card ID: {result.get('cardId')}")
|
||||
print(f"Created At: {result.get('createdAt')}")
|
||||
print(f"Is Image: {result.get('isImage')}")
|
||||
print(f"Versions: {len(result.get('versions', []))}")
|
||||
else:
|
||||
print(f"Failed to get attachment info: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- ATTACHMENT INFO END -----------
|
||||
|
||||
if sys.argv[1] == 'listcardattachments':
|
||||
# ------- LIST CARD ATTACHMENTS START -----------
|
||||
if arguments < 5:
|
||||
print("Usage: python3 api.py listcardattachments BOARDID SWIMLANEID LISTID CARDID")
|
||||
exit(1)
|
||||
|
||||
boardid = sys.argv[2]
|
||||
swimlaneid = sys.argv[3]
|
||||
listid = sys.argv[4]
|
||||
cardid = sys.argv[5]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
list_url = wekanurl + f'api/attachment/list/{boardid}/{swimlaneid}/{listid}/{cardid}'
|
||||
|
||||
try:
|
||||
response = requests.get(list_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
attachments = result.get('attachments', [])
|
||||
print(f"=== CARD ATTACHMENTS ({len(attachments)}) ===")
|
||||
for attachment in attachments:
|
||||
print(f"ID: {attachment.get('attachmentId')}")
|
||||
print(f"Name: {attachment.get('fileName')}")
|
||||
print(f"Size: {attachment.get('fileSize')} bytes")
|
||||
print(f"Type: {attachment.get('fileType')}")
|
||||
print(f"Storage: {attachment.get('storageBackend')}")
|
||||
print(f"Created: {attachment.get('createdAt')}")
|
||||
print("---")
|
||||
else:
|
||||
print(f"Failed to list attachments: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- LIST CARD ATTACHMENTS END -----------
|
||||
|
||||
if sys.argv[1] == 'copymoveattachment':
|
||||
# ------- COPY/MOVE ATTACHMENT START -----------
|
||||
if arguments < 6:
|
||||
print("Usage: python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move]")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
targetboardid = sys.argv[3]
|
||||
targetswimlaneid = sys.argv[4]
|
||||
targetlistid = sys.argv[5]
|
||||
targetcardid = sys.argv[6]
|
||||
operation = sys.argv[7] if arguments > 6 else 'copy'
|
||||
|
||||
if operation not in ['copy', 'move']:
|
||||
print("Operation must be 'copy' or 'move'")
|
||||
exit(1)
|
||||
|
||||
# Prepare request data
|
||||
request_data = {
|
||||
'attachmentId': attachmentid,
|
||||
'targetBoardId': targetboardid,
|
||||
'targetSwimlaneId': targetswimlaneid,
|
||||
'targetListId': targetlistid,
|
||||
'targetCardId': targetcardid
|
||||
}
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
api_url = wekanurl + f'api/attachment/{operation}'
|
||||
|
||||
try:
|
||||
response = requests.post(api_url, headers=headers, json=request_data)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
print(f"{operation.capitalize()} successful!")
|
||||
if operation == 'copy':
|
||||
print(f"Source Attachment ID: {result.get('sourceAttachmentId')}")
|
||||
print(f"New Attachment ID: {result.get('newAttachmentId')}")
|
||||
else:
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"Source Board: {result.get('sourceBoardId')}")
|
||||
print(f"Target Board: {result.get('targetBoardId')}")
|
||||
print(f"File: {result.get('fileName')}")
|
||||
print(f"Size: {result.get('fileSize')} bytes")
|
||||
else:
|
||||
print(f"{operation.capitalize()} failed: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"{operation.capitalize()} failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- COPY/MOVE ATTACHMENT END -----------
|
||||
|
||||
if sys.argv[1] == 'deleteattachment':
|
||||
# ------- DELETE ATTACHMENT START -----------
|
||||
if arguments < 2:
|
||||
print("Usage: python3 api.py deleteattachment ATTACHMENTID")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
delete_url = wekanurl + f'api/attachment/delete/{attachmentid}'
|
||||
|
||||
try:
|
||||
response = requests.delete(delete_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
print("Delete successful!")
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"File: {result.get('fileName')}")
|
||||
else:
|
||||
print(f"Delete failed: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Delete failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- DELETE ATTACHMENT END -----------
|
||||
|
||||
# ------- NEW ATTACHMENT API ENDPOINTS END -----------
|
||||
@@ -4,3 +4,14 @@ if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/pwa-service-worker.js');
|
||||
});
|
||||
}
|
||||
|
||||
// Import board converter for on-demand conversion
|
||||
import '/client/lib/boardConverter';
|
||||
import '/client/components/boardConversionProgress';
|
||||
|
||||
// Import migration manager and progress UI
|
||||
import '/client/lib/migrationManager';
|
||||
import '/client/components/migrationProgress';
|
||||
|
||||
// Import cron settings
|
||||
import '/client/components/settings/cronSettings';
|
||||
@@ -1,16 +1,16 @@
|
||||
.activity-title {
|
||||
margin: 0 0.5em 0.8em;
|
||||
margin: 0 0.7vw 1vh;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.reactions-popup .add-comment-reaction {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
font-size: 22px;
|
||||
border-radius: 0.7vw;
|
||||
font-size: clamp(18px, 4vw, 22px);
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
width: 40px;
|
||||
line-height: 1.3;
|
||||
width: 5vw;
|
||||
}
|
||||
.reactions-popup .add-comment-reaction:hover {
|
||||
background-color: #b0c4de;
|
||||
@@ -19,13 +19,13 @@
|
||||
clear: both;
|
||||
}
|
||||
.activities .activity {
|
||||
margin: 0.5px 0;
|
||||
padding: 6px 0;
|
||||
margin: 0.1vh 0;
|
||||
padding: 0.8vh 0;
|
||||
display: flex;
|
||||
}
|
||||
.activities .activity .member {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 4vw;
|
||||
height: 4vw;
|
||||
}
|
||||
.activities .activity .activity-member {
|
||||
font-weight: 700;
|
||||
@@ -36,18 +36,18 @@
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
margin-left: 3px;
|
||||
margin-left: 0.4vw;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
.activities .activity .activity-desc .activity-comment {
|
||||
display: block;
|
||||
border-radius: 3px;
|
||||
border-radius: 0.4vw;
|
||||
background: #fff;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
margin-top: 5px;
|
||||
padding: 5px;
|
||||
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.2);
|
||||
margin-top: 0.7vh;
|
||||
padding: 0.7vh 0.7vw;
|
||||
}
|
||||
.activities .activity .activity-desc .activity-checklist {
|
||||
display: block;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
|
||||
const activitiesPerPage = 500;
|
||||
@@ -216,15 +217,11 @@ BlazeComponent.extendComponent({
|
||||
{
|
||||
href: source.url,
|
||||
},
|
||||
DOMPurify.sanitize(source.system, {
|
||||
ALLOW_UNKNOWN_PROTOCOLS: true,
|
||||
}),
|
||||
sanitizeHTML(source.system),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return DOMPurify.sanitize(source.system, {
|
||||
ALLOW_UNKNOWN_PROTOCOLS: true,
|
||||
});
|
||||
return sanitizeHTML(source.system);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -248,10 +245,10 @@ BlazeComponent.extendComponent({
|
||||
href: `${attachment.link()}?download=true`,
|
||||
target: '_blank',
|
||||
},
|
||||
DOMPurify.sanitize(attachment.name),
|
||||
sanitizeText(attachment.name),
|
||||
),
|
||||
)) ||
|
||||
DOMPurify.sanitize(this.currentData().activity.attachmentName)
|
||||
sanitizeText(this.currentData().activity.attachmentName)
|
||||
);
|
||||
},
|
||||
|
||||
@@ -265,7 +262,7 @@ BlazeComponent.extendComponent({
|
||||
|
||||
Template.activity.helpers({
|
||||
sanitize(value) {
|
||||
return DOMPurify.sanitize(value, { ALLOW_UNKNOWN_PROTOCOLS: true });
|
||||
return sanitizeHTML(value);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -336,7 +333,7 @@ function createCardLink(card, board) {
|
||||
href: card.originRelativeUrl(),
|
||||
class: 'action-card',
|
||||
},
|
||||
DOMPurify.sanitize(text, { ALLOW_UNKNOWN_PROTOCOLS: true }),
|
||||
sanitizeHTML(text),
|
||||
),
|
||||
)
|
||||
);
|
||||
@@ -353,7 +350,7 @@ function createBoardLink(board, list) {
|
||||
href: board.originRelativeUrl(),
|
||||
class: 'action-board',
|
||||
},
|
||||
DOMPurify.sanitize(text, { ALLOW_UNKNOWN_PROTOCOLS: true }),
|
||||
sanitizeHTML(text),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
/* Board Conversion Progress Styles */
|
||||
.board-conversion-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.board-conversion-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.board-conversion-modal {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.board-conversion-header {
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.board-conversion-header h3 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.board-conversion-header h3 i {
|
||||
margin-right: 8px;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.board-conversion-header p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.board-conversion-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.conversion-progress {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #2196F3, #21CBF3);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.3),
|
||||
transparent
|
||||
);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #2196F3;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.conversion-status {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.conversion-status i {
|
||||
margin-right: 8px;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.conversion-time {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
background-color: #f5f5f5;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.conversion-time i {
|
||||
margin-right: 6px;
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
.board-conversion-footer {
|
||||
padding: 16px 24px 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.conversion-info {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.conversion-info i {
|
||||
margin-right: 6px;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 600px) {
|
||||
.board-conversion-modal {
|
||||
width: 95%;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.board-conversion-header,
|
||||
.board-conversion-content,
|
||||
.board-conversion-footer {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.board-conversion-header h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
template(name="boardConversionProgress")
|
||||
.board-conversion-overlay(class="{{#if isConverting}}active{{/if}}")
|
||||
.board-conversion-modal
|
||||
.board-conversion-header
|
||||
h3
|
||||
i.fa.fa-cogs
|
||||
| {{_ 'converting-board'}}
|
||||
p {{_ 'converting-board-description'}}
|
||||
|
||||
.board-conversion-content
|
||||
.conversion-progress
|
||||
.progress-bar
|
||||
.progress-fill(style="width: {{conversionProgress}}%")
|
||||
.progress-text {{conversionProgress}}%
|
||||
|
||||
.conversion-status
|
||||
i.fa.fa-spinner.fa-spin
|
||||
| {{conversionStatus}}
|
||||
|
||||
.conversion-time(style="{{#unless conversionEstimatedTime}}display: none;{{/unless}}")
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'estimated-time-remaining'}}: {{conversionEstimatedTime}}
|
||||
|
||||
.board-conversion-footer
|
||||
.conversion-info
|
||||
i.fa.fa-info-circle
|
||||
| {{_ 'conversion-info-text'}}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import {
|
||||
boardConverter,
|
||||
isConverting,
|
||||
conversionProgress,
|
||||
conversionStatus,
|
||||
conversionEstimatedTime
|
||||
} from '/client/lib/boardConverter';
|
||||
|
||||
Template.boardConversionProgress.helpers({
|
||||
isConverting() {
|
||||
return isConverting.get();
|
||||
},
|
||||
|
||||
conversionProgress() {
|
||||
return conversionProgress.get();
|
||||
},
|
||||
|
||||
conversionStatus() {
|
||||
return conversionStatus.get();
|
||||
},
|
||||
|
||||
conversionEstimatedTime() {
|
||||
return conversionEstimatedTime.get();
|
||||
}
|
||||
});
|
||||
|
||||
Template.boardConversionProgress.onCreated(function() {
|
||||
// Subscribe to conversion state changes
|
||||
this.autorun(() => {
|
||||
isConverting.get();
|
||||
conversionProgress.get();
|
||||
conversionStatus.get();
|
||||
conversionEstimatedTime.get();
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,27 @@
|
||||
bottom: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
/* When zoom is 50% or lower, ensure full width like content */
|
||||
.board-wrapper[style*="transform: scale(0.5)"] {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.board-wrapper[style*="transform: scale(0.4)"] {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.board-wrapper[style*="transform: scale(0.3)"] {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.board-wrapper .board-canvas {
|
||||
position: absolute;
|
||||
@@ -15,6 +36,219 @@
|
||||
bottom: 0;
|
||||
transition: margin 0.1s;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
/* Ensure horizontal scrollbar is visible for high zoom levels */
|
||||
.board-wrapper .board-canvas[style*="overflow-x: auto"] {
|
||||
overflow-x: auto !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
/* Style the scrollbar for better visibility */
|
||||
.board-wrapper .board-canvas::-webkit-scrollbar {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.board-wrapper .board-canvas::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.board-wrapper .board-canvas::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.board-wrapper .board-canvas::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Ensure content area can scroll horizontally for high zoom levels */
|
||||
#content[style*="overflow-x: auto"] {
|
||||
overflow-x: auto !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
/* Style content area scrollbar */
|
||||
#content::-webkit-scrollbar {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
#content::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
#content::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
#content::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Ensure scrollbars are always visible for high zoom levels */
|
||||
#content[style*="overflow-x: auto"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#content[style*="overflow-x: auto"]::-webkit-scrollbar {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
/* Force vertical scrollbar to always be visible */
|
||||
#content[style*="overflow-y: auto"] {
|
||||
overflow-y: scroll !important;
|
||||
}
|
||||
|
||||
/* Mobile - make all text 2x bigger inside #content by default (icons stay same size) */
|
||||
@media screen and (max-width: 800px),
|
||||
screen and (max-device-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: landscape) {
|
||||
#content {
|
||||
font-size: 2em !important; /* 2x bigger base font size for content area */
|
||||
}
|
||||
|
||||
/* Make all text elements 2x bigger */
|
||||
#content h1, #content h2, #content h3, #content h4, #content h5, #content h6,
|
||||
#content p, #content span, #content div, #content a, #content button,
|
||||
#content .minicard, #content .list-header-name, #content .board-header-btn,
|
||||
#content .card-title, #content .card-details, #content .card-description,
|
||||
#content .swimlane-header, #content .list-title, #content .card-text,
|
||||
#content .member, #content .member-name, #content .member-initials,
|
||||
#content .checklist-item, #content .checklist-title, #content .comment,
|
||||
#content .activity, #content .activity-text, #content .activity-time,
|
||||
#content .board-title, #content .board-description, #content .list-name,
|
||||
#content .card-text, #content .card-title, #content .card-description,
|
||||
#content .swimlane-title, #content .swimlane-description,
|
||||
#content .board-header-title, #content .board-header-description,
|
||||
#content .card-detail-title, #content .card-detail-description,
|
||||
#content .list-header-title, #content .list-header-description,
|
||||
#content .swimlane-header-title, #content .swimlane-header-description,
|
||||
#content .minicard-title, #content .minicard-description,
|
||||
#content .card-comment, #content .card-comment-text,
|
||||
#content .checklist-item-text, #content .checklist-item-title,
|
||||
#content .activity-item, #content .activity-item-text,
|
||||
#content .board-member, #content .board-member-name,
|
||||
#content .team-member, #content .team-member-name,
|
||||
#content .org-member, #content .org-member-name,
|
||||
#content .template-member, #content .template-member-name,
|
||||
#content .user-name, #content .user-email, #content .user-role,
|
||||
#content .setting-title, #content .setting-description,
|
||||
#content .popup-title, #content .popup-description,
|
||||
#content .modal-title, #content .modal-description,
|
||||
#content .notification-title, #content .notification-text,
|
||||
#content .announcement-title, #content .announcement-text,
|
||||
#content .offline-warning-title, #content .offline-warning-text,
|
||||
#content .error-title, #content .error-text,
|
||||
#content .success-title, #content .success-text,
|
||||
#content .info-title, #content .info-text,
|
||||
#content .warning-title, #content .warning-text {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
/* Keep icons the same size (don't scale them) */
|
||||
#content .fa, #content .icon, #content i {
|
||||
font-size: 1em !important; /* Keep original icon size */
|
||||
}
|
||||
|
||||
/* Reset specific icon sizes to prevent double scaling */
|
||||
#content .fa-home, #content .fa-bars, #content .fa-search,
|
||||
#content .fa-bell, #content .fa-user, #content .fa-cog,
|
||||
#content .fa-plus, #content .fa-minus, #content .fa-edit,
|
||||
#content .fa-trash, #content .fa-save, #content .fa-cancel,
|
||||
#content .fa-arrow-left, #content .fa-arrow-right,
|
||||
#content .fa-arrow-up, #content .fa-arrow-down,
|
||||
#content .fa-check, #content .fa-times, #content .fa-close,
|
||||
#content .fa-star, #content .fa-heart, #content .fa-thumbs-up,
|
||||
#content .fa-thumbs-down, #content .fa-comment, #content .fa-reply,
|
||||
#content .fa-share, #content .fa-download, #content .fa-upload,
|
||||
#content .fa-copy, #content .fa-paste, #content .fa-cut,
|
||||
#content .fa-undo, #content .fa-redo, #content .fa-refresh,
|
||||
#content .fa-sync, #content .fa-spinner, #content .fa-loading,
|
||||
#content .fa-info, #content .fa-question, #content .fa-exclamation,
|
||||
#content .fa-warning, #content .fa-error, #content .fa-success,
|
||||
#content .fa-check-circle, #content .fa-times-circle,
|
||||
#content .fa-exclamation-circle, #content .fa-question-circle,
|
||||
#content .fa-info-circle, #content .fa-warning-circle,
|
||||
#content .fa-error-circle, #content .fa-success-circle {
|
||||
font-size: 1em !important; /* Keep original icon size */
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback for iPhone devices using JavaScript detection */
|
||||
.iphone-device #content {
|
||||
font-size: 2em !important; /* 2x bigger base font size for content area */
|
||||
}
|
||||
|
||||
.iphone-device #content h1, .iphone-device #content h2, .iphone-device #content h3, .iphone-device #content h4, .iphone-device #content h5, .iphone-device #content h6,
|
||||
.iphone-device #content p, .iphone-device #content span, .iphone-device #content div, .iphone-device #content a, .iphone-device #content button,
|
||||
.iphone-device #content .minicard, .iphone-device #content .list-header-name, .iphone-device #content .board-header-btn,
|
||||
.iphone-device #content .card-title, .iphone-device #content .card-details, .iphone-device #content .card-description,
|
||||
.iphone-device #content .swimlane-header, .iphone-device #content .list-title, .iphone-device #content .card-text,
|
||||
.iphone-device #content .member, .iphone-device #content .member-name, .iphone-device #content .member-initials,
|
||||
.iphone-device #content .checklist-item, .iphone-device #content .checklist-title, .iphone-device #content .comment,
|
||||
.iphone-device #content .activity, .iphone-device #content .activity-text, .iphone-device #content .activity-time,
|
||||
.iphone-device #content .board-title, .iphone-device #content .board-description, .iphone-device #content .list-name,
|
||||
.iphone-device #content .card-text, .iphone-device #content .card-title, .iphone-device #content .card-description,
|
||||
.iphone-device #content .swimlane-title, .iphone-device #content .swimlane-description,
|
||||
.iphone-device #content .board-header-title, .iphone-device #content .board-header-description,
|
||||
.iphone-device #content .card-detail-title, .iphone-device #content .card-detail-description,
|
||||
.iphone-device #content .list-header-title, .iphone-device #content .list-header-description,
|
||||
.iphone-device #content .swimlane-header-title, .iphone-device #content .swimlane-header-description,
|
||||
.iphone-device #content .minicard-title, .iphone-device #content .minicard-description,
|
||||
.iphone-device #content .card-comment, .iphone-device #content .card-comment-text,
|
||||
.iphone-device #content .checklist-item-text, .iphone-device #content .checklist-item-title,
|
||||
.iphone-device #content .activity-item, .iphone-device #content .activity-item-text,
|
||||
.iphone-device #content .board-member, .iphone-device #content .board-member-name,
|
||||
.iphone-device #content .team-member, .iphone-device #content .team-member-name,
|
||||
.iphone-device #content .org-member, .iphone-device #content .org-member-name,
|
||||
.iphone-device #content .template-member, .iphone-device #content .template-member-name,
|
||||
.iphone-device #content .user-name, .iphone-device #content .user-email, .iphone-device #content .user-role,
|
||||
.iphone-device #content .setting-title, .iphone-device #content .setting-description,
|
||||
.iphone-device #content .popup-title, .iphone-device #content .popup-description,
|
||||
.iphone-device #content .modal-title, .iphone-device #content .modal-description,
|
||||
.iphone-device #content .notification-title, .iphone-device #content .notification-text,
|
||||
.iphone-device #content .announcement-title, .iphone-device #content .announcement-text,
|
||||
.iphone-device #content .offline-warning-title, .iphone-device #content .offline-warning-text,
|
||||
.iphone-device #content .error-title, .iphone-device #content .error-text,
|
||||
.iphone-device #content .success-title, .iphone-device #content .success-text,
|
||||
.iphone-device #content .info-title, .iphone-device #content .info-text,
|
||||
.iphone-device #content .warning-title, .iphone-device #content .warning-text {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
/* Keep icons the same size for iPhone devices */
|
||||
.iphone-device #content .fa, .iphone-device #content .icon, .iphone-device #content i {
|
||||
font-size: 1em !important; /* Keep original icon size */
|
||||
}
|
||||
|
||||
/* Ensure scrollbars are positioned correctly */
|
||||
#content[style*="overflow-x: auto"]::-webkit-scrollbar:vertical {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
#content[style*="overflow-x: auto"]::-webkit-scrollbar:horizontal {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
/* Force both scrollbars to always be visible for high zoom levels */
|
||||
#content[style*="overflow-x: auto"][style*="overflow-y: auto"] {
|
||||
overflow-x: scroll !important;
|
||||
overflow-y: scroll !important;
|
||||
}
|
||||
|
||||
/* Ensure the right scrollbar is always visible on the inner right edge */
|
||||
#content[style*="overflow-y: auto"] {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
.board-wrapper .board-canvas .board-overlay {
|
||||
position: fixed;
|
||||
@@ -33,15 +267,58 @@
|
||||
.board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked {
|
||||
display: none;
|
||||
}
|
||||
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
|
||||
.board-wrapper.mobile-view {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
|
||||
.board-wrapper.mobile-view .board-canvas {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
|
||||
.board-wrapper.mobile-view .board-canvas.mobile-view .swimlane {
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.board-wrapper {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
|
||||
.board-wrapper .board-canvas {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
|
||||
.board-wrapper .board-canvas .swimlane {
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0 0px 0px 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
.calendar-event-green {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
template(name="board")
|
||||
if isBoardReady.get
|
||||
|
||||
if isMigrating.get
|
||||
+migrationProgress
|
||||
else if isConverting.get
|
||||
+boardConversionProgress
|
||||
else if isBoardReady.get
|
||||
if currentBoard
|
||||
if onlyShowCurrentCard
|
||||
+cardDetails(currentCard)
|
||||
@@ -16,13 +21,18 @@ template(name="boardBody")
|
||||
if notDisplayThisBoard
|
||||
| {{_ 'tableVisibilityMode-allowPrivateOnly'}}
|
||||
else
|
||||
.board-wrapper(class=currentBoard.colorClass)
|
||||
// Debug information (remove in production)
|
||||
if debugBoardState
|
||||
.debug-info(style="position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.8); color: white; padding: 10px; z-index: 9999; font-size: 12px;")
|
||||
| Board: {{currentBoard.title}} | View: {{boardView}} | HasSwimlanes: {{hasSwimlanes}} | Swimlanes: {{currentBoard.swimlanes.length}}
|
||||
.board-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
.board-canvas.js-swimlanes(
|
||||
class="{{#if hasSwimlanes}}dragscroll{{/if}}"
|
||||
class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
|
||||
class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
|
||||
class="{{#if draggingActive.get}}is-dragging-active{{/if}}"
|
||||
class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}")
|
||||
class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}"
|
||||
class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
if showOverlay.get
|
||||
.board-overlay
|
||||
if currentBoard.isTemplatesBoard
|
||||
@@ -33,15 +43,19 @@ template(name="boardBody")
|
||||
each currentBoard.swimlanes
|
||||
+swimlane(this)
|
||||
else
|
||||
a.js-empty-board-add-swimlane(title="{{_ 'add-swimlane'}}")
|
||||
h1.big-message.quiet
|
||||
| {{_ 'add-swimlane'}} +
|
||||
// Fallback: If no swimlanes exist, show lists instead of empty message
|
||||
+listsGroup(currentBoard)
|
||||
else if isViewLists
|
||||
+listsGroup(currentBoard)
|
||||
else if isViewCalendar
|
||||
+calendarView
|
||||
else
|
||||
+listsGroup(currentBoard)
|
||||
// Default view - show swimlanes if they exist, otherwise show lists
|
||||
if hasSwimlanes
|
||||
each currentBoard.swimlanes
|
||||
+swimlane(this)
|
||||
else
|
||||
+listsGroup(currentBoard)
|
||||
+sidebar
|
||||
|
||||
template(name="calendarView")
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import dragscroll from '@wekanteam/dragscroll';
|
||||
import { boardConverter } from '/client/lib/boardConverter';
|
||||
import { migrationManager } from '/client/lib/migrationManager';
|
||||
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
|
||||
import Swimlanes from '/models/swimlanes';
|
||||
import Lists from '/models/lists';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
const { calculateIndex } = Utils;
|
||||
@@ -9,6 +14,9 @@ const swimlaneWhileSortingHeight = 150;
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.isBoardReady = new ReactiveVar(false);
|
||||
this.isConverting = new ReactiveVar(false);
|
||||
this.isMigrating = new ReactiveVar(false);
|
||||
this._swimlaneCreated = new Set(); // Track boards where we've created swimlanes
|
||||
|
||||
// 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
|
||||
@@ -17,22 +25,175 @@ BlazeComponent.extendComponent({
|
||||
this.autorun(() => {
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
if (!currentBoardId) return;
|
||||
|
||||
const handle = subManager.subscribe('board', currentBoardId, false);
|
||||
|
||||
Tracker.nonreactive(() => {
|
||||
Tracker.autorun(() => {
|
||||
this.isBoardReady.set(handle.ready());
|
||||
if (handle.ready()) {
|
||||
// Ensure default swimlane exists (only once per board)
|
||||
this.ensureDefaultSwimlane(currentBoardId);
|
||||
// Check if board needs conversion
|
||||
this.checkAndConvertBoard(currentBoardId);
|
||||
} else {
|
||||
this.isBoardReady.set(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
ensureDefaultSwimlane(boardId) {
|
||||
// Only create swimlane once per board
|
||||
if (this._swimlaneCreated.has(boardId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) return;
|
||||
|
||||
const swimlanes = board.swimlanes();
|
||||
|
||||
if (swimlanes.length === 0) {
|
||||
const swimlaneId = Swimlanes.insert({
|
||||
title: 'Default',
|
||||
boardId: boardId,
|
||||
});
|
||||
this._swimlaneCreated.add(boardId);
|
||||
} else {
|
||||
this._swimlaneCreated.add(boardId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating default swimlane:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async checkAndConvertBoard(boardId) {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) {
|
||||
this.isBoardReady.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if board needs migration based on migration version
|
||||
const needsMigration = !board.migrationVersion || board.migrationVersion < 1;
|
||||
|
||||
if (needsMigration) {
|
||||
// Start background migration for old boards
|
||||
this.isMigrating.set(true);
|
||||
await this.startBackgroundMigration(boardId);
|
||||
this.isMigrating.set(false);
|
||||
}
|
||||
|
||||
// Check if board needs conversion (for old structure)
|
||||
if (boardConverter.isBoardConverted(boardId)) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has already been converted, skipping conversion`);
|
||||
}
|
||||
this.isBoardReady.set(true);
|
||||
} else {
|
||||
const needsConversion = boardConverter.needsConversion(boardId);
|
||||
|
||||
if (needsConversion) {
|
||||
this.isConverting.set(true);
|
||||
const success = await boardConverter.convertBoard(boardId);
|
||||
this.isConverting.set(false);
|
||||
|
||||
if (success) {
|
||||
this.isBoardReady.set(true);
|
||||
} else {
|
||||
console.error('Board conversion failed, setting ready to true anyway');
|
||||
this.isBoardReady.set(true); // Still show board even if conversion failed
|
||||
}
|
||||
} else {
|
||||
this.isBoardReady.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Start attachment migration in background if needed
|
||||
this.startAttachmentMigrationIfNeeded(boardId);
|
||||
} catch (error) {
|
||||
console.error('Error during board conversion check:', error);
|
||||
this.isConverting.set(false);
|
||||
this.isMigrating.set(false);
|
||||
this.isBoardReady.set(true); // Show board even if conversion check failed
|
||||
}
|
||||
},
|
||||
|
||||
async startBackgroundMigration(boardId) {
|
||||
try {
|
||||
// Start background migration using the cron system
|
||||
Meteor.call('boardMigration.startBoardMigration', boardId, (error, result) => {
|
||||
if (error) {
|
||||
console.error('Failed to start background migration:', error);
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Background migration started for board:', boardId);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error starting background migration:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async startAttachmentMigrationIfNeeded(boardId) {
|
||||
try {
|
||||
// Check if board has already been migrated
|
||||
if (attachmentMigrationManager.isBoardMigrated(boardId)) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has already been migrated, skipping`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there are unconverted attachments
|
||||
const unconvertedAttachments = attachmentMigrationManager.getUnconvertedAttachments(boardId);
|
||||
|
||||
if (unconvertedAttachments.length > 0) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Starting attachment migration for ${unconvertedAttachments.length} attachments in board ${boardId}`);
|
||||
}
|
||||
await attachmentMigrationManager.startAttachmentMigration(boardId);
|
||||
} else {
|
||||
// No attachments to migrate, mark board as migrated
|
||||
// This will be handled by the migration manager itself
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has no attachments to migrate`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting attachment migration:', error);
|
||||
}
|
||||
},
|
||||
|
||||
onlyShowCurrentCard() {
|
||||
return Utils.isMiniScreen() && Utils.getCurrentCardId(true);
|
||||
const isMiniScreen = Utils.isMiniScreen();
|
||||
const currentCardId = Utils.getCurrentCardId(true);
|
||||
return isMiniScreen && currentCardId;
|
||||
},
|
||||
|
||||
goHome() {
|
||||
FlowRouter.go('home');
|
||||
},
|
||||
|
||||
isConverting() {
|
||||
return this.isConverting.get();
|
||||
},
|
||||
|
||||
isMigrating() {
|
||||
return this.isMigrating.get();
|
||||
},
|
||||
|
||||
isBoardReady() {
|
||||
return this.isBoardReady.get();
|
||||
},
|
||||
|
||||
currentBoard() {
|
||||
return Utils.getCurrentBoard();
|
||||
},
|
||||
}).register('board');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
@@ -46,36 +207,227 @@ BlazeComponent.extendComponent({
|
||||
|
||||
// fix swimlanes sort field if there are null values
|
||||
const currentBoardData = Utils.getCurrentBoard();
|
||||
const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
|
||||
if (nullSortSwimlanes.length > 0) {
|
||||
const swimlanes = currentBoardData.swimlanes();
|
||||
let count = 0;
|
||||
swimlanes.forEach(s => {
|
||||
Swimlanes.update(s._id, {
|
||||
$set: {
|
||||
sort: count,
|
||||
},
|
||||
if (currentBoardData && Swimlanes) {
|
||||
const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
|
||||
if (nullSortSwimlanes.length > 0) {
|
||||
const swimlanes = currentBoardData.swimlanes();
|
||||
let count = 0;
|
||||
swimlanes.forEach(s => {
|
||||
Swimlanes.update(s._id, {
|
||||
$set: {
|
||||
sort: count,
|
||||
},
|
||||
});
|
||||
count += 1;
|
||||
});
|
||||
count += 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// fix lists sort field if there are null values
|
||||
const nullSortLists = currentBoardData.nullSortLists();
|
||||
if (nullSortLists.length > 0) {
|
||||
const lists = currentBoardData.lists();
|
||||
let count = 0;
|
||||
lists.forEach(l => {
|
||||
Lists.update(l._id, {
|
||||
$set: {
|
||||
sort: count,
|
||||
},
|
||||
if (currentBoardData && Lists) {
|
||||
const nullSortLists = currentBoardData.nullSortLists();
|
||||
if (nullSortLists.length > 0) {
|
||||
const lists = currentBoardData.lists();
|
||||
let count = 0;
|
||||
lists.forEach(l => {
|
||||
Lists.update(l._id, {
|
||||
$set: {
|
||||
sort: count,
|
||||
},
|
||||
});
|
||||
count += 1;
|
||||
});
|
||||
count += 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
onRendered() {
|
||||
// Initialize user settings (zoom and mobile mode)
|
||||
Utils.initializeUserSettings();
|
||||
|
||||
// Detect iPhone devices and add class for better CSS targeting
|
||||
const isIPhone = /iPhone|iPod/.test(navigator.userAgent);
|
||||
if (isIPhone) {
|
||||
document.body.classList.add('iphone-device');
|
||||
}
|
||||
|
||||
// Accessibility: Focus management for popups and menus
|
||||
function focusFirstInteractive(container) {
|
||||
if (!container) return;
|
||||
// Find first focusable element
|
||||
const focusable = container.querySelectorAll('button, [role="button"], a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
for (let i = 0; i < focusable.length; i++) {
|
||||
if (!focusable[i].disabled && focusable[i].offsetParent !== null) {
|
||||
focusable[i].focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Observe for new popups/menus and set focus (but exclude swimlane content)
|
||||
const popupObserver = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
mutation.addedNodes.forEach(function(node) {
|
||||
if (node.nodeType === 1 &&
|
||||
(node.classList.contains('popup') || node.classList.contains('modal') || node.classList.contains('menu')) &&
|
||||
!node.closest('.js-swimlanes') &&
|
||||
!node.closest('.swimlane') &&
|
||||
!node.closest('.list') &&
|
||||
!node.closest('.minicard')) {
|
||||
setTimeout(function() { focusFirstInteractive(node); }, 10);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
popupObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// Remove tabindex from non-interactive elements (e.g., user abbreviations, labels)
|
||||
document.querySelectorAll('.user-abbreviation, .user-label, .card-header-label, .edit-label, .private-label').forEach(function(el) {
|
||||
if (el.hasAttribute('tabindex')) {
|
||||
el.removeAttribute('tabindex');
|
||||
}
|
||||
});
|
||||
/*
|
||||
// Add a toggle button for keyboard shortcuts accessibility
|
||||
if (!document.getElementById('wekan-shortcuts-toggle')) {
|
||||
const toggleContainer = document.createElement('div');
|
||||
toggleContainer.id = 'wekan-shortcuts-toggle';
|
||||
toggleContainer.style.position = 'fixed';
|
||||
toggleContainer.style.top = '10px';
|
||||
toggleContainer.style.right = '10px';
|
||||
toggleContainer.style.zIndex = '1000';
|
||||
toggleContainer.style.background = '#fff';
|
||||
toggleContainer.style.border = '2px solid #005fcc';
|
||||
toggleContainer.style.borderRadius = '6px';
|
||||
toggleContainer.style.padding = '8px 12px';
|
||||
toggleContainer.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
|
||||
toggleContainer.style.fontSize = '16px';
|
||||
toggleContainer.style.color = '#005fcc';
|
||||
toggleContainer.setAttribute('role', 'region');
|
||||
toggleContainer.setAttribute('aria-label', 'Keyboard Shortcuts Settings');
|
||||
toggleContainer.innerHTML = `
|
||||
<label for="shortcuts-toggle-checkbox" style="cursor:pointer;">
|
||||
<input type="checkbox" id="shortcuts-toggle-checkbox" ${window.wekanShortcutsEnabled ? 'checked' : ''} style="margin-right:8px;" />
|
||||
Enable keyboard shortcuts
|
||||
</label>
|
||||
`;
|
||||
document.body.appendChild(toggleContainer);
|
||||
const checkbox = document.getElementById('shortcuts-toggle-checkbox');
|
||||
checkbox.addEventListener('change', function(e) {
|
||||
window.toggleWekanShortcuts(e.target.checked);
|
||||
});
|
||||
}
|
||||
*/
|
||||
// Ensure toggle-buttons, color choices, reactions, renaming, and calendar controls are focusable and have ARIA roles
|
||||
document.querySelectorAll('.js-toggle').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
// Short, descriptive label for favorite/star toggle
|
||||
if (el.classList.contains('js-favorite-toggle')) {
|
||||
el.setAttribute('aria-label', TAPi18n.__('favorite-toggle-label'));
|
||||
} else {
|
||||
el.setAttribute('aria-label', 'Toggle');
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('.js-color-choice').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
el.setAttribute('aria-label', 'Choose color');
|
||||
});
|
||||
document.querySelectorAll('.js-reaction').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
el.setAttribute('aria-label', 'React');
|
||||
});
|
||||
document.querySelectorAll('.js-rename-swimlane').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
el.setAttribute('aria-label', 'Rename swimlane');
|
||||
});
|
||||
document.querySelectorAll('.js-rename-list').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
el.setAttribute('aria-label', 'Rename list');
|
||||
});
|
||||
document.querySelectorAll('.fc-button').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
});
|
||||
// Set the language attribute on the <html> element for accessibility
|
||||
document.documentElement.lang = TAPi18n.getLanguage();
|
||||
|
||||
// Ensure the accessible name for the board view switcher matches the visible label "Swimlanes"
|
||||
// This fixes WCAG 2.5.3: Label in Name
|
||||
const swimlanesSwitcher = this.$('.js-board-view-swimlanes');
|
||||
if (swimlanesSwitcher.length) {
|
||||
swimlanesSwitcher.attr('aria-label', swimlanesSwitcher.text().trim() || 'Swimlanes');
|
||||
}
|
||||
|
||||
// Add a highly visible focus indicator and improve contrast for interactive elements
|
||||
if (!document.getElementById('wekan-accessible-focus-style')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'wekan-accessible-focus-style';
|
||||
style.innerHTML = `
|
||||
/* Focus indicator */
|
||||
button:focus, [role="button"]:focus, a:focus, input:focus, select:focus, textarea:focus, .dropdown-menu:focus, .js-board-view-swimlanes:focus, .js-add-card:focus {
|
||||
outline: 3px solid #005fcc !important;
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
/* Input borders */
|
||||
input, textarea, select {
|
||||
border: 2px solid #222 !important;
|
||||
}
|
||||
/* Plus icon for adding a new card */
|
||||
.js-add-card {
|
||||
color: #005fcc !important; /* dark blue for contrast */
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.js-add-card[tabindex] {
|
||||
outline: none;
|
||||
}
|
||||
/* Hamburger menu */
|
||||
.fa-bars, .icon-hamburger {
|
||||
color: #222 !important;
|
||||
}
|
||||
/* Grey icons in card detail header */
|
||||
.card-detail-header .fa, .card-detail-header .icon {
|
||||
color: #444 !important;
|
||||
}
|
||||
/* Grey operating elements in card detail */
|
||||
.card-detail .fa, .card-detail .icon {
|
||||
color: #444 !important;
|
||||
}
|
||||
/* Blue bar in checklists */
|
||||
.checklist-progress-bar {
|
||||
background-color: #005fcc !important;
|
||||
}
|
||||
/* Green checkmark in checklists */
|
||||
.checklist .fa-check {
|
||||
color: #007a33 !important;
|
||||
}
|
||||
/* X-Button and arrow button in menus */
|
||||
.close, .fa-arrow-left, .icon-arrow-left {
|
||||
color: #005fcc !important;
|
||||
}
|
||||
/* Cross icon to move boards */
|
||||
.js-move-board {
|
||||
color: #005fcc !important;
|
||||
}
|
||||
/* Current date background */
|
||||
.current-date {
|
||||
background-color: #005fcc !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
// Ensure plus/add elements are focusable and have ARIA roles
|
||||
document.querySelectorAll('.js-add-card').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
el.setAttribute('aria-label', 'Add new card');
|
||||
});
|
||||
|
||||
const boardComponent = this;
|
||||
const $swimlanesDom = boardComponent.$('.js-swimlanes');
|
||||
|
||||
@@ -230,51 +582,122 @@ BlazeComponent.extendComponent({
|
||||
notDisplayThisBoard() {
|
||||
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
|
||||
let currentBoard = Utils.getCurrentBoard();
|
||||
if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard.permission == 'public') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard && currentBoard.permission == 'public';
|
||||
},
|
||||
|
||||
isViewSwimlanes() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
if (currentUser) {
|
||||
return (currentUser.profile || {}).boardView === 'board-view-swimlanes';
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
return (
|
||||
window.localStorage.getItem('boardView') === 'board-view-swimlanes'
|
||||
);
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
},
|
||||
|
||||
hasSwimlanes() {
|
||||
return Utils.getCurrentBoard().swimlanes().length > 0;
|
||||
|
||||
// If no board view is set, default to swimlanes
|
||||
if (!boardView) {
|
||||
boardView = 'board-view-swimlanes';
|
||||
}
|
||||
|
||||
return boardView === 'board-view-swimlanes';
|
||||
},
|
||||
|
||||
isViewLists() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
if (currentUser) {
|
||||
return (currentUser.profile || {}).boardView === 'board-view-lists';
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
return window.localStorage.getItem('boardView') === 'board-view-lists';
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
|
||||
return boardView === 'board-view-lists';
|
||||
},
|
||||
|
||||
isViewCalendar() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
if (currentUser) {
|
||||
return (currentUser.profile || {}).boardView === 'board-view-cal';
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
return window.localStorage.getItem('boardView') === 'board-view-cal';
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
|
||||
return boardView === 'board-view-cal';
|
||||
},
|
||||
|
||||
hasSwimlanes() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
if (!currentBoard) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('hasSwimlanes: No current board');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const swimlanes = currentBoard.swimlanes();
|
||||
const hasSwimlanes = swimlanes && swimlanes.length > 0;
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('hasSwimlanes: Board has', swimlanes ? swimlanes.length : 0, 'swimlanes');
|
||||
}
|
||||
return hasSwimlanes;
|
||||
} catch (error) {
|
||||
console.error('hasSwimlanes: Error getting swimlanes:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
isVerticalScrollbars() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
return user && user.isVerticalScrollbars();
|
||||
},
|
||||
|
||||
boardView() {
|
||||
return Utils.boardView();
|
||||
},
|
||||
|
||||
debugBoardState() {
|
||||
// Enable debug mode by setting ?debug=1 in URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('debug') === '1';
|
||||
},
|
||||
|
||||
debugBoardStateData() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
const isBoardReady = this.isBoardReady.get();
|
||||
const isConverting = this.isConverting.get();
|
||||
const isMigrating = this.isMigrating.get();
|
||||
const boardView = Utils.boardView();
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('=== BOARD DEBUG STATE ===');
|
||||
console.log('currentBoardId:', currentBoardId);
|
||||
console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none');
|
||||
console.log('isBoardReady:', isBoardReady);
|
||||
console.log('isConverting:', isConverting);
|
||||
console.log('isMigrating:', isMigrating);
|
||||
console.log('boardView:', boardView);
|
||||
console.log('========================');
|
||||
}
|
||||
|
||||
return {
|
||||
currentBoardId,
|
||||
hasCurrentBoard: !!currentBoard,
|
||||
currentBoardTitle: currentBoard ? currentBoard.title : 'none',
|
||||
isBoardReady,
|
||||
isConverting,
|
||||
isMigrating,
|
||||
boardView
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
openNewListForm() {
|
||||
if (this.isViewSwimlanes()) {
|
||||
// The form had been removed in 416b17062e57f215206e93a85b02ef9eb1ab4902
|
||||
@@ -326,8 +749,109 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
}).register('boardBody');
|
||||
|
||||
// Accessibility: Allow users to enable/disable keyboard shortcuts
|
||||
window.wekanShortcutsEnabled = true;
|
||||
window.toggleWekanShortcuts = function(enabled) {
|
||||
window.wekanShortcutsEnabled = !!enabled;
|
||||
};
|
||||
|
||||
// Example: Wrap your character key shortcut handler like this
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// Example: "W" key shortcut (replace with your actual shortcut logic)
|
||||
if (!window.wekanShortcutsEnabled) return;
|
||||
if (e.key === 'w' || e.key === 'W') {
|
||||
// ...existing shortcut logic...
|
||||
// e.g. open swimlanes view, etc.
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard accessibility for card actions (favorite, archive, duplicate, etc.)
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (!window.wekanShortcutsEnabled) return;
|
||||
// Only proceed if focus is on a card action element
|
||||
const active = document.activeElement;
|
||||
if (active && active.classList.contains('js-card-action')) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
active.click();
|
||||
}
|
||||
// Move card up/down with arrow keys
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (active.dataset.cardId) {
|
||||
Meteor.call('moveCardUp', active.dataset.cardId);
|
||||
}
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (active.dataset.cardId) {
|
||||
Meteor.call('moveCardDown', active.dataset.cardId);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Make plus/add elements keyboard accessible
|
||||
if (active && active.classList.contains('js-add-card')) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
active.click();
|
||||
}
|
||||
}
|
||||
// Keyboard move for cards (alternative to drag & drop)
|
||||
if (active && active.classList.contains('js-move-card')) {
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (active.dataset.cardId) {
|
||||
Meteor.call('moveCardUp', active.dataset.cardId);
|
||||
}
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (active.dataset.cardId) {
|
||||
Meteor.call('moveCardDown', active.dataset.cardId);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ensure move card buttons are focusable and have ARIA roles
|
||||
document.querySelectorAll('.js-move-card').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
el.setAttribute('aria-label', 'Move card');
|
||||
});
|
||||
// Make toggle-buttons, color choices, reactions, and X-buttons keyboard accessible
|
||||
if (active && (active.classList.contains('js-toggle') || active.classList.contains('js-color-choice') || active.classList.contains('js-reaction') || active.classList.contains('close'))) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
active.click();
|
||||
}
|
||||
}
|
||||
// Prevent scripts from removing focus when received
|
||||
if (active) {
|
||||
active.addEventListener('focus', function(e) {
|
||||
// Do not remove focus
|
||||
// No-op: This prevents F55 failure
|
||||
}, { once: true });
|
||||
}
|
||||
// Make swimlane/list renaming keyboard accessible
|
||||
if (active && (active.classList.contains('js-rename-swimlane') || active.classList.contains('js-rename-list'))) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
active.click();
|
||||
}
|
||||
}
|
||||
// Calendar navigation buttons
|
||||
if (active && active.classList.contains('fc-button')) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
active.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
// Set the language attribute on the <html> element for accessibility
|
||||
document.documentElement.lang = TAPi18n.getLanguage();
|
||||
|
||||
this.autorun(function () {
|
||||
$('#calendar-view').fullCalendar('refetchEvents');
|
||||
});
|
||||
@@ -341,11 +865,19 @@ BlazeComponent.extendComponent({
|
||||
timezone: 'local',
|
||||
weekNumbers: true,
|
||||
header: {
|
||||
left: 'title today prev,next',
|
||||
left: 'title today prev,next',
|
||||
center:
|
||||
'agendaDay,listDay,timelineDay agendaWeek,listWeek,timelineWeek month,listMonth',
|
||||
right: '',
|
||||
},
|
||||
buttonText: {
|
||||
prev: TAPi18n.__('calendar-previous-month-label'), // e.g. "Previous month"
|
||||
next: TAPi18n.__('calendar-next-month-label'), // e.g. "Next month"
|
||||
},
|
||||
ariaLabel: {
|
||||
prev: TAPi18n.__('calendar-previous-month-label'),
|
||||
next: TAPi18n.__('calendar-next-month-label'),
|
||||
},
|
||||
// height: 'parent', nope, doesn't work as the parent might be small
|
||||
height: 'auto',
|
||||
/* TODO: lists as resources: https://fullcalendar.io/docs/vertical-resource-view */
|
||||
@@ -476,6 +1008,9 @@ BlazeComponent.extendComponent({
|
||||
document.body.appendChild(modalElement);
|
||||
const openModal = function() {
|
||||
modalElement.style.display = 'flex';
|
||||
// Set focus to the input field for better keyboard accessibility
|
||||
const input = modalElement.querySelector('#card-title-input');
|
||||
if (input) input.focus();
|
||||
};
|
||||
const closeModal = function() {
|
||||
modalElement.style.display = 'none';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - NEPHRITIS
|
||||
=================*/
|
||||
|
||||
@@ -99,7 +99,7 @@ THEME - NEPHRITIS
|
||||
|
||||
/* === END NEPHRITIS THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Pomegranate
|
||||
=================*/
|
||||
|
||||
@@ -200,7 +200,7 @@ THEME - Pomegranate
|
||||
|
||||
/* === END Pomegranate THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Belize
|
||||
=================*/
|
||||
|
||||
@@ -301,7 +301,7 @@ THEME - Belize
|
||||
|
||||
/* === END Belize THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Wisteria
|
||||
=================*/
|
||||
|
||||
@@ -402,7 +402,7 @@ THEME - Wisteria
|
||||
|
||||
/* === END Wisteria THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Midnight
|
||||
=================*/
|
||||
|
||||
@@ -503,7 +503,7 @@ THEME - Midnight
|
||||
|
||||
/* === END Midnight THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Pumpkin
|
||||
=================*/
|
||||
|
||||
@@ -604,7 +604,7 @@ THEME - Pumpkin
|
||||
|
||||
/* === END Pumpkin THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Moderate Pink
|
||||
=================*/
|
||||
|
||||
@@ -705,7 +705,7 @@ THEME - Moderate Pink
|
||||
|
||||
/* === END Moderatepink THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Strong Cyan
|
||||
=================*/
|
||||
|
||||
@@ -806,7 +806,7 @@ THEME - Strong Cyan
|
||||
|
||||
/* === END Strongcyan THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Lime Green
|
||||
=================*/
|
||||
|
||||
@@ -907,7 +907,7 @@ THEME - Lime Green
|
||||
|
||||
/* === END Limegreen THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Dark
|
||||
=================*/
|
||||
|
||||
@@ -1111,7 +1111,7 @@ THEME - Dark
|
||||
|
||||
/* === END Dark THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Relax
|
||||
=================*/
|
||||
|
||||
@@ -1241,7 +1241,7 @@ THEME - Relax
|
||||
|
||||
/* === END Relax THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Corteza
|
||||
=================*/
|
||||
|
||||
@@ -1342,7 +1342,7 @@ THEME - Corteza
|
||||
|
||||
/* === END Corteza THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Clear Blue
|
||||
=================*/
|
||||
|
||||
@@ -1609,7 +1609,7 @@ THEME - Clear Blue
|
||||
|
||||
/* === END Clearblue THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Natural
|
||||
=================*/
|
||||
|
||||
@@ -1719,7 +1719,7 @@ THEME - Natural
|
||||
|
||||
/* === END Natural THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Modern
|
||||
=================*/
|
||||
|
||||
@@ -2006,7 +2006,7 @@ THEME - Modern
|
||||
|
||||
/* === END Modern THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Modern Dark
|
||||
=================*/
|
||||
|
||||
@@ -2496,7 +2496,7 @@ THEME - Modern Dark
|
||||
/* === END ModernDark THEME === */
|
||||
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Exodark
|
||||
=================*/
|
||||
|
||||
@@ -2744,7 +2744,7 @@ THEME - Exodark
|
||||
|
||||
/* === END Exodark THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Clean Dark
|
||||
=================*/
|
||||
|
||||
@@ -3183,7 +3183,7 @@ THEME - Clean Dark
|
||||
.board-color-cleandark.background-box {
|
||||
background-color:#23232B;
|
||||
} /*Fixes contrast issues with background box in theme selection list*/
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Clean Light
|
||||
=================*/
|
||||
/* Please note Clean Light theme elements also contain references to some cleandark theme elements so if unable to find code you're looking for under CleanDark it might be here. This should probably be cleaned up*/
|
||||
@@ -3322,7 +3322,13 @@ THEME - Clean Light
|
||||
|
||||
.board-color-cleanlight .list .list-header .list-header-menu .js-collapse,
|
||||
.board-color-cleandark .list .list-header .list-header-menu .js-collapse {
|
||||
display: none;
|
||||
/* Make collapse button visible in Clean Light / Clean Dark themes.
|
||||
Previously this was hidden which caused the missing Collapse button
|
||||
when using these themes. Use inline-block so it lines up with other
|
||||
header controls. */
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.board-color-cleanlight .list-header-add,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.integration-form {
|
||||
padding: 5px;
|
||||
padding: 0.7vh 0.7vw;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.flex,
|
||||
@@ -12,12 +12,928 @@
|
||||
display: flex;
|
||||
}
|
||||
.option {
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
-webkit-border-radius: 0.4vw;
|
||||
border-radius: 0.4vw;
|
||||
background: #fff;
|
||||
text-decoration: none;
|
||||
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
margin-top: 5px;
|
||||
padding: 5px;
|
||||
-webkit-box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.2);
|
||||
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.2);
|
||||
margin-top: 0.7vh;
|
||||
padding: 0.7vh 0.7vw;
|
||||
}
|
||||
|
||||
/* Zoom and Mobile Mode Controls */
|
||||
.board-header-btns.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5vw;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 0.5vw;
|
||||
box-shadow: 0 0.2vh 0.5vh rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn {
|
||||
padding: 0.5vh 0.8vw !important;
|
||||
border-radius: 0.3vw !important;
|
||||
background: #fff !important;
|
||||
border: 1px solid #000 !important;
|
||||
transition: all 0.2s ease !important;
|
||||
color: #000 !important;
|
||||
height: auto !important;
|
||||
line-height: normal !important;
|
||||
margin: 0 !important;
|
||||
float: none !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn i {
|
||||
color: #000 !important;
|
||||
float: none !important;
|
||||
display: inline !important;
|
||||
line-height: normal !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn:hover {
|
||||
background: #000 !important;
|
||||
border-color: #000 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn:hover i {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn.is-active {
|
||||
background: #0079bf;
|
||||
color: white;
|
||||
border-color: #005a8a;
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn.is-active i {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.zoom-level {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
min-width: 3vw;
|
||||
text-align: center;
|
||||
font-size: clamp(12px, 2vw, 14px);
|
||||
cursor: pointer;
|
||||
padding: 0.3vh 0.5vw;
|
||||
border-radius: 0.3vw;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.zoom-level:hover {
|
||||
background: #f0f0f0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Mobile Mode Styles */
|
||||
.mobile-mode .board-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mobile-mode .board-canvas {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mobile-mode .minicard {
|
||||
font-size: clamp(16px, 4vw, 20px);
|
||||
padding: 1.2vh 1.5vw 0.5vh;
|
||||
min-height: 3vh;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header-name {
|
||||
font-size: clamp(18px, 4.5vw, 24px);
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btn {
|
||||
padding: 1vh 1.5vw;
|
||||
font-size: clamp(14px, 3.5vw, 18px);
|
||||
}
|
||||
|
||||
.mobile-mode .zoom-controls {
|
||||
padding: 1vh 1.5vw;
|
||||
gap: 1vw;
|
||||
}
|
||||
|
||||
.mobile-mode .zoom-controls .board-header-btn {
|
||||
padding: 1vh 1.5vw !important;
|
||||
font-size: clamp(14px, 3.5vw, 18px) !important;
|
||||
background: #fff !important;
|
||||
border: 1px solid #000 !important;
|
||||
color: #000 !important;
|
||||
height: auto !important;
|
||||
line-height: normal !important;
|
||||
margin: 0 !important;
|
||||
float: none !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.mobile-mode .zoom-controls .board-header-btn i {
|
||||
color: #000 !important;
|
||||
float: none !important;
|
||||
display: inline !important;
|
||||
line-height: normal !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .zoom-controls .board-header-btn:hover {
|
||||
background: #000 !important;
|
||||
border-color: #000 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.mobile-mode .zoom-controls .board-header-btn:hover i {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.mobile-mode .zoom-level {
|
||||
font-size: clamp(14px, 3.5vw, 18px);
|
||||
min-width: 4vw;
|
||||
}
|
||||
|
||||
/* Comprehensive Mobile Mode Styles - Works on all screen sizes */
|
||||
.mobile-mode .board-wrapper {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
transform: none !important;
|
||||
transform-origin: initial !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-canvas {
|
||||
height: 100% !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin-bottom: 2rem !important;
|
||||
display: block !important;
|
||||
float: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane-header {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
padding: 1rem !important;
|
||||
margin-bottom: 1rem !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
display: block !important;
|
||||
float: none !important;
|
||||
margin-bottom: 2rem !important;
|
||||
border-left: none !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
clear: both !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
padding: 1rem !important;
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
display: grid !important;
|
||||
grid-template-columns: 30px 1fr auto auto !important;
|
||||
gap: 10px !important;
|
||||
align-items: center !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header .list-header-name {
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
font-weight: bold !important;
|
||||
grid-row: 1 !important;
|
||||
grid-column: 2 !important;
|
||||
align-self: end !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header .cardCount {
|
||||
font-size: clamp(14px, 2vw, 24px) !important;
|
||||
grid-row: 2 !important;
|
||||
grid-column: 2 !important;
|
||||
align-self: start !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header .list-header-menu-icon {
|
||||
position: static !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 3 !important;
|
||||
padding: 14px !important;
|
||||
font-size: clamp(24px, 3vw, 48px) !important;
|
||||
text-align: center !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header .list-header-handle {
|
||||
position: static !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 4 !important;
|
||||
padding: 14px !important;
|
||||
font-size: clamp(28px, 3.5vw, 56px) !important;
|
||||
text-align: center !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-body {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
padding: 1rem !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.mobile-mode .minicard {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
font-size: clamp(16px, 2vw, 24px) !important;
|
||||
padding: 1.2vh 1.5vw 0.5vh !important;
|
||||
min-height: 3vh !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
display: block !important;
|
||||
float: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .minicard .minicard-title {
|
||||
font-size: clamp(16px, 2vw, 24px) !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.mobile-mode .minicard .minicard-members {
|
||||
font-size: clamp(12px, 1.5vw, 18px) !important;
|
||||
}
|
||||
|
||||
.mobile-mode .minicard .minicard-lists {
|
||||
font-size: clamp(12px, 1.5vw, 18px) !important;
|
||||
}
|
||||
|
||||
/* Desktop Mode Styles */
|
||||
.desktop-mode .board-wrapper {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.desktop-mode .swimlane {
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
}
|
||||
|
||||
.desktop-mode .list {
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
display: flex !important;
|
||||
float: left !important;
|
||||
margin-bottom: 0 !important;
|
||||
border-left: 1px solid #ccc !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.desktop-mode .list-header {
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
padding: 2.5vh 1.5vw 0.5vh !important;
|
||||
font-size: clamp(14px, 3vw, 18px) !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.desktop-mode .list-header .list-header-name {
|
||||
font-size: clamp(14px, 3vw, 18px) !important;
|
||||
display: inline !important;
|
||||
grid-row: auto !important;
|
||||
grid-column: auto !important;
|
||||
align-self: auto !important;
|
||||
}
|
||||
|
||||
.desktop-mode .list-header .cardCount {
|
||||
font-size: 12px !important;
|
||||
grid-row: auto !important;
|
||||
grid-column: auto !important;
|
||||
align-self: auto !important;
|
||||
}
|
||||
|
||||
.desktop-mode .list-header .list-header-menu-icon {
|
||||
position: absolute !important;
|
||||
right: 60px !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
grid-row: auto !important;
|
||||
grid-column: auto !important;
|
||||
padding: 14px !important;
|
||||
font-size: 40px !important;
|
||||
}
|
||||
|
||||
.desktop-mode .list-header .list-header-handle {
|
||||
position: absolute !important;
|
||||
right: 10px !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
grid-row: auto !important;
|
||||
grid-column: auto !important;
|
||||
padding: 7px !important;
|
||||
font-size: clamp(16px, 3vw, 20px) !important;
|
||||
}
|
||||
|
||||
.desktop-mode .list-body {
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
padding: 5px 11px !important;
|
||||
}
|
||||
|
||||
.desktop-mode .minicard {
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
font-size: clamp(12px, 2.5vw, 16px) !important;
|
||||
padding: 0.5vh 0.8vw !important;
|
||||
min-height: auto !important;
|
||||
margin-bottom: 9px !important;
|
||||
}
|
||||
|
||||
.desktop-mode .minicard .minicard-title {
|
||||
font-size: clamp(12px, 2.5vw, 16px) !important;
|
||||
}
|
||||
|
||||
.desktop-mode .minicard .minicard-members {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
.desktop-mode .minicard .minicard-lists {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
/* Additional Mobile Mode Styles for Other Elements - Works on all screen sizes */
|
||||
.mobile-mode .swimlane-header .swimlane-title {
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
font-weight: bold !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane-header .swimlane-description {
|
||||
font-size: clamp(14px, 2vw, 24px) !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header {
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
padding: 1rem !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-title {
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
font-weight: bold !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-description {
|
||||
font-size: clamp(14px, 2vw, 24px) !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn {
|
||||
font-size: clamp(14px, 2vw, 24px) !important;
|
||||
padding: 1vh 1.5vw !important;
|
||||
display: inline-block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn i {
|
||||
font-size: clamp(14px, 2vw, 24px) !important;
|
||||
display: inline !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Force mobile mode visibility on all screen sizes */
|
||||
.mobile-mode .list-header .fa-angle-right,
|
||||
.mobile-mode .list-header .fa-arrows {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
position: static !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header .fa-angle-right {
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 3 !important;
|
||||
padding: 14px !important;
|
||||
font-size: clamp(24px, 3vw, 48px) !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header .fa-arrows {
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 4 !important;
|
||||
padding: 14px !important;
|
||||
font-size: clamp(28px, 3.5vw, 56px) !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* Override any media queries that might hide elements in mobile mode */
|
||||
.mobile-mode * {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list,
|
||||
.mobile-mode .swimlane,
|
||||
.mobile-mode .board-wrapper,
|
||||
.mobile-mode .board-canvas {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
/* Force mobile mode list styling on all screen sizes - override desktop CSS */
|
||||
.mobile-mode .board-canvas {
|
||||
display: block !important;
|
||||
flex-direction: column !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane .swimlane-header {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 0 1rem 0 !important;
|
||||
padding: 1rem !important;
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
font-weight: bold !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane .lists {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
flex-direction: column !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-top: none !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
position: static !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
bottom: auto !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list:first-child {
|
||||
margin-left: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list:last-child {
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list.ui-sortable-helper {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
height: auto !important;
|
||||
min-height: 60px !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-top: none !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
flex: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list.placeholder {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
height: auto !important;
|
||||
min-height: 60px !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-top: none !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
flex: none !important;
|
||||
}
|
||||
|
||||
/* Override any existing responsive CSS that might interfere with mobile mode */
|
||||
.mobile-mode .board-canvas .swimlane .lists {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
flex-direction: column !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-canvas .swimlane .lists .list {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-top: none !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
position: static !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
bottom: auto !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Force mobile mode to override any media query styles */
|
||||
@media screen and (min-width: 801px) {
|
||||
.mobile-mode .board-canvas {
|
||||
display: block !important;
|
||||
flex-direction: column !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane .lists {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
flex-direction: column !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-top: none !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
position: static !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
bottom: auto !important;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide desktop-only elements in mobile mode (like mobile media queries do) */
|
||||
.mobile-mode .board-header-btn i.fa + span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btn span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btn .fa + span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btn .fa + .board-header-btn-text {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btn .fa + .board-header-btn-label {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Show only icons in mobile mode */
|
||||
.mobile-mode .board-header-btn {
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
padding: 8px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btn i {
|
||||
display: inline-block !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Hide desktop-specific elements that shouldn't show in mobile mode */
|
||||
.mobile-mode .desktop-only,
|
||||
.mobile-mode .board-header .desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn.desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide desktop-specific board header buttons in mobile mode */
|
||||
.mobile-mode .board-header-btns.left {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btns.center {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Show only the right section buttons in mobile mode, but hide text labels */
|
||||
.mobile-mode .board-header-btns.right {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btns.right .board-header-btn span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btns.right .board-header-btn .fa + span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btns.right .board-header-btn .fa + .board-header-btn-text {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btns.right .board-header-btn .fa + .board-header-btn-label {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide specific desktop-only buttons that shouldn't show in mobile mode */
|
||||
.mobile-mode .board-header-btn.js-star-board span,
|
||||
.mobile-mode .board-header-btn.js-change-visibility span,
|
||||
.mobile-mode .board-header-btn.js-watch-board span,
|
||||
.mobile-mode .board-header-btn.js-sort-cards span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Show only icons for mobile mode buttons */
|
||||
.mobile-mode .board-header-btns.right .board-header-btn {
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
padding: 8px !important;
|
||||
text-align: center !important;
|
||||
margin: 0 2px !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btns.right .board-header-btn i {
|
||||
display: inline-block !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Ensure mobile mode looks like small screen mobile view */
|
||||
.mobile-mode .board-header {
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btns {
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn {
|
||||
height: 32px !important;
|
||||
line-height: 32px !important;
|
||||
font-size: 15px !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn i.fa {
|
||||
line-height: 32px !important;
|
||||
}
|
||||
|
||||
/* Copy mobile media query styles to mobile mode for consistent appearance */
|
||||
.mobile-mode .board-header {
|
||||
height: 40px !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btns {
|
||||
margin-top: 0px !important;
|
||||
height: 40px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn {
|
||||
height: 32px !important;
|
||||
line-height: 32px !important;
|
||||
font-size: 15px !important;
|
||||
margin: 0 2px !important;
|
||||
padding: 4px 8px !important;
|
||||
border-radius: 4px !important;
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2) !important;
|
||||
color: #fff !important;
|
||||
text-decoration: none !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
min-width: 32px !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
border-color: rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn i.fa {
|
||||
line-height: 32px !important;
|
||||
font-size: 15px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn i.fa + span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide the board title in mobile mode to match mobile view */
|
||||
.mobile-mode .header-board-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Ensure the board header takes full width in mobile mode */
|
||||
.mobile-mode .board-header {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: space-between !important;
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
|
||||
/* Additional Desktop Mode Styles for Other Elements */
|
||||
.desktop-mode .swimlane-header .swimlane-title {
|
||||
font-size: clamp(14px, 3vw, 18px) !important;
|
||||
}
|
||||
|
||||
.desktop-mode .swimlane-header .swimlane-description {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.desktop-mode .board-header {
|
||||
font-size: clamp(14px, 3vw, 18px) !important;
|
||||
padding: 2.5vh 1.5vw 0.5vh !important;
|
||||
}
|
||||
|
||||
.desktop-mode .board-header .board-header-title {
|
||||
font-size: clamp(14px, 3vw, 18px) !important;
|
||||
}
|
||||
|
||||
.desktop-mode .board-header .board-header-description {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.desktop-mode .board-header .board-header-btn {
|
||||
font-size: clamp(12px, 2.5vw, 16px) !important;
|
||||
padding: 0.5vh 0.8vw !important;
|
||||
}
|
||||
|
||||
.desktop-mode .board-header .board-header-btn i {
|
||||
font-size: clamp(12px, 2.5vw, 16px) !important;
|
||||
}
|
||||
@@ -17,7 +17,7 @@ template(name="boardHeaderBar")
|
||||
i.fa.fa-pencil-square-o
|
||||
|
||||
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'}}")
|
||||
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}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
if showStarCounter
|
||||
span
|
||||
@@ -51,6 +51,8 @@ template(name="boardHeaderBar")
|
||||
i.fa.fa-sign-in
|
||||
span {{_ 'log-in'}}
|
||||
|
||||
.board-header-btns.center
|
||||
|
||||
.board-header-btns.right
|
||||
if currentBoard
|
||||
if isMiniScreen
|
||||
@@ -62,15 +64,11 @@ template(name="boardHeaderBar")
|
||||
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'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
if showStarCounter
|
||||
span
|
||||
= currentBoard.stars
|
||||
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
span {{_ currentBoard.permission}}
|
||||
|
||||
a.board-header-btn.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
@@ -80,10 +78,8 @@ template(name="boardHeaderBar")
|
||||
i.fa.fa-bell
|
||||
if $eq watchLevel "muted"
|
||||
i.fa.fa-bell-slash
|
||||
span {{_ watchLevel}}
|
||||
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
||||
i.fa.fa-sort
|
||||
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
|
||||
if isSortActive
|
||||
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
|
||||
i.fa.fa-times-thin
|
||||
@@ -92,13 +88,11 @@ template(name="boardHeaderBar")
|
||||
a.board-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
span {{_ 'log-in'}}
|
||||
|
||||
if isSandstorm
|
||||
if currentUser
|
||||
a.board-header-btn.js-open-archived-board
|
||||
i.fa.fa-archive
|
||||
span {{_ 'archives'}}
|
||||
|
||||
//if showSort
|
||||
// a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
|
||||
@@ -109,14 +103,12 @@ template(name="boardHeaderBar")
|
||||
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
|
||||
class="{{#if Filter.isActive}}emphasis{{/if}}")
|
||||
i.fa.fa-filter
|
||||
span {{#if Filter.isActive}}{{_ 'filter-on'}}{{else}}{{_ 'filter'}}{{/if}}
|
||||
if Filter.isActive
|
||||
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
|
||||
i.fa.fa-times-thin
|
||||
|
||||
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
|
||||
i.fa.fa-search
|
||||
span {{_ 'search'}}
|
||||
|
||||
unless currentBoard.isTemplatesBoard
|
||||
a.board-header-btn.js-toggle-board-view(
|
||||
@@ -128,14 +120,12 @@ template(name="boardHeaderBar")
|
||||
i.fa.fa-trello
|
||||
if $eq boardView 'board-view-cal'
|
||||
i.fa.fa-calendar
|
||||
span {{#if boardView}}{{_ boardView}}{{else}}{{_ 'board-view-swimlanes'}}{{/if}}
|
||||
|
||||
if canModifyBoard
|
||||
a.board-header-btn.js-multiselection-activate(
|
||||
title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
|
||||
class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
|
||||
i.fa.fa-check-square-o
|
||||
span {{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}
|
||||
if MultiSelection.isActive
|
||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||
i.fa.fa-times-thin
|
||||
|
||||
@@ -33,6 +33,8 @@ BlazeComponent.extendComponent({
|
||||
return currentBoard && currentBoard.getWatchLevel(Meteor.userId());
|
||||
},
|
||||
|
||||
|
||||
|
||||
isStarred() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
@@ -80,10 +82,29 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
'click .js-toggle-board-view': Popup.open('boardChangeView'),
|
||||
'click .js-toggle-sidebar'() {
|
||||
Sidebar.toggle();
|
||||
console.log('Hamburger menu clicked');
|
||||
// Use the same approach as keyboard shortcuts
|
||||
if (typeof Sidebar !== 'undefined' && Sidebar && typeof Sidebar.toggle === 'function') {
|
||||
console.log('Using Sidebar.toggle()');
|
||||
Sidebar.toggle();
|
||||
} else {
|
||||
console.warn('Sidebar not available, trying alternative approach');
|
||||
// Try to trigger the sidebar through the global Blaze helper
|
||||
if (typeof Blaze !== 'undefined' && Blaze._globalHelpers && Blaze._globalHelpers.Sidebar) {
|
||||
const sidebar = Blaze._globalHelpers.Sidebar();
|
||||
if (sidebar && typeof sidebar.toggle === 'function') {
|
||||
console.log('Using Blaze helper Sidebar.toggle()');
|
||||
sidebar.toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-open-filter-view'() {
|
||||
Sidebar.setView('filter');
|
||||
if (Sidebar) {
|
||||
Sidebar.setView('filter');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
},
|
||||
'click .js-sort-cards': Popup.open('cardsSort'),
|
||||
/*
|
||||
@@ -100,14 +121,22 @@ BlazeComponent.extendComponent({
|
||||
*/
|
||||
'click .js-filter-reset'(event) {
|
||||
event.stopPropagation();
|
||||
Sidebar.setView();
|
||||
if (Sidebar) {
|
||||
Sidebar.setView();
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
Filter.reset();
|
||||
},
|
||||
'click .js-sort-reset'() {
|
||||
Session.set('sortBy', '');
|
||||
},
|
||||
'click .js-open-search-view'() {
|
||||
Sidebar.setView('search');
|
||||
if (Sidebar) {
|
||||
Sidebar.setView('search');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
},
|
||||
'click .js-multiselection-activate'() {
|
||||
const currentCard = Utils.getCurrentCardId();
|
||||
@@ -201,6 +230,7 @@ const CreateBoard = BlazeComponent.extendComponent({
|
||||
title: title,
|
||||
permission: 'private',
|
||||
type: 'template-container',
|
||||
migrationVersion: 1, // Latest version - no migration needed
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -244,6 +274,7 @@ const CreateBoard = BlazeComponent.extendComponent({
|
||||
Boards.insert({
|
||||
title,
|
||||
permission: visibility,
|
||||
migrationVersion: 1, // Latest version - no migration needed
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,86 @@
|
||||
@import url("../../../css/reset.css") print, screen;
|
||||
|
||||
/* Board List Header with Zoom Controls */
|
||||
.board-list-header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 1vh 0 2vh 0;
|
||||
padding: 1vh 0;
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5vw;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 0.5vw;
|
||||
box-shadow: 0 0.2vh 0.5vh rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn {
|
||||
padding: 0.5vh 0.8vw !important;
|
||||
border-radius: 0.3vw !important;
|
||||
background: #fff !important;
|
||||
border: 1px solid #000 !important;
|
||||
transition: all 0.2s ease !important;
|
||||
text-decoration: none !important;
|
||||
color: #000 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 0.3vw !important;
|
||||
height: auto !important;
|
||||
line-height: normal !important;
|
||||
margin: 0 !important;
|
||||
float: none !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn i {
|
||||
color: #000 !important;
|
||||
float: none !important;
|
||||
display: inline !important;
|
||||
line-height: normal !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn:hover {
|
||||
background: #000 !important;
|
||||
border-color: #000 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn:hover i {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn.is-active {
|
||||
background: #0079bf;
|
||||
color: white;
|
||||
border-color: #005a8a;
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn.is-active i {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.zoom-level {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
min-width: 3vw;
|
||||
text-align: center;
|
||||
font-size: clamp(12px, 2vw, 14px);
|
||||
cursor: pointer;
|
||||
padding: 0.3vh 0.5vw;
|
||||
border-radius: 0.3vw;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.zoom-level:hover {
|
||||
background: #f0f0f0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.board-list {
|
||||
margin: 0 8px;
|
||||
}
|
||||
@@ -192,17 +273,131 @@
|
||||
font-size: 25px;
|
||||
color: #fff;
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
|
||||
.board-list.mobile-view {
|
||||
height: calc(100vh - 120px);
|
||||
overflow-y: scroll !important;
|
||||
overflow-x: hidden;
|
||||
padding: 0 1rem;
|
||||
margin: 0;
|
||||
scrollbar-width: auto !important;
|
||||
scrollbar-color: #888 #f1f1f1;
|
||||
}
|
||||
.board-list.mobile-view li {
|
||||
width: 100%;
|
||||
float: none;
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
padding-right: 50px; /* Space for drag handle */
|
||||
}
|
||||
.board-list.mobile-view .board-list-item {
|
||||
overflow: visible;
|
||||
height: 8rem;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding-right: 50px; /* Ensure content doesn't overlap with drag handle */
|
||||
}
|
||||
|
||||
.board-list.mobile-view .board-list-item .details {
|
||||
padding-right: 50px; /* Extra space for drag handle */
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.board-list.mobile-view .board-list-item-sub-name {
|
||||
position: relative;
|
||||
top: -100px;
|
||||
left: -100px;
|
||||
}
|
||||
.board-list.mobile-view .board-handle {
|
||||
position: absolute;
|
||||
padding: 7px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 10px;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.board-list.mobile-view .board-handle:hover {
|
||||
background: rgba(255, 255, 0, 0.8) !important; /* Yellow hover */
|
||||
}
|
||||
|
||||
/* Force scrollbar to be visible on mobile view */
|
||||
.board-list.mobile-view::-webkit-scrollbar {
|
||||
width: 12px !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.board-list.mobile-view::-webkit-scrollbar-track {
|
||||
background: #f1f1f1 !important;
|
||||
border-radius: 6px !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.board-list.mobile-view::-webkit-scrollbar-thumb {
|
||||
background: #888 !important;
|
||||
border-radius: 6px !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.board-list.mobile-view::-webkit-scrollbar-thumb:hover {
|
||||
background: #555 !important;
|
||||
}
|
||||
|
||||
/* Force mobile view to have scrollable content */
|
||||
.board-list.mobile-view {
|
||||
min-height: 100vh; /* Force content to be tall enough to scroll */
|
||||
}
|
||||
|
||||
.board-list.mobile-view::after {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px),
|
||||
screen and (max-device-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: landscape) {
|
||||
.board-list {
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
.board-list li {
|
||||
width: 50%;
|
||||
width: 100%;
|
||||
float: none;
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
padding-right: 50px; /* Space for drag handle */
|
||||
}
|
||||
.board-list .board-list-item {
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
height: 8rem;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding-right: 50px; /* Ensure content doesn't overlap with drag handle */
|
||||
}
|
||||
|
||||
.board-list .board-list-item .details {
|
||||
padding-right: 50px; /* Extra space for drag handle */
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.board-list .board-list-item-sub-name {
|
||||
position: relative;
|
||||
@@ -216,19 +411,296 @@
|
||||
transform: translateY(-50%);
|
||||
right: 10px;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.board-list .board-handle:hover {
|
||||
background: rgba(255, 255, 0, 0.8) !important; /* Yellow hover */
|
||||
}
|
||||
}
|
||||
/* Very small screens - ensure one board per row */
|
||||
@media screen and (max-width: 360px) {
|
||||
li {
|
||||
width: 100%;
|
||||
.board-list li {
|
||||
width: 100% !important;
|
||||
float: none !important;
|
||||
display: block !important;
|
||||
}
|
||||
.board-handle {
|
||||
.board-list .board-handle {
|
||||
position: absolute;
|
||||
padding: 7px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 10px;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile - make all text and icons 2x bigger above #content on All Boards page */
|
||||
@media screen and (max-width: 800px),
|
||||
screen and (max-device-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: landscape) {
|
||||
.wrapper {
|
||||
font-size: 2em !important; /* 2x bigger base font size for All Boards page */
|
||||
}
|
||||
|
||||
.wrapper * {
|
||||
font-size: inherit !important; /* Inherit the 2x scaling */
|
||||
}
|
||||
|
||||
.wrapper .fa, .wrapper .icon {
|
||||
font-size: 2em !important; /* 2x bigger icons */
|
||||
}
|
||||
|
||||
.board-list-header {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.board-list-header h1 {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.AllBoardTeamsOrgs {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.AllBoardTeamsOrgs select {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.AllBoardTeamsOrgs input {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.AllBoardTeamsOrgs .fa {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.board-list {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.board-list .board-list-item {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.board-list .board-list-item-name {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.board-list .board-list-item-desc {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.board-list .minicard-members {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.board-list .minicard-lists {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.board-list .fa {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.board-list .board-handle {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback for iPhone devices using JavaScript detection - All Boards page */
|
||||
.iphone-device .wrapper {
|
||||
font-size: 2em !important; /* 2x bigger base font size for All Boards page */
|
||||
}
|
||||
|
||||
.iphone-device .wrapper * {
|
||||
font-size: inherit !important; /* Inherit the 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .wrapper .fa, .iphone-device .wrapper .icon {
|
||||
font-size: 2em !important; /* 2x bigger icons */
|
||||
}
|
||||
|
||||
.iphone-device .board-list-header {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .board-list-header h1 {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .AllBoardTeamsOrgs {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .AllBoardTeamsOrgs select {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .AllBoardTeamsOrgs input {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .AllBoardTeamsOrgs .fa {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .board-list {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .board-list .board-list-item {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .board-list .board-list-item-name {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .board-list .board-list-item-desc {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .board-list .minicard-members {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .board-list .minicard-lists {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .board-list .fa {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .board-list .board-handle {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
/* iPhone 12 Mini and very small screens - make everything much larger */
|
||||
@media screen and (max-width: 400px) and (max-height: 900px) {
|
||||
.board-list {
|
||||
height: calc(100vh - 120px) !important;
|
||||
overflow-y: scroll !important;
|
||||
overflow-x: hidden !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.board-list li {
|
||||
width: 100% !important;
|
||||
float: none !important;
|
||||
display: block !important;
|
||||
margin-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
.board-list .board-list-item {
|
||||
height: 12rem !important; /* Much taller */
|
||||
width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 1rem !important; /* More padding */
|
||||
font-size: 18px !important; /* Much larger text */
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
|
||||
.board-list .board-list-item .board-list-item-name {
|
||||
font-size: 20px !important; /* Larger board names */
|
||||
font-weight: bold !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.board-list .board-list-item .board-list-item-desc {
|
||||
font-size: 16px !important; /* Larger descriptions */
|
||||
line-height: 1.3 !important;
|
||||
}
|
||||
|
||||
.board-list .board-list-item .minicard-members {
|
||||
font-size: 14px !important; /* Larger member avatars */
|
||||
}
|
||||
|
||||
.board-list .board-list-item .minicard-lists {
|
||||
font-size: 14px !important; /* Larger list counters */
|
||||
}
|
||||
|
||||
.board-list .board-handle {
|
||||
position: absolute;
|
||||
padding: 10px !important;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 15px !important;
|
||||
font-size: 28px !important; /* Much larger drag handle */
|
||||
color: #fff;
|
||||
background: rgba(0,0,0,0.4) !important;
|
||||
border-radius: 50%;
|
||||
width: 50px !important; /* Larger handle */
|
||||
height: 50px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.board-list .board-handle:hover {
|
||||
background: rgba(255, 255, 0, 0.8) !important; /* Yellow hover */
|
||||
}
|
||||
|
||||
/* Force scrollbar to be visible and larger */
|
||||
.board-list::-webkit-scrollbar {
|
||||
width: 16px !important; /* Much wider scrollbar */
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.board-list::-webkit-scrollbar-track {
|
||||
background: #f1f1f1 !important;
|
||||
border-radius: 8px !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.board-list::-webkit-scrollbar-thumb {
|
||||
background: #666 !important; /* Darker for better visibility */
|
||||
border-radius: 8px !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
min-height: 50px !important; /* Minimum thumb size */
|
||||
}
|
||||
|
||||
.board-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #333 !important;
|
||||
}
|
||||
|
||||
/* Ensure scrollbar is always visible */
|
||||
.board-list {
|
||||
scrollbar-gutter: stable;
|
||||
scrollbar-width: auto !important;
|
||||
min-height: 100vh !important;
|
||||
}
|
||||
|
||||
.board-list::after {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 200px !important; /* More space to ensure scrolling */
|
||||
}
|
||||
}
|
||||
.AllBoardTeamsOrgs {
|
||||
@@ -279,3 +751,78 @@
|
||||
padding-right: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Fix multiple scrollbars issue on mobile */
|
||||
@media screen and (max-width: 800px),
|
||||
screen and (max-device-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: landscape) {
|
||||
.wrapper {
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.board-list {
|
||||
overflow-y: scroll !important;
|
||||
overflow-x: hidden !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #888 #f1f1f1;
|
||||
height: calc(100vh - 120px); /* Ensure there's content to scroll */
|
||||
}
|
||||
|
||||
/* Force scrollbar to always be visible */
|
||||
.board-list::-webkit-scrollbar {
|
||||
width: 12px !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.board-list::-webkit-scrollbar-track {
|
||||
background: #f1f1f1 !important;
|
||||
border-radius: 6px !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.board-list::-webkit-scrollbar-thumb {
|
||||
background: #888 !important;
|
||||
border-radius: 6px !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.board-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #555 !important;
|
||||
}
|
||||
|
||||
/* Ensure scrollbar is always visible */
|
||||
.board-list::-webkit-scrollbar-corner {
|
||||
background: #f1f1f1 !important;
|
||||
}
|
||||
|
||||
/* Force scrollbar to be visible */
|
||||
.board-list {
|
||||
scrollbar-gutter: stable;
|
||||
scrollbar-width: auto !important;
|
||||
min-height: 100vh; /* Force content to be tall enough to scroll */
|
||||
}
|
||||
|
||||
/* Ensure there's always content to scroll */
|
||||
.board-list::after {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
/* Ensure only one scrollbar is visible */
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#content {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
template(name="boardList")
|
||||
.wrapper
|
||||
.board-list-header
|
||||
|
||||
ul.AllBoardTeamsOrgs
|
||||
li.AllBoardTeams
|
||||
if userHasTeams
|
||||
@@ -29,7 +31,7 @@ template(name="boardList")
|
||||
input#filterBtn(type="button" value="{{_ 'filter'}}")
|
||||
input#resetBtn(type="button" value="{{_ 'filter-clear'}}")
|
||||
|
||||
ul.board-list.clearfix.js-boards
|
||||
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
li.js-add-board
|
||||
a.board-list-item.label(title="{{_ 'add-board'}}")
|
||||
| {{_ 'add-board'}}
|
||||
|
||||
@@ -22,6 +22,9 @@ Template.boardListHeaderBar.events({
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardList.events({
|
||||
});
|
||||
|
||||
Template.boardListHeaderBar.helpers({
|
||||
title() {
|
||||
//if (FlowRouter.getRouteName() === 'template-container') {
|
||||
|
||||
@@ -106,6 +106,142 @@
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 4em;
|
||||
}
|
||||
|
||||
/* Upload progress indicators for drag-and-drop uploads */
|
||||
.minicard-upload-progress,
|
||||
.card-details-upload-progress {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.upload-progress-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.upload-progress-header i {
|
||||
margin-right: 8px;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.upload-progress-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
background: white;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.upload-progress-item.upload-error {
|
||||
border-color: #dc3545;
|
||||
background: #f8d7da;
|
||||
}
|
||||
|
||||
.upload-progress-filename {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
color: #495057;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.upload-progress-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: #e9ecef;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.upload-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #007bff, #0056b3);
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.upload-progress-item.upload-error .upload-progress-fill {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.upload-progress-error,
|
||||
.upload-progress-success {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-progress-error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.upload-progress-success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.upload-progress-error i,
|
||||
.upload-progress-success i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* Minicard specific styles */
|
||||
.minicard-upload-progress {
|
||||
margin: 4px 0;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.minicard-upload-progress .upload-progress-item {
|
||||
padding: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.minicard-upload-progress .upload-progress-filename {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Card details specific styles */
|
||||
.card-details-upload-progress {
|
||||
margin: 12px 0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card-details-upload-progress .upload-progress-header {
|
||||
font-size: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-details-upload-progress .upload-progress-item {
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-details-upload-progress .upload-progress-filename {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Drag over state for minicards */
|
||||
.minicard.is-dragging-over {
|
||||
border: 2px dashed #007bff !important;
|
||||
background: rgba(0, 123, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Drag over state for card details */
|
||||
.js-card-details.is-dragging-over {
|
||||
border: 2px dashed #007bff !important;
|
||||
background: rgba(0, 123, 255, 0.05) !important;
|
||||
}
|
||||
top: 0;
|
||||
right: 8px;
|
||||
position: absolute;
|
||||
@@ -200,3 +336,36 @@
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Attachment migration styles */
|
||||
.attachment-item.migrating {
|
||||
position: relative;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.attachment-migration-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.migration-spinner {
|
||||
font-size: 24px;
|
||||
color: #007cba;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.migration-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -57,7 +57,7 @@ template(name="attachmentGallery")
|
||||
|
||||
each attachments
|
||||
|
||||
.attachment-item
|
||||
.attachment-item(class="{{#if isAttachmentMigrating _id}}migrating{{/if}}")
|
||||
.attachment-thumbnail-container.open-preview(data-attachment-id="{{_id}}" data-card-id="{{ meta.cardId }}")
|
||||
if link
|
||||
if(isImage)
|
||||
@@ -97,6 +97,12 @@ template(name="attachmentGallery")
|
||||
i.fa.fa-trash.icon(title="{{_ 'delete'}}")
|
||||
a.fa.fa-navicon.icon.js-open-attachment-menu(data-attachment-link="{{link}}" title="{{_ 'attachmentActionsPopup-title'}}")
|
||||
|
||||
// Migration spinner overlay
|
||||
if isAttachmentMigrating _id
|
||||
.attachment-migration-overlay
|
||||
.migration-spinner
|
||||
i.fa.fa-cog.fa-spin
|
||||
.migration-text {{_ 'migrating-attachment'}}
|
||||
|
||||
template(name="attachmentActionsPopup")
|
||||
ul.pop-over-list
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { ObjectID } from 'bson';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
|
||||
import uploadProgressManager from '../../lib/uploadProgressManager';
|
||||
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
|
||||
|
||||
const filesize = require('filesize');
|
||||
const prettyMilliseconds = require('pretty-ms');
|
||||
@@ -268,7 +271,7 @@ Template.attachmentGallery.helpers({
|
||||
return ret;
|
||||
},
|
||||
sanitize(value) {
|
||||
return DOMPurify.sanitize(value);
|
||||
return sanitizeHTML(value);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -296,44 +299,13 @@ Template.cardAttachmentsPopup.events({
|
||||
const files = event.currentTarget.files;
|
||||
if (files) {
|
||||
let uploads = [];
|
||||
for (const file of files) {
|
||||
const fileId = new ObjectID().toString();
|
||||
let fileName = DOMPurify.sanitize(file.name);
|
||||
const uploaders = handleFileUpload(card, files);
|
||||
|
||||
// If sanitized filename is not same as original filename,
|
||||
// it could be XSS that is already fixed with sanitize,
|
||||
// or just normal mistake, so it is not a problem.
|
||||
// That is why here is no warning.
|
||||
if (fileName !== file.name) {
|
||||
// If filename is empty, only in that case add some filename
|
||||
if (fileName.length === 0) {
|
||||
fileName = 'Empty-filename-after-sanitize.txt';
|
||||
}
|
||||
}
|
||||
|
||||
const config = {
|
||||
file: file,
|
||||
fileId: fileId,
|
||||
fileName: fileName,
|
||||
meta: Utils.getCommonAttachmentMetaFrom(card),
|
||||
chunkSize: 'dynamic',
|
||||
};
|
||||
config.meta.fileId = fileId;
|
||||
const uploader = Attachments.insert(
|
||||
config,
|
||||
false,
|
||||
);
|
||||
uploaders.forEach(uploader => {
|
||||
uploader.on('start', function() {
|
||||
uploads.push(this);
|
||||
templateInstance.uploads.set(uploads);
|
||||
});
|
||||
uploader.on('uploaded', (error, fileRef) => {
|
||||
if (!error) {
|
||||
if (fileRef.isImage) {
|
||||
card.setCover(fileRef._id);
|
||||
}
|
||||
}
|
||||
});
|
||||
uploader.on('end', (error, fileRef) => {
|
||||
uploads = uploads.filter(_upload => _upload.config.fileId != fileRef._id);
|
||||
templateInstance.uploads.set(uploads);
|
||||
@@ -341,8 +313,7 @@ Template.cardAttachmentsPopup.events({
|
||||
Popup.back();
|
||||
}
|
||||
});
|
||||
uploader.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
'click .js-computer-upload'(event, templateInstance) {
|
||||
@@ -356,6 +327,105 @@ const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
|
||||
const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
|
||||
let pastedResults = null;
|
||||
|
||||
// Shared upload logic for drag-and-drop functionality
|
||||
export function handleFileUpload(card, files) {
|
||||
if (!files || files.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if board allows attachments
|
||||
const board = card.board();
|
||||
if (!board || !board.allowsAttachments) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('Attachments not allowed on this board');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if user can modify the card
|
||||
if (!card.canModifyCard()) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('User does not have permission to modify this card');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
const uploads = [];
|
||||
|
||||
for (const file of files) {
|
||||
// Basic file validation
|
||||
if (!file || !file.name) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('Invalid file object');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileId = new ObjectID().toString();
|
||||
let fileName = sanitizeText(file.name);
|
||||
|
||||
// If sanitized filename is not same as original filename,
|
||||
// it could be XSS that is already fixed with sanitize,
|
||||
// or just normal mistake, so it is not a problem.
|
||||
// That is why here is no warning.
|
||||
if (fileName !== file.name) {
|
||||
// If filename is empty, only in that case add some filename
|
||||
if (fileName.length === 0) {
|
||||
fileName = 'Empty-filename-after-sanitize.txt';
|
||||
}
|
||||
}
|
||||
|
||||
const config = {
|
||||
file: file,
|
||||
fileId: fileId,
|
||||
fileName: fileName,
|
||||
meta: Utils.getCommonAttachmentMetaFrom(card),
|
||||
chunkSize: 'dynamic',
|
||||
};
|
||||
config.meta.fileId = fileId;
|
||||
|
||||
try {
|
||||
const uploader = Attachments.insert(
|
||||
config,
|
||||
false,
|
||||
);
|
||||
|
||||
// Add to progress manager for tracking
|
||||
const uploadId = uploadProgressManager.addUpload(card._id, uploader, file);
|
||||
|
||||
uploader.on('uploaded', (error, fileRef) => {
|
||||
if (!error) {
|
||||
if (fileRef.isImage) {
|
||||
card.setCover(fileRef._id);
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Set cover image for card ${card._id}: ${fileRef.name}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.error('Upload error:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
uploader.on('error', (error) => {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.error('Upload error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
uploads.push(uploader);
|
||||
uploader.start();
|
||||
} catch (error) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.error('Failed to create uploader:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uploads;
|
||||
}
|
||||
|
||||
Template.previewClipboardImagePopup.onRendered(() => {
|
||||
// we can paste image from clipboard
|
||||
const handle = results => {
|
||||
@@ -498,7 +568,7 @@ BlazeComponent.extendComponent({
|
||||
const name = this.$('.js-edit-attachment-name')[0]
|
||||
.value
|
||||
.trim() + this.data().extensionWithDot;
|
||||
if (name === DOMPurify.sanitize(name)) {
|
||||
if (name === sanitizeText(name)) {
|
||||
Meteor.call('renameAttachment', this.data()._id, name);
|
||||
}
|
||||
Popup.back();
|
||||
@@ -507,3 +577,20 @@ BlazeComponent.extendComponent({
|
||||
]
|
||||
}
|
||||
}).register('attachmentRenamePopup');
|
||||
|
||||
// Template helpers for attachment migration status
|
||||
Template.registerHelper('attachmentMigrationStatus', function(attachmentId) {
|
||||
return attachmentMigrationManager.getAttachmentMigrationStatus(attachmentId);
|
||||
});
|
||||
|
||||
Template.registerHelper('isAttachmentMigrating', function(attachmentId) {
|
||||
return attachmentMigrationManager.isAttachmentBeingMigrated(attachmentId);
|
||||
});
|
||||
|
||||
Template.registerHelper('attachmentMigrationProgress', function() {
|
||||
return attachmentMigrationManager.attachmentMigrationProgress.get();
|
||||
});
|
||||
|
||||
Template.registerHelper('attachmentMigrationStatusText', function() {
|
||||
return attachmentMigrationManager.attachmentMigrationStatus.get();
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
.assignee {
|
||||
border-radius: 3px;
|
||||
border-radius: 0.4vw;
|
||||
display: block;
|
||||
position: relative;
|
||||
float: left;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
margin: .3vh;
|
||||
height: 4vw;
|
||||
width: 4vw;
|
||||
margin: 0.4vh;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
z-index: 1;
|
||||
@@ -34,11 +34,11 @@
|
||||
background-color: #b3b3b3;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 50%;
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
height: 1vw;
|
||||
width: 1vw;
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
right: -0.1vw;
|
||||
bottom: -0.1vw;
|
||||
border: 1px solid #fff;
|
||||
z-index: 15;
|
||||
}
|
||||
@@ -70,23 +70,23 @@
|
||||
}
|
||||
.copied-tooltip {
|
||||
display: none;
|
||||
padding: 0px 10px;
|
||||
padding: 0 1.3vw;
|
||||
background-color: rgba(0,0,0,0.875);
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
border-radius: 0.7vw;
|
||||
}
|
||||
.card-details {
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: 600px;
|
||||
flex-basis: min(600px, 80vw);
|
||||
will-change: flex-basis;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
background: #f7f7f7;
|
||||
border-radius: bottom 3px;
|
||||
border-radius: bottom 0.4vw;
|
||||
z-index: 30;
|
||||
animation: flexGrowIn 0.1s;
|
||||
box-shadow: 0 0 7px 0 #b3b3b3;
|
||||
box-shadow: 0 0 0.9vh 0 #b3b3b3;
|
||||
transition: flex-basis 0.1s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -95,7 +95,7 @@
|
||||
}
|
||||
.card-details .card-details-canvas {
|
||||
width: auto;
|
||||
padding: 0 20px;
|
||||
padding: 0 2.5vw;
|
||||
}
|
||||
.card-details .card-details-header {
|
||||
margin: 0 -20px 5px;
|
||||
@@ -596,3 +596,11 @@ input[type="submit"].attachment-add-link-submit {
|
||||
overflow: hidden;
|
||||
background-color: #cecece;
|
||||
}
|
||||
|
||||
/* Drag and drop file upload visual feedback */
|
||||
.js-card-details.is-dragging-over {
|
||||
border: 2px dashed #0079bf;
|
||||
background-color: #e3f2fd !important;
|
||||
transform: scale(1.01);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
@@ -65,6 +65,26 @@ template(name="cardDetails")
|
||||
else
|
||||
p.warning {{_ 'card-archived'}}
|
||||
|
||||
// Upload progress indicator for drag-and-drop uploads
|
||||
if hasActiveUploads
|
||||
.card-details-upload-progress
|
||||
.upload-progress-header
|
||||
i.fa.fa-upload
|
||||
span {{_ 'uploading-files'}} ({{uploadCount}})
|
||||
each uploads
|
||||
.upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
|
||||
.upload-progress-filename {{file.name}}
|
||||
.upload-progress-bar
|
||||
.upload-progress-fill(style="width: {{progress}}%")
|
||||
if $eq status 'error'
|
||||
.upload-progress-error
|
||||
i.fa.fa-exclamation-triangle
|
||||
span {{_ 'upload-failed'}}
|
||||
else if $eq status 'completed'
|
||||
.upload-progress-success
|
||||
i.fa.fa-check
|
||||
span {{_ 'upload-completed'}}
|
||||
|
||||
.card-details-left
|
||||
|
||||
.card-details-items
|
||||
@@ -675,6 +695,14 @@ template(name="cardDetailsActionsPopup")
|
||||
a.js-set-card-color
|
||||
i.fa.fa-paint-brush
|
||||
| {{_ 'setCardColorPopup-title'}}
|
||||
li
|
||||
a.js-toggle-show-list-on-minicard
|
||||
if showListOnMinicard
|
||||
i.fa.fa-eye
|
||||
| {{_ 'hide-list-on-minicard'}}
|
||||
else
|
||||
i.fa.fa-eye-slash
|
||||
| {{_ 'show-list-on-minicard'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
|
||||
@@ -12,6 +12,8 @@ import CardComments from '/models/cardComments';
|
||||
import { ALLOWED_COLORS } from '/config/const';
|
||||
import { UserAvatar } from '../users/userAvatar';
|
||||
import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList';
|
||||
import { handleFileUpload } from './attachments';
|
||||
import uploadProgressManager from '../../lib/uploadProgressManager';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
const { calculateIndexData } = Utils;
|
||||
@@ -481,6 +483,59 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
}
|
||||
},
|
||||
// Drag and drop file upload handlers
|
||||
'dragover .js-card-details'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
'dragenter .js-card-details'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const card = this.data();
|
||||
const board = card.board();
|
||||
// Only allow drag-and-drop if user can modify card and board allows attachments
|
||||
if (card.canModifyCard() && board && board.allowsAttachments) {
|
||||
// Check if the drag contains files
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
$(event.currentTarget).addClass('is-dragging-over');
|
||||
}
|
||||
}
|
||||
},
|
||||
'dragleave .js-card-details'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
},
|
||||
'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;
|
||||
if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the drop contains files (not just text/HTML)
|
||||
if (!dataTransfer.types.includes('Files')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileUpload(card, files);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
@@ -490,6 +545,16 @@ Template.cardDetails.helpers({
|
||||
isPopup() {
|
||||
let ret = !!Utils.getPopupCardId();
|
||||
return ret;
|
||||
},
|
||||
// Upload progress helpers
|
||||
hasActiveUploads() {
|
||||
return uploadProgressManager.hasActiveUploads(this._id);
|
||||
},
|
||||
uploads() {
|
||||
return uploadProgressManager.getUploadsForCard(this._id);
|
||||
},
|
||||
uploadCount() {
|
||||
return uploadProgressManager.getUploadCountForCard(this._id);
|
||||
}
|
||||
});
|
||||
Template.cardDetailsPopup.onDestroyed(() => {
|
||||
@@ -592,6 +657,10 @@ Template.cardDetailsActionsPopup.helpers({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
|
||||
showListOnMinicard() {
|
||||
return this.showListOnMinicard;
|
||||
},
|
||||
});
|
||||
|
||||
Template.cardDetailsActionsPopup.events({
|
||||
@@ -637,6 +706,12 @@ Template.cardDetailsActionsPopup.events({
|
||||
if (!err && ret) Popup.close();
|
||||
});
|
||||
},
|
||||
'click .js-toggle-show-list-on-minicard'() {
|
||||
const currentCard = this;
|
||||
const newValue = !currentCard.showListOnMinicard;
|
||||
Cards.update(currentCard._id, { $set: { showListOnMinicard: newValue } });
|
||||
Popup.close();
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
.palette-colors {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start; /* left-align color chips in wider popovers */
|
||||
}
|
||||
.palette-colors .palette-color {
|
||||
flex-grow: 1;
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 9px;
|
||||
margin-bottom: 1.2vh;
|
||||
}
|
||||
.minicard-wrapper.placeholder {
|
||||
background: #ccc;
|
||||
border-radius: 9px;
|
||||
border-radius: 1.2vw;
|
||||
}
|
||||
.minicard-wrapper.ui-sortable-helper {
|
||||
cursor: grabbing;
|
||||
@@ -16,28 +16,28 @@
|
||||
}
|
||||
.minicard-wrapper.ui-sortable-helper .and-n-other {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
padding: 4px;
|
||||
height: 2.5vh;
|
||||
padding: 0.5vh;
|
||||
background-color: #f2f2f2;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
border-radius: 0.4vw;
|
||||
}
|
||||
.minicard-wrapper.ui-sortable-helper .multi-selection-checkbox {
|
||||
display: none;
|
||||
}
|
||||
.minicard-wrapper .multi-selection-checkbox + .minicard {
|
||||
margin-left: 8px;
|
||||
margin-left: 1vw;
|
||||
}
|
||||
@media only screen {
|
||||
.minicard {
|
||||
padding: 6px 8px 2px;
|
||||
padding: 0.8vh 1vw 0.3vh;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
background-color: #fff;
|
||||
min-height: 20px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
|
||||
border-radius: 2px;
|
||||
min-height: 2.5vh;
|
||||
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.15);
|
||||
border-radius: 0.3vw;
|
||||
color: #4d4d4d;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s, border-radius 0.2s;
|
||||
@@ -45,14 +45,14 @@
|
||||
}
|
||||
.minicard-details-menu-with-handle {
|
||||
float: right;
|
||||
font-size: 18px;
|
||||
padding-right: 30px;
|
||||
padding-left: 5px;
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
padding-right: 4vw;
|
||||
padding-left: 0.7vw;
|
||||
}
|
||||
.minicard-details-menu {
|
||||
float: right;
|
||||
font-size: 18px;
|
||||
padding-left: 5px;
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
padding-left: 0.7vw;
|
||||
}
|
||||
@media print {
|
||||
.minicard-details-menu,
|
||||
@@ -63,7 +63,7 @@
|
||||
.minicard.linked-board .linked-icon,
|
||||
.minicard.linked-card .linked-icon {
|
||||
display: inline-block;
|
||||
margin-right: 11px;
|
||||
margin-right: 1.5vw;
|
||||
vertical-align: baseline;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -72,11 +72,11 @@
|
||||
color: #937760;
|
||||
}
|
||||
.is-selected .minicard {
|
||||
transform: translateX(11px);
|
||||
transform: translateX(1.5vw);
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
z-index: 25;
|
||||
box-shadow: -2px 1px 2px rgba(0,0,0,0.2);
|
||||
box-shadow: -0.3vw 0.2vh 0.3vh rgba(0,0,0,0.2);
|
||||
}
|
||||
.minicard:hover:not(.minicard-composer),
|
||||
.is-selected .minicard,
|
||||
@@ -90,20 +90,20 @@
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
height: 145px;
|
||||
height: 20vh;
|
||||
user-select: none;
|
||||
margin: 6px -8px 6px -8px;
|
||||
border-radius: top 2px;
|
||||
margin: 0.8vh -1vw 0.8vh -1vw;
|
||||
border-radius: top 0.3vw;
|
||||
}
|
||||
.minicard .minicard-labels {
|
||||
float: none;
|
||||
}
|
||||
.minicard .minicard-labels .minicard-label {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 2px;
|
||||
margin-right: 3px;
|
||||
margin-bottom: 3px;
|
||||
width: 1.5vw;
|
||||
height: 1.5vw;
|
||||
border-radius: 0.3vw;
|
||||
margin-right: 0.4vw;
|
||||
margin-bottom: 0.4vh;
|
||||
}
|
||||
.minicard .minicard-labels-no-text {
|
||||
display: flex;
|
||||
@@ -119,22 +119,22 @@
|
||||
flex-grow: 1;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
max-width: 100px;
|
||||
margin-right: 4px;
|
||||
max-width: 13vw;
|
||||
margin-right: 0.5vw;
|
||||
}
|
||||
.minicard .minicard-custom-field-item-fullwidth {
|
||||
flex-grow: 1;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
max-width: 100%;
|
||||
margin-right: 4px;
|
||||
margin-right: 0.5vw;
|
||||
}
|
||||
.minicard .handle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 2.5vw;
|
||||
height: 2.5vw;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
right: 0.7vw;
|
||||
top: 0.7vh;
|
||||
display: none;
|
||||
}
|
||||
@media only screen {
|
||||
@@ -143,13 +143,13 @@
|
||||
}
|
||||
}
|
||||
.minicard .handle .fa-arrows {
|
||||
font-size: 20px;
|
||||
font-size: clamp(16px, 3vw, 20px);
|
||||
color: #ccc;
|
||||
}
|
||||
.minicard .minicard-title .card-number {
|
||||
color: #b3b3b3;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
margin-right: 0.7vw;
|
||||
}
|
||||
@media only screen {
|
||||
.minicard .minicard-title p:last-child {
|
||||
@@ -166,11 +166,11 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.minicard .date {
|
||||
margin-right: 3px;
|
||||
margin-right: 0.4vw;
|
||||
}
|
||||
.minicard .badges {
|
||||
float: left;
|
||||
margin-top: 7px;
|
||||
margin-top: 1vh;
|
||||
color: #808080;
|
||||
}
|
||||
.minicard .badges:empty {
|
||||
@@ -178,14 +178,14 @@
|
||||
}
|
||||
.minicard .badges .badge {
|
||||
float: left;
|
||||
margin-right: 11px;
|
||||
margin-bottom: 3px;
|
||||
margin-right: 1.5vw;
|
||||
margin-bottom: 0.4vh;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.minicard .badges .badge.is-finished {
|
||||
background: #3cb500;
|
||||
padding: 0px 3px;
|
||||
border-radius: 3px;
|
||||
padding: 0 0.4vw;
|
||||
border-radius: 0.4vw;
|
||||
color: #fff;
|
||||
}
|
||||
.minicard .badges .badge:last-of-type {
|
||||
@@ -201,41 +201,41 @@
|
||||
}
|
||||
.minicard .badges .badge .badge-text {
|
||||
font-size: 0.9em;
|
||||
padding-left: 2px;
|
||||
line-height: 14px;
|
||||
padding-left: 0.3vw;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.minicard .badges .badge .check-list-text {
|
||||
padding-left: 0px;
|
||||
line-height: 12px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.minicard .minicard-members,
|
||||
.minicard .minicard-assignees,
|
||||
.minicard .minicard-creator {
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
margin-bottom: 4px;
|
||||
margin-left: 0.7vw;
|
||||
margin-bottom: 0.5vh;
|
||||
}
|
||||
.minicard .minicard-members .member,
|
||||
.minicard .minicard-assignees .member,
|
||||
.minicard .minicard-creator .member {
|
||||
float: right;
|
||||
border-radius: 50%;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
margin-bottom: 4px;
|
||||
height: 3.5vw;
|
||||
width: 3.5vw;
|
||||
margin-bottom: 0.5vh;
|
||||
}
|
||||
.minicard .minicard-members .assignee,
|
||||
.minicard .minicard-assignees .assignee,
|
||||
.minicard .minicard-creator .assignee {
|
||||
float: right;
|
||||
border-radius: 50%;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
height: 3.5vw;
|
||||
width: 3.5vw;
|
||||
}
|
||||
.minicard .minicard-members + .badges,
|
||||
.minicard .minicard-assignees + .badges,
|
||||
.minicard .minicard-creator + .badges {
|
||||
margin-top: 5px;
|
||||
margin-top: 0.7vh;
|
||||
}
|
||||
.minicard .minicard-assignees {
|
||||
border-bottom: 1px solid #f00;
|
||||
@@ -248,13 +248,13 @@
|
||||
display: none;
|
||||
}
|
||||
.minicard .minicard-description {
|
||||
padding: 6px 0 0 8px;
|
||||
padding: 0.8vh 0 0 1vw;
|
||||
color: #000;
|
||||
background-color: #eee;
|
||||
width: 100%;
|
||||
margin-bottom: 2px;
|
||||
margin-left: -4px;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 0.3vh;
|
||||
margin-left: -0.5vw;
|
||||
border-radius: 0.4vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
@@ -262,7 +262,7 @@
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.minicard.minicard-composer {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 1.3vh;
|
||||
}
|
||||
.minicard.minicard-composer textarea.minicard-composer-textarea,
|
||||
.minicard.minicard-composer textarea.minicard-composer-textarea:focus {
|
||||
@@ -273,9 +273,9 @@
|
||||
height: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: 162px;
|
||||
min-height: 36px;
|
||||
margin-bottom: 20px;
|
||||
max-height: 22vh;
|
||||
min-height: 5vh;
|
||||
margin-bottom: 2.5vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.parent-prefix {
|
||||
@@ -580,3 +580,26 @@
|
||||
.text-green {
|
||||
color: #008000;
|
||||
}
|
||||
|
||||
/* Drag and drop file upload visual feedback */
|
||||
.minicard.is-dragging-over {
|
||||
border: 2px dashed #0079bf;
|
||||
background-color: #e3f2fd !important;
|
||||
transform: scale(1.02);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* List name display on minicard */
|
||||
.minicard-list-name {
|
||||
font-size: 0.75em;
|
||||
color: #8c8c8c;
|
||||
margin-top: 0.2vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3vw;
|
||||
}
|
||||
|
||||
.minicard-list-name i.fa {
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
@@ -31,6 +31,27 @@ template(name="minicard")
|
||||
if cover
|
||||
if currentBoard.allowsCoverAttachmentOnMinicard
|
||||
.minicard-cover(style="background-image: url('{{cover.link 'original'}}?dummyReloadAfterSessionEstablished={{sess}}');")
|
||||
|
||||
// Upload progress indicator for drag-and-drop uploads
|
||||
if hasActiveUploads
|
||||
.minicard-upload-progress
|
||||
.upload-progress-header
|
||||
i.fa.fa-upload
|
||||
span {{_ 'uploading-files'}} ({{uploadCount}})
|
||||
each uploads
|
||||
.upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
|
||||
.upload-progress-filename {{file.name}}
|
||||
.upload-progress-bar
|
||||
.upload-progress-fill(style="width: {{progress}}%")
|
||||
if $eq status 'error'
|
||||
.upload-progress-error
|
||||
i.fa.fa-exclamation-triangle
|
||||
span {{_ 'upload-failed'}}
|
||||
else if $eq status 'completed'
|
||||
.upload-progress-success
|
||||
i.fa.fa-check
|
||||
span {{_ 'upload-completed'}}
|
||||
|
||||
.minicard-title
|
||||
if $eq 'prefix-with-full-path' currentBoard.presentParentTask
|
||||
.parent-prefix
|
||||
@@ -51,12 +72,6 @@ template(name="minicard")
|
||||
span.card-number
|
||||
| ##{getCardNumber}
|
||||
= getTitle
|
||||
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentString ' > ' }}
|
||||
if $eq 'subtext-with-parent' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentCardName }}
|
||||
if labels
|
||||
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
|
||||
each labels
|
||||
@@ -176,6 +191,16 @@ template(name="minicard")
|
||||
.minicard-description
|
||||
+viewer
|
||||
| {{ getDescription }}
|
||||
if shouldShowListOnMinicard
|
||||
.minicard-list-name
|
||||
i.fa.fa-list
|
||||
| {{ listName }}
|
||||
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentString ' > ' }}
|
||||
if $eq 'subtext-with-parent' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentCardName }}
|
||||
|
||||
template(name="editCardSortOrderPopup")
|
||||
input.js-edit-card-sort-popup(type='text' autofocus value=sort dir="auto")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { CustomFieldStringTemplate } from '/client/lib/customFields'
|
||||
import { CustomFieldStringTemplate } from '/client/lib/customFields';
|
||||
import { handleFileUpload } from './attachments';
|
||||
import uploadProgressManager from '../../lib/uploadProgressManager';
|
||||
|
||||
// Template.cards.events({
|
||||
// 'click .member': Popup.open('cardMember')
|
||||
@@ -107,6 +109,59 @@ BlazeComponent.extendComponent({
|
||||
'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
|
||||
'click .minicard-labels' : this.cardLabelsPopup,
|
||||
'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
|
||||
// Drag and drop file upload handlers
|
||||
'dragover .minicard'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
'dragenter .minicard'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const card = this.data();
|
||||
const board = card.board();
|
||||
// Only allow drag-and-drop if user can modify card and board allows attachments
|
||||
if (card.canModifyCard() && board && board.allowsAttachments) {
|
||||
// Check if the drag contains files
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
$(event.currentTarget).addClass('is-dragging-over');
|
||||
}
|
||||
}
|
||||
},
|
||||
'dragleave .minicard'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
},
|
||||
'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;
|
||||
if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the drop contains files (not just text/HTML)
|
||||
if (!dataTransfer.types.includes('Files')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileUpload(card, files);
|
||||
}
|
||||
},
|
||||
}
|
||||
];
|
||||
},
|
||||
@@ -131,6 +186,29 @@ Template.minicard.helpers({
|
||||
},
|
||||
isWatching() {
|
||||
return this.findWatcher(Meteor.userId());
|
||||
},
|
||||
// Upload progress helpers
|
||||
hasActiveUploads() {
|
||||
return uploadProgressManager.hasActiveUploads(this._id);
|
||||
},
|
||||
uploads() {
|
||||
return uploadProgressManager.getUploadsForCard(this._id);
|
||||
},
|
||||
uploadCount() {
|
||||
return uploadProgressManager.getUploadCountForCard(this._id);
|
||||
},
|
||||
listName() {
|
||||
const list = this.list();
|
||||
return list ? list.title : '';
|
||||
},
|
||||
|
||||
shouldShowListOnMinicard() {
|
||||
// Show list name if either:
|
||||
// 1. Board-wide setting is enabled, OR
|
||||
// 2. This specific card has the setting enabled
|
||||
const currentBoard = this.currentBoard;
|
||||
if (!currentBoard) return false;
|
||||
return currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ button {
|
||||
box-sizing: border-box;
|
||||
background-color: #ebebeb;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
border-radius: 0.4vw;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
min-height: 34px;
|
||||
padding: 7px;
|
||||
margin-bottom: 1.5vh;
|
||||
min-height: 4.5vh;
|
||||
padding: 1vh 1vw;
|
||||
}
|
||||
select.full,
|
||||
textarea.full,
|
||||
@@ -31,17 +31,18 @@ button:focus {
|
||||
outline: 0;
|
||||
}
|
||||
input[type="file"] {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 2vh;
|
||||
}
|
||||
input[type="radio"] {
|
||||
-webkit-appearance: radio;
|
||||
appearance: radio;
|
||||
min-height: inherit;
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="email"] {
|
||||
transition: background 85ms ease-in, border-color 85ms ease-in;
|
||||
width: 250px;
|
||||
width: min(250px, 30vw);
|
||||
}
|
||||
input[type="text"].inline-input,
|
||||
input[type="password"].inline-input,
|
||||
@@ -49,10 +50,10 @@ input[type="email"].inline-input {
|
||||
background: none;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 2px;
|
||||
padding: 0.3vh;
|
||||
min-height: 0;
|
||||
height: 18px;
|
||||
width: 200px;
|
||||
height: 2.5vh;
|
||||
width: min(200px, 25vw);
|
||||
}
|
||||
input[type="text"].full-line,
|
||||
input[type="password"].full-line,
|
||||
@@ -96,31 +97,31 @@ input[type="email"]:disabled,
|
||||
textarea:disabled {
|
||||
background-color: #dcdcdc;
|
||||
border-color: #bfbfbf;
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
select {
|
||||
max-height: 300px;
|
||||
width: 256px;
|
||||
margin-bottom: 8px;
|
||||
max-height: 40vh;
|
||||
width: min(256px, 32vw);
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
select.inline {
|
||||
width: 100%;
|
||||
}
|
||||
option[disabled] {
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
}
|
||||
textarea {
|
||||
height: 150px;
|
||||
height: 20vh;
|
||||
transition: background 85ms ease-in, border-color 85ms ease-in;
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
}
|
||||
textarea.editor {
|
||||
resize: none;
|
||||
padding-bottom: 22px;
|
||||
padding-bottom: 3vh;
|
||||
}
|
||||
.button {
|
||||
border-radius: 3px;
|
||||
@@ -135,14 +136,14 @@ button {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
padding: 7px 20px;
|
||||
line-height: 1.3;
|
||||
padding: 1vh 2.5vw;
|
||||
text-align: center;
|
||||
}
|
||||
input[type="submit"] .wide,
|
||||
button .wide {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
padding-left: 4vw;
|
||||
padding-right: 4vw;
|
||||
}
|
||||
input[type="submit"]:hover,
|
||||
button:hover,
|
||||
@@ -223,8 +224,8 @@ button.disabled:active,
|
||||
}
|
||||
fieldset {
|
||||
border: 1px solid #bfbfbf;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
padding: 2vh 2vw;
|
||||
margin-bottom: 2vh;
|
||||
}
|
||||
input[type="hidden"] {
|
||||
display: none;
|
||||
@@ -232,18 +233,18 @@ input[type="hidden"] {
|
||||
.radio-div,
|
||||
.check-div {
|
||||
display: block;
|
||||
margin: 0 0 4px 20px;
|
||||
min-height: 20px;
|
||||
margin: 0 0 0.5vh 2.5vw;
|
||||
min-height: 2.5vh;
|
||||
position: relative;
|
||||
}
|
||||
.radio-div input,
|
||||
.check-div input {
|
||||
left: -18px;
|
||||
left: -2.5vw;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
top: 0.3vh;
|
||||
}
|
||||
.radio-div label,
|
||||
.check-div label {
|
||||
@@ -252,23 +253,23 @@ input[type="hidden"] {
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 0.5vh;
|
||||
}
|
||||
label.form-error {
|
||||
color: #ba1212;
|
||||
color: #d32f2f;
|
||||
}
|
||||
input::-webkit-input-placeholder,
|
||||
textarea::-webkit-input-placeholder,
|
||||
input::-moz-placeholder,
|
||||
textarea::-moz-placeholder {
|
||||
color: #8c8c8c;
|
||||
color: #333 !important;
|
||||
}
|
||||
.edit-controls,
|
||||
.add-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 1.5vh;
|
||||
}
|
||||
@media print {
|
||||
.add-controls {
|
||||
@@ -280,13 +281,13 @@ textarea::-moz-placeholder {
|
||||
.edit-controls input[type=button],
|
||||
.add-controls input[type=button] {
|
||||
float: left;
|
||||
height: 32px;
|
||||
height: 4.5vh;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.edit-controls .fa-times-thin,
|
||||
.add-controls .fa-times-thin {
|
||||
font-size: 26px;
|
||||
margin: 3px 10px;
|
||||
font-size: clamp(20px, 4vw, 26px);
|
||||
margin: 0.5vh 1.5vw;
|
||||
}
|
||||
[type="checkbox"]:not(:checked),
|
||||
[type="checkbox"]:checked {
|
||||
@@ -316,6 +317,7 @@ textarea::-moz-placeholder {
|
||||
border-left: 2px solid transparent;
|
||||
transform: rotate(40deg);
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
transform-origin: 100% 100%;
|
||||
}
|
||||
.button-link {
|
||||
@@ -376,14 +378,14 @@ textarea::-moz-placeholder {
|
||||
.button-link.setting.disabled {
|
||||
background: #fff;
|
||||
border-color: #e9e9e9;
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
cursor: default;
|
||||
}
|
||||
.button-link.setting.disabled select {
|
||||
display: none;
|
||||
}
|
||||
.button-link.setting.disabled:hover .label {
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
}
|
||||
.button-link.setting.disabled,
|
||||
.button-link.setting.disabled:hover,
|
||||
@@ -399,7 +401,7 @@ textarea::-moz-placeholder {
|
||||
color: #a8a8a8;
|
||||
}
|
||||
.button-link.setting .label {
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
@@ -509,7 +511,7 @@ button.loud-text-button:hover {
|
||||
border-radius: 3px;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
display: block;
|
||||
margin: 2px 0;
|
||||
padding: 6px 8px;
|
||||
@@ -574,7 +576,7 @@ button.loud-text-button:hover {
|
||||
top: 6px;
|
||||
}
|
||||
.big-link.none {
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
text-decoration: none;
|
||||
}
|
||||
.big-link.none:hover {
|
||||
@@ -604,7 +606,7 @@ button.loud-text-button:hover {
|
||||
}
|
||||
.show-more {
|
||||
border-radius: 3px;
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
display: block;
|
||||
padding: 16px 8px 16px 40px;
|
||||
margin: 8px 0;
|
||||
|
||||
Vendored
+8
-8
@@ -5,17 +5,17 @@
|
||||
.map-members .mapping-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -4px;
|
||||
margin: 0 -0.5vw;
|
||||
}
|
||||
.map-members .mapping-list .mapping-item {
|
||||
max-width: 300px;
|
||||
min-width: 200px;
|
||||
padding: 6px;
|
||||
margin: 5px;
|
||||
max-width: min(300px, 40vw);
|
||||
min-width: min(200px, 26vw);
|
||||
padding: 0.8vh 0.8vw;
|
||||
margin: 0.7vh;
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
|
||||
border-radius: 0.4vw;
|
||||
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.15);
|
||||
}
|
||||
.map-members .mapping-list .mapping-item:hover {
|
||||
background: #f2f2f2;
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
.map-members .mapping-list .wekan {
|
||||
display: inline-block;
|
||||
width: 35px;
|
||||
width: 4.5vw;
|
||||
}
|
||||
.map-members .mapping-list .wekan .member {
|
||||
float: none;
|
||||
|
||||
@@ -8,8 +8,221 @@
|
||||
padding: 0;
|
||||
float: left;
|
||||
}
|
||||
|
||||
/* List resize handle */
|
||||
.list-resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -3px;
|
||||
width: 6px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
z-index: 10;
|
||||
background: transparent;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.list-resize-handle:hover {
|
||||
background: rgba(0, 123, 255, 0.4);
|
||||
box-shadow: 0 0 4px rgba(0, 123, 255, 0.3);
|
||||
}
|
||||
|
||||
.list-resize-handle:active {
|
||||
background: rgba(0, 123, 255, 0.6);
|
||||
box-shadow: 0 0 6px rgba(0, 123, 255, 0.4);
|
||||
}
|
||||
|
||||
/* Show resize handle only on hover */
|
||||
.list:hover .list-resize-handle {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.list:hover .list-resize-handle:hover {
|
||||
background: rgba(0, 123, 255, 0.4);
|
||||
box-shadow: 0 0 4px rgba(0, 123, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Add a subtle indicator line */
|
||||
.list-resize-handle::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 1px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.list-resize-handle:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Disable resize handle for collapsed lists and mobile view */
|
||||
.list.list-collapsed .list-resize-handle,
|
||||
.list.mobile-view .list-resize-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Disable resize handle for auto-width lists */
|
||||
.list.list-auto-width .list-resize-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Visual feedback during resize */
|
||||
.list.list-resizing {
|
||||
transition: none !important;
|
||||
box-shadow: 0 0 10px rgba(0, 123, 255, 0.3);
|
||||
/* Ensure the list maintains its new width during resize */
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
/* Override any conflicting layout properties */
|
||||
float: left !important;
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
/* Force width to be respected */
|
||||
width: var(--list-width, auto) !important;
|
||||
min-width: var(--list-width, auto) !important;
|
||||
max-width: var(--list-width, auto) !important;
|
||||
}
|
||||
|
||||
body.list-resizing-active {
|
||||
cursor: col-resize !important;
|
||||
}
|
||||
|
||||
body.list-resizing-active * {
|
||||
cursor: col-resize !important;
|
||||
}
|
||||
|
||||
/* Ensure swimlane container doesn't interfere with list resizing */
|
||||
.swimlane .list.list-resizing {
|
||||
/* Override any swimlane flex properties */
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
/* Ensure width is respected */
|
||||
width: var(--list-width, auto) !important;
|
||||
min-width: var(--list-width, auto) !important;
|
||||
max-width: var(--list-width, auto) !important;
|
||||
}
|
||||
|
||||
/* More aggressive override for any container that might interfere */
|
||||
.js-swimlane .list.list-resizing,
|
||||
.dragscroll .list.list-resizing,
|
||||
[id^="swimlane-"] .list.list-resizing {
|
||||
/* Force the width to be applied */
|
||||
width: var(--list-width, auto) !important;
|
||||
min-width: var(--list-width, auto) !important;
|
||||
max-width: var(--list-width, auto) !important;
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
float: left !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Ensure the width persists after resize is complete */
|
||||
.js-swimlane .list[style*="--list-width"],
|
||||
.dragscroll .list[style*="--list-width"],
|
||||
[id^="swimlane-"] .list[style*="--list-width"] {
|
||||
/* Maintain the width after resize */
|
||||
width: var(--list-width, auto) !important;
|
||||
min-width: var(--list-width, auto) !important;
|
||||
max-width: var(--list-width, auto) !important;
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
float: left !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Ensure consistent header height for all lists */
|
||||
.list-header {
|
||||
/* Maintain consistent height and padding for all lists */
|
||||
min-height: 2.5vh !important;
|
||||
height: auto !important;
|
||||
padding: 2.5vh 1.5vw 0.5vh !important;
|
||||
/* Make sure the background covers the full height */
|
||||
background-color: #e4e4e4 !important;
|
||||
border-bottom: 0.8vh solid #e4e4e4 !important;
|
||||
/* Use original display for consistent button positioning */
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
/* Prevent vertical expansion but allow normal height */
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* Ensure title text doesn't cause height changes for all lists */
|
||||
.list-header .list-header-name {
|
||||
/* Prevent text wrapping to maintain consistent height */
|
||||
white-space: nowrap !important;
|
||||
/* Truncate text with ellipsis if too long */
|
||||
text-overflow: ellipsis !important;
|
||||
/* Ensure proper line height */
|
||||
line-height: 1.2 !important;
|
||||
/* Ensure it doesn't overflow */
|
||||
overflow: hidden !important;
|
||||
/* Add margin to prevent overlap with buttons */
|
||||
margin-right: 120px !important;
|
||||
}
|
||||
|
||||
/* Position drag handle at top-right corner for ALL lists */
|
||||
.list-header .list-header-handle {
|
||||
/* Position at top-right corner, aligned with title text top */
|
||||
position: absolute !important;
|
||||
top: 2.5vh !important;
|
||||
right: 1.5vw !important;
|
||||
/* Ensure it's above other elements */
|
||||
z-index: 15 !important;
|
||||
/* Remove margin since it's absolutely positioned */
|
||||
margin-right: 0 !important;
|
||||
/* Ensure proper display */
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
/* Ensure buttons maintain original positioning */
|
||||
.js-swimlane .list[style*="--list-width"] .list-header .list-header-plus-top,
|
||||
.js-swimlane .list[style*="--list-width"] .list-header .js-collapse,
|
||||
.js-swimlane .list[style*="--list-width"] .list-header .js-open-list-menu,
|
||||
.dragscroll .list[style*="--list-width"] .list-header .list-header-plus-top,
|
||||
.dragscroll .list[style*="--list-width"] .list-header .js-collapse,
|
||||
.dragscroll .list[style*="--list-width"] .list-header .js-open-list-menu,
|
||||
[id^="swimlane-"] .list[style*="--list-width"] .list-header .list-header-plus-top,
|
||||
[id^="swimlane-"] .list[style*="--list-width"] .list-header .js-collapse,
|
||||
[id^="swimlane-"] .list[style*="--list-width"] .list-header .js-open-list-menu {
|
||||
/* Use original positioning to maintain layout */
|
||||
position: relative !important;
|
||||
/* Maintain original spacing */
|
||||
margin-right: 15px !important;
|
||||
/* Ensure proper display */
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
/* Ensure watch icon and card count maintain original positioning */
|
||||
.js-swimlane .list[style*="--list-width"] .list-header .list-header-watch-icon,
|
||||
.dragscroll .list[style*="--list-width"] .list-header .list-header-watch-icon,
|
||||
[id^="swimlane-"] .list[style*="--list-width"] .list-header .list-header-watch-icon,
|
||||
.js-swimlane .list[style*="--list-width"] .list-header .cardCount,
|
||||
.dragscroll .list[style*="--list-width"] .list-header .cardCount,
|
||||
[id^="swimlane-"] .list[style*="--list-width"] .list-header .cardCount {
|
||||
/* Use original positioning to maintain layout */
|
||||
position: relative !important;
|
||||
/* Maintain original spacing */
|
||||
margin-right: 15px !important;
|
||||
/* Ensure proper display */
|
||||
display: inline-block !important;
|
||||
}
|
||||
[id^="swimlane-"] .list:first-child {
|
||||
min-width: 20px;
|
||||
min-width: 2.5vw;
|
||||
}
|
||||
.list.list-auto-width {
|
||||
flex: 1;
|
||||
@@ -33,7 +246,7 @@
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
height: 100px;
|
||||
height: 15vh;
|
||||
}
|
||||
.list.list-collapsed {
|
||||
flex: none;
|
||||
@@ -45,24 +258,24 @@
|
||||
.list.list-composer .list-name-input,
|
||||
.list .list-composer .list-name-input {
|
||||
background: #fff;
|
||||
margin: -3px 0 8px;
|
||||
margin: -0.4vh 0 1vh;
|
||||
}
|
||||
.list-header-add {
|
||||
flex: 0 0 auto;
|
||||
padding: 12px;
|
||||
padding: 1.5vh 1.5vw;
|
||||
position: relative;
|
||||
min-height: 20px;
|
||||
min-height: 2.5vh;
|
||||
}
|
||||
.list-header {
|
||||
flex: 0 0 auto;
|
||||
padding: 20px 12px 4px;
|
||||
padding: 2.5vh 1.5vw 0.5vh;
|
||||
position: relative;
|
||||
min-height: 20px;
|
||||
min-height: 2.5vh;
|
||||
background-color: #e4e4e4;
|
||||
border-bottom: 6px solid #e4e4e4;
|
||||
border-bottom: 0.8vh solid #e4e4e4;
|
||||
}
|
||||
.list-header.list-header-card-count {
|
||||
min-height: 35px;
|
||||
min-height: 4.5vh;
|
||||
height: auto;
|
||||
}
|
||||
.list-header.ui-sortable-handle {
|
||||
@@ -73,21 +286,21 @@
|
||||
}
|
||||
.list-header .list-header-name {
|
||||
display: inline;
|
||||
font-size: 16px;
|
||||
line-height: 17px;
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
min-height: 9px;
|
||||
min-width: 30px;
|
||||
min-height: 1.2vh;
|
||||
min-width: 4vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.list-rotated {
|
||||
width: 10px;
|
||||
height: 250px;
|
||||
margin-top: -90px;
|
||||
margin-left: -110px;
|
||||
width: 1.3vw;
|
||||
height: 35vh;
|
||||
margin-top: -12vh;
|
||||
margin-left: -14vw;
|
||||
margin-right: 0;
|
||||
transform: rotate(90deg);
|
||||
position: relative;
|
||||
@@ -214,11 +427,137 @@
|
||||
#js-list-width-edit .list-width-error {
|
||||
display: none;
|
||||
}
|
||||
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
|
||||
.mini-list.mobile-view {
|
||||
flex: 0 0 60px;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
border-left: 0px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.list.mobile-view {
|
||||
display: contents;
|
||||
flex-basis: auto;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
border-left: 0px;
|
||||
}
|
||||
.list.mobile-view:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
.list.mobile-view.ui-sortable-helper {
|
||||
flex: 0 0 60px;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
border-left: 0px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.list.mobile-view.ui-sortable-helper .list-header.ui-sortable-handle {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.list.mobile-view.placeholder {
|
||||
flex: 0 0 60px;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
border-left: 0px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.list.mobile-view .list-body {
|
||||
padding: 15px 19px;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
.list.mobile-view .list-header {
|
||||
/*Updated padding values for mobile devices, this should fix text grouping issue*/
|
||||
padding: 20px 0px 20px 0px;
|
||||
border-bottom: 0px solid #e4e4e4;
|
||||
min-height: 30px;
|
||||
margin-top: 10px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
/* Force grid layout for iPhone */
|
||||
display: grid !important;
|
||||
grid-template-columns: 30px 1fr auto auto !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-left-icon {
|
||||
padding: 7px;
|
||||
padding-right: 27px;
|
||||
margin-top: 1px;
|
||||
top: -7px;
|
||||
left: -7px;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-menu-icon {
|
||||
padding: 14px;
|
||||
font-size: 40px !important;
|
||||
text-align: center;
|
||||
/* Force positioning for iPhone */
|
||||
position: absolute !important;
|
||||
right: 60px !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
z-index: 10;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-handle {
|
||||
padding: 14px;
|
||||
font-size: 48px !important;
|
||||
text-align: center;
|
||||
/* Force positioning for iPhone */
|
||||
position: absolute !important;
|
||||
right: 10px !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
z-index: 10;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-left-icon {
|
||||
display: grid;
|
||||
grid-row: 1/3;
|
||||
grid-column: 1;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-name {
|
||||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
align-self: end;
|
||||
font-size: 20px !important;
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.list.mobile-view .list-header .cardCount {
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
align-self: start;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-menu {
|
||||
grid-row: 1/3;
|
||||
grid-column: 3;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-menu-icon {
|
||||
grid-row: 1/3;
|
||||
grid-column: 3;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-handle {
|
||||
grid-row: 1/3;
|
||||
grid-column: 4;
|
||||
}
|
||||
.list.mobile-view .list-header .inlined-form {
|
||||
grid-row: 1/3;
|
||||
grid-column: 1/4;
|
||||
}
|
||||
.list.mobile-view .list-header .edit-controls {
|
||||
align-items: initial;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.mini-list {
|
||||
flex: 0 0 60px;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
border-left: 0px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
@@ -226,6 +565,7 @@
|
||||
display: contents;
|
||||
flex-basis: auto;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
border-left: 0px;
|
||||
}
|
||||
.list:first-child {
|
||||
@@ -250,6 +590,8 @@
|
||||
}
|
||||
.list-body {
|
||||
padding: 15px 19px;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
.list-header {
|
||||
/*Updated padding values for mobile devices, this should fix text grouping issue*/
|
||||
@@ -258,6 +600,8 @@
|
||||
min-height: 30px;
|
||||
margin-top: 10px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
.list-header .list-header-left-icon {
|
||||
padding: 7px;
|
||||
@@ -267,24 +611,29 @@
|
||||
left: -7px;
|
||||
}
|
||||
.list-header .list-header-menu-icon {
|
||||
padding: 14px;
|
||||
font-size: 40px;
|
||||
text-align: center;
|
||||
/* iOS Safari fallback positioning */
|
||||
position: absolute;
|
||||
padding: 7px;
|
||||
right: 60px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 47px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.list-header .list-header-handle {
|
||||
padding: 14px;
|
||||
font-size: 48px;
|
||||
text-align: center;
|
||||
/* iOS Safari fallback positioning */
|
||||
position: absolute;
|
||||
padding: 7px;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 10px;
|
||||
font-size: 24px;
|
||||
}
|
||||
.list-header {
|
||||
display: grid;
|
||||
grid-template-columns: 30px 5fr 1fr;
|
||||
grid-template-columns: 30px 1fr auto auto;
|
||||
gap: 10px;
|
||||
}
|
||||
.list-header .list-header-left-icon {
|
||||
display: grid;
|
||||
@@ -295,16 +644,30 @@
|
||||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
align-self: end;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.list-header .cardCount {
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
align-self: start;
|
||||
font-size: 16px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.list-header .list-header-menu {
|
||||
grid-row: 1/3;
|
||||
grid-column: 3;
|
||||
}
|
||||
.list-header .list-header-menu-icon {
|
||||
grid-row: 1/3;
|
||||
grid-column: 3;
|
||||
}
|
||||
.list-header .list-header-handle {
|
||||
grid-row: 1/3;
|
||||
grid-column: 4;
|
||||
}
|
||||
.list-header .inlined-form {
|
||||
grid-row: 1/3;
|
||||
grid-column: 1/4;
|
||||
@@ -313,6 +676,132 @@
|
||||
align-items: initial;
|
||||
}
|
||||
}
|
||||
|
||||
/* iPhone 12 Mini specific - fix icon positioning in stacked lists view */
|
||||
@media screen and (device-width: 375px) and (device-height: 812px), /* iPhone 12 Mini exact */
|
||||
screen and (max-width: 375px) and (max-height: 812px), /* iPhone 12 Mini viewport */
|
||||
screen and (-webkit-min-device-pixel-ratio: 3) and (max-width: 375px) /* iPhone 12 Mini Retina */ {
|
||||
.list.mobile-view .list-header {
|
||||
/* Force grid layout for iPhone 12 Mini */
|
||||
display: grid !important;
|
||||
grid-template-columns: 30px 1fr auto auto !important;
|
||||
gap: 10px !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.list.mobile-view .list-header .list-header-menu-icon {
|
||||
/* Remove absolute positioning for iPhone 12 Mini */
|
||||
position: static !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
/* Use grid positioning */
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 3 !important;
|
||||
padding: 14px !important;
|
||||
font-size: 40px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.list.mobile-view .list-header .list-header-handle {
|
||||
/* Remove absolute positioning for iPhone 12 Mini */
|
||||
position: static !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
/* Use grid positioning */
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 4 !important;
|
||||
padding: 14px !important;
|
||||
font-size: 48px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.list.mobile-view .list-header .list-header-name {
|
||||
grid-row: 1 !important;
|
||||
grid-column: 2 !important;
|
||||
align-self: end !important;
|
||||
font-size: 20px !important;
|
||||
font-weight: bold !important;
|
||||
line-height: 1.2 !important;
|
||||
padding-bottom: 2px !important;
|
||||
}
|
||||
|
||||
.list.mobile-view .list-header .cardCount {
|
||||
grid-row: 2 !important;
|
||||
grid-column: 2 !important;
|
||||
align-self: start !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
|
||||
.list.mobile-view .list-header .list-header-left-icon {
|
||||
display: grid !important;
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* iPhone device JavaScript detection fallback - fix icon positioning */
|
||||
.iphone-device .list.mobile-view .list-header {
|
||||
/* Force grid layout for iPhone devices */
|
||||
display: grid !important;
|
||||
grid-template-columns: 30px 1fr auto auto !important;
|
||||
gap: 10px !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.iphone-device .list.mobile-view .list-header .list-header-menu-icon {
|
||||
/* Remove absolute positioning for iPhone devices */
|
||||
position: static !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
/* Use grid positioning */
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 3 !important;
|
||||
padding: 14px !important;
|
||||
font-size: 40px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.iphone-device .list.mobile-view .list-header .list-header-handle {
|
||||
/* Remove absolute positioning for iPhone devices */
|
||||
position: static !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
/* Use grid positioning */
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 4 !important;
|
||||
padding: 14px !important;
|
||||
font-size: 48px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.iphone-device .list.mobile-view .list-header .list-header-name {
|
||||
grid-row: 1 !important;
|
||||
grid-column: 2 !important;
|
||||
align-self: end !important;
|
||||
font-size: 20px !important;
|
||||
font-weight: bold !important;
|
||||
line-height: 1.2 !important;
|
||||
padding-bottom: 2px !important;
|
||||
}
|
||||
|
||||
.iphone-device .list.mobile-view .list-header .cardCount {
|
||||
grid-row: 2 !important;
|
||||
grid-column: 2 !important;
|
||||
align-self: start !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
|
||||
.iphone-device .list.mobile-view .list-header .list-header-left-icon {
|
||||
display: grid !important;
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 1 !important;
|
||||
}
|
||||
.link-board-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
template(name='list')
|
||||
.list.js-list(id="js-list-{{_id}}"
|
||||
style="{{#unless collapsed}}min-width:{{listWidth}}px;max-width:{{listConstraint}}px;{{/unless}}"
|
||||
class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}}")
|
||||
class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}} {{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
+listHeader
|
||||
+listBody
|
||||
.list-resize-handle.js-list-resize-handle
|
||||
|
||||
template(name='miniList')
|
||||
a.mini-list.js-select-list.js-list(id="js-list-{{_id}}")
|
||||
a.mini-list.js-select-list.js-list(id="js-list-{{_id}}" class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
+listHeader
|
||||
@@ -24,6 +24,9 @@ BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
const boardComponent = this.parentComponent().parentComponent();
|
||||
|
||||
// Initialize list resize functionality
|
||||
this.initializeListResize();
|
||||
|
||||
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
|
||||
const $cards = this.$('.js-minicards');
|
||||
|
||||
@@ -212,6 +215,146 @@ BlazeComponent.extendComponent({
|
||||
const list = Template.currentData();
|
||||
return user.isAutoWidth(list.boardId);
|
||||
},
|
||||
|
||||
initializeListResize() {
|
||||
const list = Template.currentData();
|
||||
const $list = this.$('.js-list');
|
||||
const $resizeHandle = this.$('.js-list-resize-handle');
|
||||
|
||||
// Only enable resize for non-collapsed, non-auto-width lists
|
||||
if (list.collapsed || this.autoWidth()) {
|
||||
$resizeHandle.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
let isResizing = false;
|
||||
let startX = 0;
|
||||
let startWidth = 0;
|
||||
let minWidth = 100; // Minimum width as defined in the existing code
|
||||
let maxWidth = this.listConstraint() || 1000; // Use constraint as max width
|
||||
let listConstraint = this.listConstraint(); // Store constraint value for use in event handlers
|
||||
const component = this; // Store reference to component for use in event handlers
|
||||
|
||||
const startResize = (e) => {
|
||||
isResizing = true;
|
||||
startX = e.pageX || e.originalEvent.touches[0].pageX;
|
||||
startWidth = $list.outerWidth();
|
||||
|
||||
// Add visual feedback
|
||||
$list.addClass('list-resizing');
|
||||
$('body').addClass('list-resizing-active');
|
||||
|
||||
// Prevent text selection during resize
|
||||
$('body').css('user-select', 'none');
|
||||
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const doResize = (e) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
|
||||
const deltaX = currentX - startX;
|
||||
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
|
||||
|
||||
// Apply the new width immediately for real-time feedback using CSS custom properties
|
||||
$list[0].style.setProperty('--list-width', `${newWidth}px`);
|
||||
$list.css({
|
||||
'width': `${newWidth}px`,
|
||||
'min-width': `${newWidth}px`,
|
||||
'max-width': `${newWidth}px`,
|
||||
'flex': 'none',
|
||||
'flex-basis': 'auto',
|
||||
'flex-grow': '0',
|
||||
'flex-shrink': '0'
|
||||
});
|
||||
|
||||
// Debug: log the width change
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Resizing list to ${newWidth}px`);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const stopResize = (e) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
isResizing = false;
|
||||
|
||||
// Calculate final width
|
||||
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
|
||||
const deltaX = currentX - startX;
|
||||
const finalWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
|
||||
|
||||
// Ensure the final width is applied using CSS custom properties
|
||||
$list[0].style.setProperty('--list-width', `${finalWidth}px`);
|
||||
$list.css({
|
||||
'width': `${finalWidth}px`,
|
||||
'min-width': `${finalWidth}px`,
|
||||
'max-width': `${finalWidth}px`,
|
||||
'flex': 'none',
|
||||
'flex-basis': 'auto',
|
||||
'flex-grow': '0',
|
||||
'flex-shrink': '0'
|
||||
});
|
||||
|
||||
// Remove visual feedback but keep the width
|
||||
$list.removeClass('list-resizing');
|
||||
$('body').removeClass('list-resizing-active');
|
||||
$('body').css('user-select', '');
|
||||
|
||||
// Keep the CSS custom property for persistent width
|
||||
// The CSS custom property will remain on the element to maintain the width
|
||||
|
||||
// Save the new width using the existing system
|
||||
const boardId = list.boardId;
|
||||
const listId = list._id;
|
||||
|
||||
// Use the same method as the hamburger menu
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Saving list width: ${finalWidth}px for list ${listId}`);
|
||||
}
|
||||
Meteor.call('applyListWidth', boardId, listId, finalWidth, listConstraint, (error, result) => {
|
||||
if (error) {
|
||||
console.error('Error saving list width:', error);
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('List width saved successfully:', result);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// Mouse events
|
||||
$resizeHandle.on('mousedown', startResize);
|
||||
$(document).on('mousemove', doResize);
|
||||
$(document).on('mouseup', stopResize);
|
||||
|
||||
// Touch events for mobile
|
||||
$resizeHandle.on('touchstart', startResize);
|
||||
$(document).on('touchmove', doResize);
|
||||
$(document).on('touchend', stopResize);
|
||||
|
||||
// Reactively update resize handle visibility when auto-width changes
|
||||
component.autorun(() => {
|
||||
if (component.autoWidth()) {
|
||||
$resizeHandle.hide();
|
||||
} else {
|
||||
$resizeHandle.show();
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up on component destruction
|
||||
component.onDestroyed(() => {
|
||||
$(document).off('mousemove', doResize);
|
||||
$(document).off('mouseup', stopResize);
|
||||
$(document).off('touchmove', doResize);
|
||||
$(document).off('touchend', stopResize);
|
||||
});
|
||||
},
|
||||
}).register('list');
|
||||
|
||||
Template.miniList.events({
|
||||
|
||||
@@ -314,7 +314,8 @@ BlazeComponent.extendComponent({
|
||||
// Pressing Tab should open the form of the next column, and Maj+Tab go
|
||||
// in the reverse order
|
||||
} else if (evt.keyCode === 9) {
|
||||
evt.preventDefault();
|
||||
// Prevent custom focus movement on Tab key for accessibility
|
||||
// evt.preventDefault();
|
||||
const isReverse = evt.shiftKey;
|
||||
const list = $(`#js-list-${this.data().listId}`);
|
||||
const listSelector = '.js-list:not(.js-list-composer)';
|
||||
@@ -413,13 +414,16 @@ BlazeComponent.extendComponent({
|
||||
// or `Enter` to validation the auto-completion. We also need to stop the
|
||||
// event propagation to prevent the card from submitting (on `Enter`) or
|
||||
// going on the next column (on `Tab`).
|
||||
/*
|
||||
onKeydown(evt, commands) {
|
||||
if (evt.keyCode === 9 || evt.keyCode === 13) {
|
||||
evt.stopPropagation();
|
||||
return commands.KEY_ENTER;
|
||||
}
|
||||
// Prevent custom focus movement on Tab key for accessibility
|
||||
// if (evt.keyCode === 9 || evt.keyCode === 13) {
|
||||
// evt.stopPropagation();
|
||||
// return commands.KEY_ENTER;
|
||||
//}
|
||||
return null;
|
||||
},
|
||||
*/
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -468,6 +472,14 @@ BlazeComponent.extendComponent({
|
||||
if (!this.selectedBoardId.get()) {
|
||||
return [];
|
||||
}
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
if (!board) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Ensure default swimlane exists
|
||||
board.getDefaultSwimline();
|
||||
|
||||
const swimlanes = ReactiveCache.getSwimlanes(
|
||||
{
|
||||
boardId: this.selectedBoardId.get()
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
.my-cards-board-wrapper {
|
||||
border-radius: 0 0 4px 4px;
|
||||
min-width: 400px;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0 0 0.5vw 0.5vw;
|
||||
min-width: min(400px, 52vw);
|
||||
margin-bottom: 2.5vh;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
border-width: 2px;
|
||||
border-width: 0.3vw;
|
||||
border-style: solid;
|
||||
border-color: #a2a2a2;
|
||||
}
|
||||
.my-cards-board-title {
|
||||
font-size: 1.4rem;
|
||||
font-size: clamp(1.2rem, 3vw, 1.6rem);
|
||||
font-weight: bold;
|
||||
padding: 0.5rem;
|
||||
padding: 0.7vh 0.7vw;
|
||||
background-color: #808080;
|
||||
color: #fff;
|
||||
}
|
||||
.my-cards-swimlane-title {
|
||||
font-size: 1.1rem;
|
||||
font-size: clamp(1rem, 2.5vw, 1.3rem);
|
||||
font-weight: bold;
|
||||
padding: 0.5rem;
|
||||
padding-bottom: 0.4rem;
|
||||
padding: 0.7vh 0.7vw;
|
||||
padding-bottom: 0.5vh;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 0.7vh;
|
||||
text-align: center;
|
||||
}
|
||||
.swimlane-default-color {
|
||||
@@ -29,23 +29,23 @@
|
||||
}
|
||||
.my-cards-list-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
font-size: clamp(1rem, 2.5vw, 1.3rem);
|
||||
text-align: center;
|
||||
margin-bottom: 0.7rem;
|
||||
margin-bottom: 0.9vh;
|
||||
}
|
||||
.my-cards-list-wrapper {
|
||||
margin: 1rem;
|
||||
border-radius: 5px;
|
||||
margin: 1.3vh 1.3vw;
|
||||
border-radius: 0.7vw;
|
||||
display: inline-grid;
|
||||
min-width: 250px;
|
||||
max-width: 350px;
|
||||
min-width: min(250px, 32vw);
|
||||
max-width: min(350px, 45vw);
|
||||
}
|
||||
.my-cards-card-wrapper {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 1.3vh;
|
||||
}
|
||||
.my-cards-dueat-list-wrapper {
|
||||
max-width: 500px;
|
||||
max-width: min(500px, 65vw);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
@@ -72,3 +72,28 @@
|
||||
border-radius: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Accessibility page styles */
|
||||
.accessibility-page {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.accessibility-page h2 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
.accessibility-page-content {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.accessibility-page-content p {
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@@ -1,8 +1,18 @@
|
||||
template(name="accessibilityHeaderBar")
|
||||
if currentUser
|
||||
h1
|
||||
| {{_ 'accessibility-title'}}
|
||||
if isAccessibilityEnabled
|
||||
= accessibilityTitle
|
||||
else
|
||||
| {{_ 'accessibility'}}
|
||||
|
||||
template(name="accessibility")
|
||||
if currentUser
|
||||
| {{_ 'accessibility-content'}}
|
||||
.accessibility-page
|
||||
if isAccessibilityEnabled
|
||||
.accessibility-page-content
|
||||
+viewer
|
||||
| {{accessibilityContent}}
|
||||
else
|
||||
.accessibility-page-content
|
||||
| {{_ 'accessibility-info-not-added-yet'}}
|
||||
@@ -1,11 +1,38 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
|
||||
// Shared helpers for both accessibility templates
|
||||
const accessibilityHelpers = {
|
||||
accessibilityTitle() {
|
||||
const setting = AccessibilitySettings.findOne({});
|
||||
return setting && setting.title ? setting.title : TAPi18n.__('accessibility-title');
|
||||
},
|
||||
accessibilityContent() {
|
||||
const setting = AccessibilitySettings.findOne({});
|
||||
return setting && setting.body ? setting.body : TAPi18n.__('accessibility-content');
|
||||
},
|
||||
isAccessibilityEnabled() {
|
||||
const setting = AccessibilitySettings.findOne({});
|
||||
return setting && setting.enabled;
|
||||
}
|
||||
};
|
||||
|
||||
// Main accessibility page component
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.error = new ReactiveVar('');
|
||||
this.loading = new ReactiveVar(false);
|
||||
|
||||
Meteor.subscribe('setting');
|
||||
Meteor.subscribe('accessibilitySettings');
|
||||
},
|
||||
...accessibilityHelpers
|
||||
}).register('accessibility');
|
||||
|
||||
// Header bar component
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
Meteor.subscribe('accessibilitySettings');
|
||||
},
|
||||
...accessibilityHelpers
|
||||
}).register('accessibilityHeaderBar');
|
||||
@@ -0,0 +1,29 @@
|
||||
template(name="bookmarks")
|
||||
.panel
|
||||
h2 {{_ 'bookmarks'}}
|
||||
if currentUser
|
||||
if hasStarredBoards
|
||||
ul
|
||||
each starredBoards
|
||||
li
|
||||
a(href="{{pathFor 'board' id=_id slug=slug}}")= title
|
||||
a.js-toggle-star(title="{{_ 'star-board-short-unstar'}}")
|
||||
i.fa.fa-star
|
||||
else
|
||||
p {{_ 'no-starred-boards'}}
|
||||
else
|
||||
p {{_ 'please-sign-in'}}
|
||||
|
||||
// Desktop popup
|
||||
template(name="bookmarksPopup")
|
||||
ul.pop-over-list
|
||||
if hasStarredBoards
|
||||
each starredBoards
|
||||
li
|
||||
a(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
i.fa.fa-star
|
||||
| #{title}
|
||||
a.js-toggle-star.right(title="{{_ 'star-board-short-unstar'}}")
|
||||
i.fa.fa-star
|
||||
else
|
||||
li {{_ 'no-starred-boards'}}
|
||||
@@ -0,0 +1,55 @@
|
||||
Template.bookmarks.helpers({
|
||||
hasStarredBoards() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (!user) return false;
|
||||
const { starredBoards = [] } = user.profile || {};
|
||||
return Array.isArray(starredBoards) && starredBoards.length > 0;
|
||||
},
|
||||
starredBoards() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (!user) return [];
|
||||
const { starredBoards = [] } = user.profile || {};
|
||||
if (!Array.isArray(starredBoards) || starredBoards.length === 0) return [];
|
||||
return Boards.find({ _id: { $in: starredBoards } }, { sort: { sort: 1 } });
|
||||
},
|
||||
});
|
||||
|
||||
Template.bookmarks.events({
|
||||
'click .js-toggle-star'(e) {
|
||||
e.preventDefault();
|
||||
const boardId = this._id;
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && boardId) {
|
||||
user.toggleBoardStar(boardId);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.bookmarksPopup.helpers({
|
||||
hasStarredBoards() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (!user) return false;
|
||||
const { starredBoards = [] } = user.profile || {};
|
||||
return Array.isArray(starredBoards) && starredBoards.length > 0;
|
||||
},
|
||||
starredBoards() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (!user) return [];
|
||||
const { starredBoards = [] } = user.profile || {};
|
||||
if (!Array.isArray(starredBoards) || starredBoards.length === 0) return [];
|
||||
return Boards.find({ _id: { $in: starredBoards } }, { sort: { sort: 1 } });
|
||||
},
|
||||
});
|
||||
|
||||
Template.bookmarksPopup.events({
|
||||
'click .js-toggle-star'(e) {
|
||||
e.preventDefault();
|
||||
const boardId = this._id;
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && boardId) {
|
||||
user.toggleBoardStar(boardId);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
.broken-cards-card-wrapper {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
border-width: 3px !important;
|
||||
margin-bottom: 1.3vh;
|
||||
border-width: 0.4vw !important;
|
||||
border-color: #808080 !important;
|
||||
border-style: solid;
|
||||
border-radius: 5px;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.7vw;
|
||||
padding: 2vh 2vw;
|
||||
background-color: #fff;
|
||||
}
|
||||
.broken-cards-wrapper {
|
||||
max-width: 500px;
|
||||
max-width: min(500px, 65vw);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.due-cards-dueat-list-wrapper {
|
||||
max-width: 500px;
|
||||
max-width: min(500px, 65vw);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
@@ -79,7 +79,6 @@ BlazeComponent.extendComponent({
|
||||
autosize($textarea);
|
||||
$textarea.escapeableTextComplete(mentions);
|
||||
};
|
||||
/*
|
||||
if (Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR === true || Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR === 'true') {
|
||||
const isSmall = Utils.isMiniScreen();
|
||||
const toolbar = isSmall
|
||||
@@ -117,7 +116,7 @@ BlazeComponent.extendComponent({
|
||||
].join('|');
|
||||
const badPatterns = new RegExp(
|
||||
`(?:${[
|
||||
`<(${badTags})s*[^>][\\s\\S]*?<\\/\\1>`,
|
||||
`<(${badTags})\\s*[^>][\\s\\S]*?<\\/\\1>`,
|
||||
`<(${badTags})[^>]*?\\/>`,
|
||||
].join('|')})`,
|
||||
'gi',
|
||||
@@ -128,9 +127,9 @@ BlazeComponent.extendComponent({
|
||||
// remove attributes ' style="..."'
|
||||
const badAttributes = new RegExp(
|
||||
`(?:${[
|
||||
'on\\S+=([\'"]?).*?\\1',
|
||||
'href=([\'"]?)javascript:.*?\\2',
|
||||
'style=([\'"]?).*?\\3',
|
||||
'on\\S+=([\'\"]?).*?\\1',
|
||||
'href=([\'\"]?)javascript:.*?\\2',
|
||||
'style=([\'\"]?).*?\\3',
|
||||
'target=\\S+',
|
||||
].join('|')})`,
|
||||
'gi',
|
||||
@@ -300,7 +299,6 @@ BlazeComponent.extendComponent({
|
||||
} else {
|
||||
enableTextarea();
|
||||
}
|
||||
*/
|
||||
enableTextarea();
|
||||
},
|
||||
events() {
|
||||
@@ -327,6 +325,7 @@ BlazeComponent.extendComponent({
|
||||
}).register('editor');
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
import { sanitizeHTML } from '/imports/lib/secureDOMPurify';
|
||||
|
||||
// Additional safeAttrValue function to allow for other specific protocols
|
||||
// See https://github.com/leizongmin/js-xss/issues/52#issuecomment-241354114
|
||||
@@ -373,9 +372,7 @@ Blaze.Template.registerHelper(
|
||||
let content = Blaze.toHTML(view.templateContentBlock);
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
if (!currentBoard)
|
||||
return HTML.Raw(
|
||||
DOMPurify.sanitize(content, { ALLOW_UNKNOWN_PROTOCOLS: true }),
|
||||
);
|
||||
return HTML.Raw(sanitizeHTML(content));
|
||||
const knowedUsers = _.union(currentBoard.members.map(member => {
|
||||
const u = ReactiveCache.getUser(member.userId);
|
||||
if (u) {
|
||||
@@ -419,9 +416,7 @@ Blaze.Template.registerHelper(
|
||||
content = content.replace(fullMention, Blaze.toHTML(link));
|
||||
}
|
||||
|
||||
return HTML.Raw(
|
||||
DOMPurify.sanitize(content, { ALLOW_UNKNOWN_PROTOCOLS: true }),
|
||||
);
|
||||
return HTML.Raw(sanitizeHTML(content));
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -99,7 +99,36 @@
|
||||
display: flex;
|
||||
z-index: 1000;
|
||||
padding: 10px 0px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap; /* Allow wrapping on mobile */
|
||||
min-height: 28px; /* Allow height to grow */
|
||||
}
|
||||
#header-quick-access .home-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#header-quick-access .home-icon a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
#header-quick-access .home-icon a:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
#header-quick-access .home-icon .fa-home {
|
||||
font-size: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#header-quick-access .allBoards {
|
||||
font-size: 14px;
|
||||
padding: 4px 15px;
|
||||
@@ -196,6 +225,197 @@
|
||||
margin: 4px 8px 0 0;
|
||||
float: left;
|
||||
}
|
||||
|
||||
/* Zoom Controls in Header */
|
||||
#header-quick-access .zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5vw;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 0.5vw;
|
||||
box-shadow: 0 0.2vh 0.5vh rgba(0,0,0,0.1);
|
||||
margin: 0 1vw;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls .board-header-btn {
|
||||
padding: 0.5vh 0.8vw !important;
|
||||
border-radius: 0.3vw !important;
|
||||
background: #fff !important;
|
||||
border: 1px solid #000 !important;
|
||||
transition: all 0.2s ease !important;
|
||||
color: #000 !important;
|
||||
height: auto !important;
|
||||
line-height: normal !important;
|
||||
margin: 0 !important;
|
||||
float: none !important;
|
||||
overflow: visible !important;
|
||||
text-decoration: none !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 0.3vw !important;
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls .board-header-btn i {
|
||||
color: #000 !important;
|
||||
float: none !important;
|
||||
display: inline !important;
|
||||
line-height: normal !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls .board-header-btn:hover {
|
||||
background: #000 !important;
|
||||
border-color: #000 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls .board-header-btn:hover i {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls .zoom-level {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
min-width: 3vw;
|
||||
text-align: center;
|
||||
font-size: clamp(12px, 2vw, 14px);
|
||||
cursor: pointer;
|
||||
padding: 0.3vh 0.5vw;
|
||||
border-radius: 0.3vw;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls .zoom-level:hover {
|
||||
background: #f0f0f0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls .zoom-display {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls .zoom-input {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.3vw;
|
||||
padding: 0.3vh 0.5vw;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
min-width: 3vw;
|
||||
font-size: clamp(12px, 2vw, 14px);
|
||||
}
|
||||
|
||||
/* Make zoom input wider on all mobile screens */
|
||||
@media screen and (max-width: 800px) {
|
||||
#header-quick-access .zoom-controls .zoom-input {
|
||||
min-width: 50px !important; /* Wider on mobile */
|
||||
width: 50px !important; /* Fixed width to show all numbers */
|
||||
font-size: 14px !important; /* Slightly larger text */
|
||||
}
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls .zoom-input:focus {
|
||||
outline: 2px solid #005fcc;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
/* Mobile Mode Toggle in Header */
|
||||
#header-quick-access .mobile-mode-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 1vw;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn {
|
||||
padding: 0.5vh 0.8vw !important;
|
||||
border-radius: 0.3vw !important;
|
||||
background: #fff !important;
|
||||
border: 1px solid #000 !important;
|
||||
transition: all 0.2s ease !important;
|
||||
color: #000 !important;
|
||||
height: auto !important;
|
||||
line-height: normal !important;
|
||||
margin: 0 !important;
|
||||
float: none !important;
|
||||
overflow: visible !important;
|
||||
text-decoration: none !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
gap: 6px !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn i {
|
||||
color: #666 !important;
|
||||
float: none !important;
|
||||
display: inline !important;
|
||||
line-height: normal !important;
|
||||
margin: 0 !important;
|
||||
transition: all 0.2s ease !important;
|
||||
font-size: clamp(14px, 2.8vw, 18px) !important;
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn i.active {
|
||||
color: #000 !important;
|
||||
font-weight: bold !important;
|
||||
transform: scale(1.1) !important;
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn:hover {
|
||||
background: #000 !important;
|
||||
border-color: #000 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn:hover i {
|
||||
color: #ccc !important;
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn:hover i.active {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn.mobile-active {
|
||||
background: #fff !important;
|
||||
border-color: #000 !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn.mobile-active i.mobile-icon {
|
||||
color: #000 !important;
|
||||
font-weight: bold !important;
|
||||
transform: scale(1.1) !important;
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn.mobile-active i.desktop-icon {
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn.desktop-active {
|
||||
background: #fff !important;
|
||||
border-color: #000 !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn.desktop-active i.mobile-icon {
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn.desktop-active i.desktop-icon {
|
||||
color: #000 !important;
|
||||
font-weight: bold !important;
|
||||
transform: scale(1.1) !important;
|
||||
}
|
||||
#header-quick-access #header-user-bar .header-user-bar-name i.fa-chevron-down {
|
||||
margin-right: 4px;
|
||||
}
|
||||
@@ -227,6 +447,650 @@
|
||||
width: 100%;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
/* Mobile home icon styling */
|
||||
#header-quick-access .home-icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
#header-quick-access .home-icon .fa-home {
|
||||
font-size: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#header-quick-access .home-icon a {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Ensure All Boards text is visible on mobile */
|
||||
#header-quick-access .home-icon.allBoards {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Adjust for very small screens */
|
||||
@media screen and (max-width: 480px) {
|
||||
#header-quick-access .home-icon a {
|
||||
font-size: 11px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
|
||||
#header-quick-access .home-icon .fa-home {
|
||||
font-size: 14px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile - make all text and icons 2x bigger above #content by default */
|
||||
@media screen and (max-width: 800px),
|
||||
screen and (max-device-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: landscape) {
|
||||
#header-quick-access {
|
||||
height: auto !important; /* Allow height to grow */
|
||||
min-height: 48px !important; /* Minimum height for mobile */
|
||||
flex-wrap: wrap !important; /* Force wrapping */
|
||||
align-items: flex-start !important; /* Align to top when wrapping */
|
||||
padding: 8px 0px !important; /* Adjust padding for mobile */
|
||||
}
|
||||
#header-quick-access {
|
||||
font-size: 2em !important; /* 2x bigger base font size */
|
||||
}
|
||||
|
||||
#header-quick-access * {
|
||||
font-size: inherit !important; /* Inherit the 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access .fa,
|
||||
#header-quick-access .icon {
|
||||
font-size: 2em !important; /* 2x bigger icons */
|
||||
}
|
||||
|
||||
#header-quick-access .home-icon a {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access .home-icon .fa-home {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls .zoom-level {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls .zoom-input {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn i {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
/* Mobile header wrapping and spacing */
|
||||
#header-quick-access .home-icon {
|
||||
flex-shrink: 0 !important;
|
||||
margin-right: 0.5rem !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
#header-quick-access #notifications {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
#header-quick-access #header-user-bar {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
#header-quick-access ul.header-quick-access-list {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 4px !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile All Boards page - make logo row elements 2x bigger */
|
||||
@media screen and (max-width: 800px),
|
||||
screen and (max-device-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: landscape) {
|
||||
.wrapper ~ #header-quick-access,
|
||||
body:not(.board-view) #header-quick-access {
|
||||
font-size: 2em !important; /* 2x bigger base font size for logo row */
|
||||
}
|
||||
|
||||
/* iPhone 12 Mini specific - 3x bigger for All Boards page */
|
||||
@media screen and (device-width: 375px) and (device-height: 812px), /* iPhone 12 Mini exact */
|
||||
screen and (max-width: 375px) and (max-height: 812px), /* iPhone 12 Mini viewport */
|
||||
screen and (-webkit-min-device-pixel-ratio: 3) and (max-width: 375px) /* iPhone 12 Mini Retina */ {
|
||||
.wrapper ~ #header-quick-access,
|
||||
body:not(.board-view) #header-quick-access {
|
||||
font-size: 3em !important; /* 3x bigger base font size for iPhone 12 Mini All Boards page */
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper ~ #header-quick-access *,
|
||||
body:not(.board-view) #header-quick-access * {
|
||||
font-size: inherit !important; /* Inherit the 2x scaling */
|
||||
}
|
||||
|
||||
.wrapper ~ #header-quick-access .fa,
|
||||
.wrapper ~ #header-quick-access .icon,
|
||||
body:not(.board-view) #header-quick-access .fa,
|
||||
body:not(.board-view) #header-quick-access .icon {
|
||||
font-size: 2em !important; /* 2x bigger icons in logo row */
|
||||
}
|
||||
|
||||
.wrapper ~ #header-quick-access .home-icon a,
|
||||
body:not(.board-view) #header-quick-access .home-icon a {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.wrapper ~ #header-quick-access .home-icon .fa-home,
|
||||
body:not(.board-view) #header-quick-access .home-icon .fa-home {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.wrapper ~ #header-quick-access .zoom-controls,
|
||||
body:not(.board-view) #header-quick-access .zoom-controls {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.wrapper ~ #header-quick-access .zoom-controls .zoom-level,
|
||||
body:not(.board-view) #header-quick-access .zoom-controls .zoom-level {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.wrapper ~ #header-quick-access .zoom-controls .zoom-input,
|
||||
body:not(.board-view) #header-quick-access .zoom-controls .zoom-input {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.wrapper ~ #header-quick-access .mobile-mode-toggle .board-header-btn,
|
||||
body:not(.board-view) #header-quick-access .mobile-mode-toggle .board-header-btn {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.wrapper ~ #header-quick-access .mobile-mode-toggle .board-header-btn i,
|
||||
body:not(.board-view) #header-quick-access .mobile-mode-toggle .board-header-btn i {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.wrapper ~ #header-quick-access #notifications,
|
||||
body:not(.board-view) #header-quick-access #notifications {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.wrapper ~ #header-quick-access #notifications .fa,
|
||||
body:not(.board-view) #header-quick-access #notifications .fa {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.wrapper ~ #header-quick-access #header-user-bar,
|
||||
body:not(.board-view) #header-quick-access #header-user-bar {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.wrapper ~ #header-quick-access #header-user-bar .fa,
|
||||
body:not(.board-view) #header-quick-access #header-user-bar .fa {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
}
|
||||
|
||||
/* iPhone 12 Mini specific - make header elements 3x bigger */
|
||||
@media screen and (device-width: 375px) and (device-height: 812px), /* iPhone 12 Mini exact */
|
||||
screen and (max-width: 375px) and (max-height: 812px), /* iPhone 12 Mini viewport */
|
||||
screen and (-webkit-min-device-pixel-ratio: 3) and (max-width: 375px), /* iPhone 12 Mini Retina */
|
||||
screen and (max-width: 375px) and (orientation: portrait), /* iPhone 12 Mini portrait */
|
||||
screen and (max-width: 375px) and (orientation: landscape) /* iPhone 12 Mini landscape */ {
|
||||
#header-quick-access {
|
||||
font-size: 3em !important; /* 3x bigger base font size for iPhone 12 Mini */
|
||||
height: auto !important; /* Allow height to grow */
|
||||
min-height: 84px !important; /* Much taller minimum height for iPhone 12 Mini */
|
||||
flex-wrap: wrap !important; /* Force wrapping */
|
||||
align-items: flex-start !important; /* Align to top when wrapping */
|
||||
padding: 18px 0px !important; /* More padding for iPhone 12 Mini */
|
||||
}
|
||||
|
||||
#header-quick-access * {
|
||||
font-size: inherit !important; /* Inherit the 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access .fa,
|
||||
#header-quick-access .icon {
|
||||
font-size: 3em !important; /* 3x bigger icons for iPhone 12 Mini */
|
||||
}
|
||||
|
||||
#header-quick-access .home-icon a {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access .home-icon .fa-home {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls .zoom-level {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls .zoom-input {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn i {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access #notifications {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access #notifications .fa {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access #header-user-bar {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access #header-user-bar .fa {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
/* iPhone 12 Mini header wrapping and spacing */
|
||||
#header-quick-access .home-icon {
|
||||
flex-shrink: 0 !important;
|
||||
margin-right: 0.5rem !important;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
#header-quick-access #notifications {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
#header-quick-access #header-user-bar {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
#header-quick-access ul.header-quick-access-list {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 6px !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* iPhone 12 Mini and very small screens - make header elements much larger */
|
||||
@media screen and (max-width: 400px) and (max-height: 900px),
|
||||
screen and (max-device-width: 400px) and (max-device-height: 900px),
|
||||
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 400px),
|
||||
screen and (max-width: 400px) and (orientation: portrait),
|
||||
screen and (max-width: 400px) and (orientation: landscape),
|
||||
screen and (max-width: 430px) and (max-height: 950px), /* iPhone 12 Mini range */
|
||||
screen and (max-width: 450px) and (max-height: 1000px), /* iPhone range */
|
||||
screen and (-webkit-min-device-pixel-ratio: 3) and (max-width: 450px), /* Retina displays */
|
||||
screen and (device-width: 375px) and (device-height: 812px), /* iPhone 12 Mini exact */
|
||||
screen and (device-width: 390px) and (device-height: 844px), /* iPhone 12/13 */
|
||||
screen and (device-width: 428px) and (device-height: 926px) /* iPhone 12 Pro Max */ {
|
||||
#header-quick-access {
|
||||
height: 40px !important; /* Taller header */
|
||||
padding: 12px 0px !important;
|
||||
}
|
||||
|
||||
#header-quick-access .home-icon a {
|
||||
font-size: 16px !important; /* Much larger text */
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
|
||||
#header-quick-access .home-icon .fa-home {
|
||||
font-size: 20px !important; /* Much larger icon */
|
||||
margin-right: 6px !important;
|
||||
}
|
||||
|
||||
#header-quick-access .home-icon {
|
||||
margin-right: 1rem !important;
|
||||
}
|
||||
|
||||
/* Make zoom controls larger */
|
||||
#header-quick-access .zoom-controls {
|
||||
padding: 0.8vh 1.5vw !important;
|
||||
margin: 0 1.5vw !important;
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls .zoom-level {
|
||||
font-size: 16px !important; /* Larger zoom text */
|
||||
padding: 0.5vh 0.8vw !important;
|
||||
min-width: 4vw !important;
|
||||
}
|
||||
|
||||
#header-quick-access .zoom-controls .zoom-input {
|
||||
font-size: 16px !important; /* Larger input text */
|
||||
padding: 0.5vh 0.8vw !important;
|
||||
min-width: 6vw !important; /* Much wider for mobile */
|
||||
width: 60px !important; /* Fixed width to show all numbers */
|
||||
}
|
||||
|
||||
/* Make mobile mode toggle larger */
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn {
|
||||
padding: 0.8vh 1.2vw !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
#header-quick-access .mobile-mode-toggle .board-header-btn i {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback for iPhone devices using JavaScript detection */
|
||||
.iphone-device #header-quick-access {
|
||||
font-size: 2em !important; /* 2x bigger base font size */
|
||||
height: auto !important; /* Allow height to grow */
|
||||
min-height: 48px !important; /* Minimum height for mobile */
|
||||
flex-wrap: wrap !important; /* Force wrapping */
|
||||
align-items: flex-start !important; /* Align to top when wrapping */
|
||||
padding: 8px 0px !important; /* Adjust padding for mobile */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access * {
|
||||
font-size: inherit !important; /* Inherit the 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .fa,
|
||||
.iphone-device #header-quick-access .icon {
|
||||
font-size: 2em !important; /* 2x bigger icons */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .home-icon a {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .home-icon .fa-home {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .zoom-controls {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .zoom-controls .zoom-level {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .zoom-controls .zoom-input {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .mobile-mode-toggle .board-header-btn {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .mobile-mode-toggle .board-header-btn i {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
/* iPhone device header wrapping and spacing */
|
||||
.iphone-device #header-quick-access .home-icon {
|
||||
flex-shrink: 0 !important;
|
||||
margin-right: 0.5rem !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .zoom-controls {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .mobile-mode-toggle {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access #notifications {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access #header-user-bar {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access ul.header-quick-access-list {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 4px !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
/* iPhone 12 Mini specific - JavaScript detection fallback */
|
||||
.iphone-device #header-quick-access {
|
||||
font-size: 3em !important; /* 3x bigger base font size for iPhone 12 Mini */
|
||||
height: auto !important; /* Allow height to grow */
|
||||
min-height: 84px !important; /* Much taller minimum height for iPhone 12 Mini */
|
||||
flex-wrap: wrap !important; /* Force wrapping */
|
||||
align-items: flex-start !important; /* Align to top when wrapping */
|
||||
padding: 18px 0px !important; /* More padding for iPhone 12 Mini */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access * {
|
||||
font-size: inherit !important; /* Inherit the 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .fa,
|
||||
.iphone-device #header-quick-access .icon {
|
||||
font-size: 3em !important; /* 3x bigger icons for iPhone 12 Mini */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .home-icon a {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .home-icon .fa-home {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .zoom-controls {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .zoom-controls .zoom-level {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .zoom-controls .zoom-input {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .mobile-mode-toggle .board-header-btn {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .mobile-mode-toggle .board-header-btn i {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access #notifications {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access #notifications .fa {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access #header-user-bar {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access #header-user-bar .fa {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
/* iPhone 12 Mini header wrapping and spacing - JavaScript fallback */
|
||||
.iphone-device #header-quick-access .home-icon {
|
||||
flex-shrink: 0 !important;
|
||||
margin-right: 0.5rem !important;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .zoom-controls {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access .mobile-mode-toggle {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access #notifications {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access #header-user-bar {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
.iphone-device #header-quick-access ul.header-quick-access-list {
|
||||
flex-shrink: 0 !important;
|
||||
margin: 0 0.5rem !important;
|
||||
margin-bottom: 6px !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
/* iPhone 12 Mini All Boards page - make logo row elements 3x bigger */
|
||||
.iphone-device .wrapper ~ #header-quick-access,
|
||||
.iphone-device body:not(.board-view) #header-quick-access {
|
||||
font-size: 3em !important; /* 3x bigger base font size for logo row */
|
||||
}
|
||||
|
||||
.iphone-device .wrapper ~ #header-quick-access *,
|
||||
.iphone-device body:not(.board-view) #header-quick-access * {
|
||||
font-size: inherit !important; /* Inherit the 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .wrapper ~ #header-quick-access .fa,
|
||||
.iphone-device .wrapper ~ #header-quick-access .icon,
|
||||
.iphone-device body:not(.board-view) #header-quick-access .fa,
|
||||
.iphone-device body:not(.board-view) #header-quick-access .icon {
|
||||
font-size: 2em !important; /* 2x bigger icons in logo row */
|
||||
}
|
||||
|
||||
.iphone-device .wrapper ~ #header-quick-access .home-icon a,
|
||||
.iphone-device body:not(.board-view) #header-quick-access .home-icon a {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .wrapper ~ #header-quick-access .home-icon .fa-home,
|
||||
.iphone-device body:not(.board-view) #header-quick-access .home-icon .fa-home {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .wrapper ~ #header-quick-access .zoom-controls,
|
||||
.iphone-device body:not(.board-view) #header-quick-access .zoom-controls {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .wrapper ~ #header-quick-access .zoom-controls .zoom-level,
|
||||
.iphone-device body:not(.board-view) #header-quick-access .zoom-controls .zoom-level {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .wrapper ~ #header-quick-access .zoom-controls .zoom-input,
|
||||
.iphone-device body:not(.board-view) #header-quick-access .zoom-controls .zoom-input {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .wrapper ~ #header-quick-access .mobile-mode-toggle .board-header-btn,
|
||||
.iphone-device body:not(.board-view) #header-quick-access .mobile-mode-toggle .board-header-btn {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .wrapper ~ #header-quick-access .mobile-mode-toggle .board-header-btn i,
|
||||
.iphone-device body:not(.board-view) #header-quick-access .mobile-mode-toggle .board-header-btn i {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .wrapper ~ #header-quick-access #notifications,
|
||||
.iphone-device body:not(.board-view) #header-quick-access #notifications {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .wrapper ~ #header-quick-access #notifications .fa,
|
||||
.iphone-device body:not(.board-view) #header-quick-access #notifications .fa {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .wrapper ~ #header-quick-access #header-user-bar,
|
||||
.iphone-device body:not(.board-view) #header-quick-access #header-user-bar {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
.iphone-device .wrapper ~ #header-quick-access #header-user-bar .fa,
|
||||
.iphone-device body:not(.board-view) #header-quick-access #header-user-bar .fa {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
#header-quick-access ul {
|
||||
width: calc(100% - 60px);
|
||||
margin-right: 10px;
|
||||
@@ -237,12 +1101,6 @@
|
||||
#header-quick-access ul li a {
|
||||
height: 100%;
|
||||
}
|
||||
#header-quick-access span .fa-home {
|
||||
font-size: 26px;
|
||||
margin-top: -2px;
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
#header-quick-access #header-new-board-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,31 @@ template(name="header")
|
||||
The first link goes to the boards page.
|
||||
if currentUser
|
||||
#header-quick-access(class=currentBoard.colorClass)
|
||||
// Home icon - always at left side of logo
|
||||
span.home-icon.allBoards
|
||||
a(href="{{pathFor 'home'}}")
|
||||
span.fa.fa-home
|
||||
| {{_ 'all-boards'}}
|
||||
|
||||
// Logo - visible; on mobile constrained by CSS
|
||||
unless currentSetting.hideLogo
|
||||
if currentSetting.customTopLeftCornerLogoImageUrl
|
||||
if currentSetting.customTopLeftCornerLogoLinkUrl
|
||||
a(href="{{currentSetting.customTopLeftCornerLogoLinkUrl}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
|
||||
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" height="{{#if currentSetting.customTopLeftCornerLogoHeight}}#{currentSetting.customTopLeftCornerLogoHeight}{{else}}27{{/if}}" width="auto" margin="0" padding="0")
|
||||
unless currentSetting.customTopLeftCornerLogoLinkUrl
|
||||
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" height="{{#if currentSetting.customTopLeftCornerLogoHeight}}#{currentSetting.customTopLeftCornerLogoHeight}{{else}}27{{/if}}" width="auto" margin="0" padding="0" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
|
||||
unless currentSetting.customTopLeftCornerLogoImageUrl
|
||||
div#headerIsSettingDatabaseCallDone
|
||||
img(src="{{pathFor '/logo-header.png'}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
|
||||
|
||||
// Zoom controls - always visible
|
||||
.zoom-controls
|
||||
span.zoom-level.js-zoom-level-click(title="{{_ 'click-to-change-zoom'}}")
|
||||
span.zoom-display {{zoomLevel}}%
|
||||
input.zoom-input.js-zoom-input(type="number" value=zoomLevel min="50" max="300" step="10" style="display: none;")
|
||||
|
||||
if isMiniScreen
|
||||
span
|
||||
a(href="{{pathFor 'home'}}")
|
||||
span.fa.fa-home
|
||||
ul.header-quick-access-list
|
||||
if currentList
|
||||
each currentBoard.lists
|
||||
@@ -23,32 +44,14 @@ template(name="header")
|
||||
a(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
+viewer
|
||||
= title
|
||||
a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}")
|
||||
i.fa.fa-arrows
|
||||
if isShowDesktopDragHandles
|
||||
i.fa.fa-check-square-o
|
||||
unless isShowDesktopDragHandles
|
||||
i.fa.fa-ban
|
||||
//a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}")
|
||||
// i.fa.fa-arrows
|
||||
// if isShowDesktopDragHandles
|
||||
// i.fa.fa-check-square-o
|
||||
// unless isShowDesktopDragHandles
|
||||
// i.fa.fa-ban
|
||||
#header-new-board-icon
|
||||
else
|
||||
//-
|
||||
On sandstorm, the logo shouldn't be clickable, because we only have one
|
||||
page/document on it, and we don't want to see the home page containing
|
||||
the list of all boards.
|
||||
unless currentSetting.hideLogo
|
||||
if currentSetting.customTopLeftCornerLogoImageUrl
|
||||
if currentSetting.customTopLeftCornerLogoLinkUrl
|
||||
a(href="{{currentSetting.customTopLeftCornerLogoLinkUrl}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
|
||||
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" height="{{#if currentSetting.customTopLeftCornerLogoHeight}}#{currentSetting.customTopLeftCornerLogoHeight}{{else}}27{{/if}}" width="auto" margin="0" padding="0")
|
||||
unless currentSetting.customTopLeftCornerLogoLinkUrl
|
||||
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" height="{{#if currentSetting.customTopLeftCornerLogoHeight}}#{currentSetting.customTopLeftCornerLogoHeight}{{else}}27{{/if}}" width="auto" margin="0" padding="0" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
|
||||
unless currentSetting.customTopLeftCornerLogoImageUrl
|
||||
div#headerIsSettingDatabaseCallDone
|
||||
img(src="{{pathFor '/logo-header.png'}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
|
||||
span.allBoards
|
||||
a(href="{{pathFor 'home'}}")
|
||||
span.fa.fa-home
|
||||
| {{_ 'all-boards'}}
|
||||
ul.header-quick-access-list
|
||||
//li
|
||||
// a(href="{{pathFor 'public'}}")
|
||||
@@ -61,12 +64,12 @@ template(name="header")
|
||||
= title
|
||||
else
|
||||
li.current.empty {{_ 'quick-access-description'}}
|
||||
a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}")
|
||||
i.fa.fa-arrows
|
||||
if isShowDesktopDragHandles
|
||||
i.fa.fa-check-square-o
|
||||
unless isShowDesktopDragHandles
|
||||
i.fa.fa-ban
|
||||
//a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}")
|
||||
// i.fa.fa-arrows
|
||||
// if isShowDesktopDragHandles
|
||||
// i.fa.fa-check-square-o
|
||||
// unless isShowDesktopDragHandles
|
||||
// i.fa.fa-ban
|
||||
// Next line is used only for spacing at header,
|
||||
// there is no visible clickable icon.
|
||||
#header-new-board-icon
|
||||
@@ -75,6 +78,16 @@ template(name="header")
|
||||
//a#header-new-board-icon.js-create-board
|
||||
// i.fa.fa-plus(title="Create a new board")
|
||||
|
||||
.mobile-mode-toggle
|
||||
a.board-header-btn.js-mobile-mode-toggle(title="{{_ 'mobile-desktop-toggle'}}" class="{{#if mobileMode}}mobile-active{{else}}desktop-active{{/if}}")
|
||||
i.fa.fa-mobile.mobile-icon(class="{{#if mobileMode}}active{{/if}}")
|
||||
i.fa.fa-desktop.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'}}")
|
||||
i.fa.fa-bookmark
|
||||
|
||||
// Notifications
|
||||
+notifications
|
||||
|
||||
if currentSetting.customHelpLinkUrl
|
||||
|
||||
@@ -44,10 +44,69 @@ Template.header.helpers({
|
||||
const announcements = Announcements.findOne();
|
||||
return announcements && announcements.body;
|
||||
},
|
||||
|
||||
zoomLevel() {
|
||||
const sessionZoom = Session.get('wekan-zoom-level');
|
||||
if (sessionZoom !== undefined) {
|
||||
return Math.round(sessionZoom * 100);
|
||||
}
|
||||
return Math.round(Utils.getZoomLevel() * 100);
|
||||
},
|
||||
|
||||
mobileMode() {
|
||||
const sessionMode = Session.get('wekan-mobile-mode');
|
||||
if (sessionMode !== undefined) {
|
||||
return sessionMode;
|
||||
}
|
||||
return Utils.getMobileMode();
|
||||
},
|
||||
});
|
||||
|
||||
Template.header.events({
|
||||
'click .js-create-board': Popup.open('headerBarCreateBoard'),
|
||||
'click .js-zoom-level-click'(evt) {
|
||||
const $zoomDisplay = $(evt.currentTarget).find('.zoom-display');
|
||||
const $zoomInput = $(evt.currentTarget).find('.zoom-input');
|
||||
|
||||
// Hide display, show input
|
||||
$zoomDisplay.hide();
|
||||
$zoomInput.show().focus().select();
|
||||
},
|
||||
|
||||
'keypress .js-zoom-input'(evt) {
|
||||
if (evt.which === 13) { // Enter key
|
||||
const newZoomPercent = parseInt(evt.target.value);
|
||||
|
||||
if (!isNaN(newZoomPercent) && newZoomPercent >= 50 && newZoomPercent <= 300) {
|
||||
const newZoom = newZoomPercent / 100;
|
||||
Utils.setZoomLevel(newZoom);
|
||||
|
||||
// Hide input, show display
|
||||
const $zoomDisplay = $(evt.target).siblings('.zoom-display');
|
||||
const $zoomInput = $(evt.target);
|
||||
$zoomInput.hide();
|
||||
$zoomDisplay.show();
|
||||
} else {
|
||||
alert('Please enter a zoom level between 50% and 300%');
|
||||
evt.target.focus().select();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'blur .js-zoom-input'(evt) {
|
||||
// When input loses focus, hide it and show display
|
||||
const $zoomDisplay = $(evt.target).siblings('.zoom-display');
|
||||
const $zoomInput = $(evt.target);
|
||||
$zoomInput.hide();
|
||||
$zoomDisplay.show();
|
||||
},
|
||||
'click .js-mobile-mode-toggle'() {
|
||||
const currentMode = Utils.getMobileMode();
|
||||
Utils.setMobileMode(!currentMode);
|
||||
},
|
||||
'click .js-open-bookmarks'(evt) {
|
||||
// Already added but ensure single definition -- safe guard
|
||||
},
|
||||
'click .js-close-announcement'() {
|
||||
$('.announcement').hide();
|
||||
},
|
||||
@@ -68,6 +127,14 @@ Template.header.events({
|
||||
location.reload();
|
||||
}
|
||||
},
|
||||
'click .js-open-bookmarks'(evt) {
|
||||
// Desktop: open popup, Mobile: route to page
|
||||
if (Utils.isMiniScreen()) {
|
||||
FlowRouter.go('bookmarks');
|
||||
} else {
|
||||
Popup.open('bookmarksPopup')(evt);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.offlineWarning.events({
|
||||
|
||||
@@ -52,9 +52,15 @@ input,
|
||||
select,
|
||||
textarea,
|
||||
button {
|
||||
font: 14px Roboto, Poppins, "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
line-height: 18px;
|
||||
font: clamp(14px, 2.5vw, 18px) Roboto, Poppins, "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
line-height: 1.4;
|
||||
color: #4d4d4d;
|
||||
/* Improve text rendering */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
/* Better text selection */
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
html {
|
||||
font-size: 100%;
|
||||
@@ -85,8 +91,8 @@ body {
|
||||
margin-top: 30vh;
|
||||
}
|
||||
#content > .wrapper {
|
||||
margin-top: 10px;
|
||||
padding: 15px;
|
||||
margin-top: 1vh;
|
||||
padding: 2vh 2vw;
|
||||
}
|
||||
#modal {
|
||||
position: absolute;
|
||||
@@ -99,56 +105,56 @@ body {
|
||||
overflow-y: auto;
|
||||
}
|
||||
#modal .modal-content {
|
||||
width: 500px;
|
||||
min-height: 160px;
|
||||
margin: 42px auto;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
width: min(500px, 90vw);
|
||||
min-height: 20vh;
|
||||
margin: 5vh auto;
|
||||
padding: 2vh 2vw;
|
||||
border-radius: 0.5vw;
|
||||
background: #dedede;
|
||||
z-index: 110;
|
||||
}
|
||||
#modal .modal-content h2 {
|
||||
margin-bottom: 25px;
|
||||
margin-bottom: 3vh;
|
||||
}
|
||||
#modal .modal-content .modal-close-btn {
|
||||
display: block;
|
||||
float: right;
|
||||
font-size: 24px;
|
||||
font-size: clamp(18px, 4vw, 24px);
|
||||
}
|
||||
#modal .modal-content-wide {
|
||||
width: 800px;
|
||||
width: min(800px, 95vw);
|
||||
min-height: 0px;
|
||||
margin: 42px auto;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin: 5vh auto;
|
||||
padding: 2vh 2vw;
|
||||
border-radius: 0.5vw;
|
||||
background: #dedede;
|
||||
z-index: 110;
|
||||
}
|
||||
#modal .modal-content-wide h2 {
|
||||
margin-bottom: 25px;
|
||||
margin-bottom: 3vh;
|
||||
}
|
||||
#modal .modal-content-wide .modal-close-btn {
|
||||
display: block;
|
||||
float: right;
|
||||
font-size: 24px;
|
||||
font-size: clamp(18px, 4vw, 24px);
|
||||
}
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
font-size: clamp(18px, 4vw, 24px);
|
||||
line-height: 1.2em;
|
||||
margin: 0 0 10px;
|
||||
margin: 0 0 1vh;
|
||||
}
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-size: clamp(16px, 3.5vw, 20px);
|
||||
line-height: 1.2em;
|
||||
margin: 0 0 9px;
|
||||
margin: 0 0 0.8vh;
|
||||
}
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 16px;
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
line-height: 1.25em;
|
||||
margin: 0 0 6px;
|
||||
margin: 0 0 0.6vh;
|
||||
}
|
||||
.quiet,
|
||||
.quiet a {
|
||||
@@ -160,14 +166,14 @@ h6 {
|
||||
}
|
||||
.no-items-message {
|
||||
color: #666;
|
||||
margin: 30px 0;
|
||||
margin: 4vh 0;
|
||||
text-align: center;
|
||||
}
|
||||
.warning {
|
||||
background: #f0ecdb;
|
||||
border-radius: 3px;
|
||||
border-radius: 0.4vw;
|
||||
color: #aa8f09;
|
||||
padding: 6px 8px;
|
||||
padding: 1vh 1.2vw;
|
||||
}
|
||||
.warning a {
|
||||
color: #aa8f09;
|
||||
@@ -194,7 +200,7 @@ strong {
|
||||
p {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
|
||||
|
||||
|
||||
}
|
||||
p a {
|
||||
@@ -203,12 +209,12 @@ p a {
|
||||
}
|
||||
table,
|
||||
p {
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
pre {
|
||||
margin: 15px 0;
|
||||
margin: 2vh 0;
|
||||
white-space: pre;
|
||||
max-height: 516px;
|
||||
max-height: 60vh;
|
||||
}
|
||||
pre,
|
||||
code,
|
||||
@@ -217,10 +223,10 @@ tt {
|
||||
line-height: 1.3em;
|
||||
}
|
||||
blockquote {
|
||||
margin: 8px 0 8px 8px;
|
||||
margin: 1vh 0 1vh 1vw;
|
||||
border-left: 1px solid #ccc;
|
||||
color: #666;
|
||||
padding: 0 0 0 8px;
|
||||
padding: 0 0 0 1vw;
|
||||
}
|
||||
hr {
|
||||
height: 1px;
|
||||
@@ -229,7 +235,7 @@ hr {
|
||||
width: 100%;
|
||||
background: #dbdbdb;
|
||||
color: #dbdbdb;
|
||||
margin: 15px 0;
|
||||
margin: 2vh 0;
|
||||
padding: 0;
|
||||
}
|
||||
table,
|
||||
@@ -241,7 +247,7 @@ th {
|
||||
}
|
||||
td,
|
||||
th {
|
||||
padding: 5px;
|
||||
padding: 0.8vh 0.8vw;
|
||||
border-right: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
@@ -257,20 +263,20 @@ tbody {
|
||||
}
|
||||
dl,
|
||||
dt {
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
dd {
|
||||
margin: 0 0 16px 24px;
|
||||
margin: 0 0 2vh 3vw;
|
||||
}
|
||||
kbd {
|
||||
padding: 1px 3px;
|
||||
margin: 3px;
|
||||
padding: 0.2vh 0.5vw;
|
||||
margin: 0.5vh;
|
||||
font-weight: bold;
|
||||
background: #fafafa;
|
||||
border-radius: 3px;
|
||||
border-radius: 0.4vw;
|
||||
border: 1px solid #e6e6e6;
|
||||
color: unset;
|
||||
box-shadow: 0px 2px 3px rgba(0,0,0,0.15);
|
||||
box-shadow: 0px 0.3vh 0.5vh rgba(0,0,0,0.15);
|
||||
}
|
||||
.clear {
|
||||
clear: both;
|
||||
@@ -310,7 +316,7 @@ kbd {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.wrapper {
|
||||
width: calc(100% - 20px);
|
||||
width: calc(100% - 2vw);
|
||||
margin: 0 auto;
|
||||
}
|
||||
.relative {
|
||||
@@ -401,23 +407,23 @@ a:not(.disabled).is-active i.fa {
|
||||
}
|
||||
.big-message {
|
||||
display: block;
|
||||
margin: 75px auto;
|
||||
margin: 10vh auto;
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
max-width: min(600px, 90vw);
|
||||
}
|
||||
.big-message h1 {
|
||||
font-size: 26px;
|
||||
margin-bottom: 24px;
|
||||
font-size: clamp(20px, 5vw, 28px);
|
||||
margin-bottom: 3vh;
|
||||
}
|
||||
.big-message p {
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
font-size: clamp(16px, 3.5vw, 20px);
|
||||
line-height: 1.3;
|
||||
}
|
||||
.gutter {
|
||||
margin-left: 38px;
|
||||
margin-left: 5vw;
|
||||
}
|
||||
.viewer {
|
||||
min-height: 18px;
|
||||
min-height: 2.5vh;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
@@ -427,13 +433,13 @@ a:not(.disabled).is-active i.fa {
|
||||
}
|
||||
.viewer ol {
|
||||
list-style-type: decimal;
|
||||
padding-left: 20px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 3vw;
|
||||
padding-bottom: 1.5vh;
|
||||
}
|
||||
.viewer ul {
|
||||
list-style-type: initial;
|
||||
padding-left: 20px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 3vw;
|
||||
padding-bottom: 1.5vh;
|
||||
}
|
||||
.viewer em {
|
||||
font-style: italic;
|
||||
@@ -460,20 +466,291 @@ a:not(.disabled).is-active i.fa {
|
||||
.no-scrollbars::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
/* ========================================
|
||||
MOBILE & TABLET RESPONSIVE IMPROVEMENTS
|
||||
======================================== */
|
||||
|
||||
/* Mobile devices (up to 800px) */
|
||||
@media screen and (max-width: 800px) {
|
||||
#content {
|
||||
margin: 1px 0px 0px 0px;
|
||||
height: calc(100% - 0px);
|
||||
/* Improve touch scrolling */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
#content > .wrapper {
|
||||
margin-top: 0px;
|
||||
padding: 8px;
|
||||
}
|
||||
.wrapper {
|
||||
height: calc(100% - 31px);
|
||||
margin: 0px;
|
||||
padding: 8px;
|
||||
}
|
||||
.panel-default {
|
||||
width: 83vw;
|
||||
width: 95vw;
|
||||
max-width: 95vw;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Improve touch targets */
|
||||
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px; /* Prevent zoom on iOS */
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
input, select, textarea {
|
||||
font-size: 16px; /* Prevent zoom on iOS */
|
||||
padding: 12px;
|
||||
min-height: 44px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Cards and lists */
|
||||
.minicard {
|
||||
min-height: 48px;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0 8px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
/* Board canvas */
|
||||
.board-canvas {
|
||||
padding: 8px;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Header mobile layout */
|
||||
#header {
|
||||
padding: 8px;
|
||||
/* Keep top bar on a single row on small screens */
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#header-quick-access {
|
||||
/* Keep quick-access items in one row */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Hide elements that should move to the hamburger menu on mobile */
|
||||
#header-quick-access .header-quick-access-list,
|
||||
#header-quick-access #header-help {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Show only the home icon (hide the trailing text) on mobile */
|
||||
#header-quick-access .home-icon a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 28px; /* enough to display the icon */
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Hide text in home icon on mobile, show only icon */
|
||||
#header-quick-access .home-icon a span:not(.fa) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Ensure proper spacing for mobile header elements */
|
||||
#header-quick-access .zoom-controls {
|
||||
margin-left: auto;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.mobile-mode-toggle {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#header-user-bar {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Ensure header elements don't wrap on very small screens */
|
||||
#header-quick-access {
|
||||
min-width: 0; /* Allow flexbox to shrink */
|
||||
}
|
||||
|
||||
/* Make sure logo doesn't take too much space on mobile */
|
||||
#header-quick-access img {
|
||||
max-height: 24px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
/* Ensure zoom controls are compact on mobile */
|
||||
.zoom-controls .zoom-level {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Modal mobile optimization */
|
||||
#modal .modal-content,
|
||||
#modal .modal-content-wide {
|
||||
width: 95vw;
|
||||
max-width: 95vw;
|
||||
margin: 2vh auto;
|
||||
padding: 16px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Table mobile optimization */
|
||||
table {
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Admin panel mobile optimization */
|
||||
.setting-content .content-body {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.setting-content .content-body .side-menu {
|
||||
width: 100%;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.setting-content .content-body .main-body {
|
||||
order: 1;
|
||||
min-height: 60vh;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet devices (768px - 1024px) */
|
||||
@media screen and (min-width: 768px) and (max-width: 1024px) {
|
||||
#content > .wrapper {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.panel-default {
|
||||
width: 90vw;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
/* Touch-friendly but more compact */
|
||||
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
|
||||
min-height: 48px;
|
||||
min-width: 48px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.minicard {
|
||||
min-height: 40px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0 12px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.board-canvas {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
#modal .modal-content {
|
||||
width: 80vw;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
#modal .modal-content-wide {
|
||||
width: 90vw;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.setting-content .content-body {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.setting-content .content-body .side-menu {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Large displays and digital signage (1920px+) */
|
||||
@media screen and (min-width: 1920px) {
|
||||
body {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
|
||||
min-height: 56px;
|
||||
min-width: 56px;
|
||||
padding: 16px 20px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.minicard {
|
||||
min-height: 56px;
|
||||
padding: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0 20px;
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
.board-canvas {
|
||||
padding: 20px;
|
||||
max-width: 2400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#header {
|
||||
padding: 20px 32px;
|
||||
max-width: 2400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#modal .modal-content {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
#modal .modal-content-wide {
|
||||
width: 1000px;
|
||||
}
|
||||
|
||||
.setting-content .content-body {
|
||||
gap: 32px;
|
||||
max-width: 2400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.setting-content .content-body .side-menu {
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
.inline-input {
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
head
|
||||
title
|
||||
meta(name="viewport" content="width=device-width, initial-scale=1")
|
||||
meta(http-equiv="X-UA-Compatible" content="IE=edge")
|
||||
//- XXX We should use pathFor in the following `href` to support the case
|
||||
where the application is deployed with a path prefix, but it seems to be
|
||||
difficult to do that cleanly with Blaze -- at least without adding extra
|
||||
packages.
|
||||
link(rel="shortcut icon" type="image/x-icon" href="/favicon.ico")
|
||||
link(rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png")
|
||||
link(rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png")
|
||||
link(rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png")
|
||||
link(rel="manifest" crossOrigin="use-credentials" href="/site.webmanifest")
|
||||
link(rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5")
|
||||
meta(name="apple-mobile-web-app-title" content="Wekan")
|
||||
meta(name="application-name" content="Wekan")
|
||||
meta(name="msapplication-TileColor" content="#00aba9")
|
||||
meta(name="theme-color" content="#ffffff")
|
||||
//link(rel="stylesheet" type="text/css" class="__meteor-css__" href="css/html5-default-theme.css")
|
||||
template(name="main")
|
||||
html(lang="{{TAPi18n.getLanguage}}")
|
||||
head
|
||||
title
|
||||
meta(name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes")
|
||||
meta(http-equiv="X-UA-Compatible" content="IE=edge")
|
||||
//- XXX We should use pathFor in the following `href` to support the case
|
||||
where the application is deployed with a path prefix, but it seems to be
|
||||
difficult to do that cleanly with Blaze -- at least without adding extra
|
||||
packages.
|
||||
link(rel="shortcut icon" type="image/x-icon" href="/favicon.ico")
|
||||
link(rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png")
|
||||
link(rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png")
|
||||
link(rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png")
|
||||
link(rel="manifest" crossOrigin="use-credentials" href="/site.webmanifest")
|
||||
link(rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5")
|
||||
meta(name="apple-mobile-web-app-title" content="Wekan")
|
||||
meta(name="application-name" content="Wekan")
|
||||
meta(name="msapplication-TileColor" content="#00aba9")
|
||||
meta(name="theme-color" content="#ffffff")
|
||||
//link(rel="stylesheet" type="text/css" class="__meteor-css__" href="css/html5-default-theme.css")
|
||||
|
||||
template(name="userFormsLayout")
|
||||
section.auth-layout
|
||||
@@ -48,6 +50,8 @@ template(name="userFormsLayout")
|
||||
if isLoading
|
||||
+loader
|
||||
else
|
||||
// ARIA live region for error messages
|
||||
div#login-error-message(role="alert" aria-live="assertive" style="color: #d32f2f; margin-bottom: 1em;")
|
||||
+Template.dynamic(template=content)
|
||||
if currentSetting.displayAuthenticationMethod
|
||||
+connectionMethod(authenticationMethod=currentSetting.defaultAuthenticationMethod)
|
||||
@@ -59,7 +63,8 @@ template(name="userFormsLayout")
|
||||
if getLegalNoticeWithWritTraduction
|
||||
div
|
||||
div.at-form-lang
|
||||
select.select-lang.js-userform-set-language
|
||||
label(for="userform-set-language-select") {{_ 'changeLanguagePopup-title'}}
|
||||
select.select-lang.js-userform-set-language#userform-set-language-select(aria-label="{{_ 'changeLanguagePopup-title'}}")
|
||||
each languages
|
||||
if isCurrentLanguage
|
||||
option(value="{{tag}}" selected="selected") {{name}}
|
||||
@@ -72,6 +77,8 @@ template(name="defaultLayout")
|
||||
| {{{afterBodyStart}}}
|
||||
+Template.dynamic(template=content)
|
||||
| {{{beforeBodyEnd}}}
|
||||
+migrationProgress
|
||||
+boardConversionProgress
|
||||
if (Modal.isOpen)
|
||||
#modal
|
||||
.overlay
|
||||
|
||||
@@ -86,6 +86,12 @@ Template.userFormsLayout.onRendered(() => {
|
||||
validator,
|
||||
);
|
||||
EscapeActions.executeAll();
|
||||
|
||||
// Add autocomplete attribute to login input for WCAG compliance
|
||||
const loginInput = document.querySelector('input[type="text"], input[type="email"]');
|
||||
if (loginInput && loginInput.name && (loginInput.name.toLowerCase().includes('user') || loginInput.name.toLowerCase().includes('email'))) {
|
||||
loginInput.setAttribute('autocomplete', 'username email');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
.my-cards-board-wrapper {
|
||||
border-radius: 0 0 4px 4px;
|
||||
min-width: 400px;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0 0 0.5vw 0.5vw;
|
||||
min-width: min(400px, 52vw);
|
||||
margin-bottom: 2.5vh;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
border-width: 2px;
|
||||
border-width: 0.3vw;
|
||||
border-style: solid;
|
||||
border-color: #a2a2a2;
|
||||
}
|
||||
.my-cards-board-title {
|
||||
font-size: 1.4rem;
|
||||
font-size: clamp(1.2rem, 3vw, 1.6rem);
|
||||
font-weight: bold;
|
||||
padding: 0.5rem;
|
||||
padding: 0.7vh 0.7vw;
|
||||
background-color: #808080;
|
||||
color: #fff;
|
||||
}
|
||||
.my-cards-swimlane-title {
|
||||
font-size: 1.1rem;
|
||||
font-size: clamp(1rem, 2.5vw, 1.3rem);
|
||||
font-weight: bold;
|
||||
padding: 0.5rem;
|
||||
padding-bottom: 0.4rem;
|
||||
padding: 0.7vh 0.7vw;
|
||||
padding-bottom: 0.5vh;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 0.7vh;
|
||||
text-align: center;
|
||||
}
|
||||
.swimlane-default-color {
|
||||
@@ -29,23 +29,23 @@
|
||||
}
|
||||
.my-cards-list-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
font-size: clamp(1rem, 2.5vw, 1.3rem);
|
||||
text-align: center;
|
||||
margin-bottom: 0.7rem;
|
||||
margin-bottom: 0.9vh;
|
||||
}
|
||||
.my-cards-list-wrapper {
|
||||
margin: 1rem;
|
||||
border-radius: 5px;
|
||||
margin: 1.3vh 1.3vw;
|
||||
border-radius: 0.7vw;
|
||||
display: inline-grid;
|
||||
min-width: 250px;
|
||||
max-width: 350px;
|
||||
min-width: min(250px, 32vw);
|
||||
max-width: min(350px, 45vw);
|
||||
}
|
||||
.my-cards-card-wrapper {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 1.3vh;
|
||||
}
|
||||
.my-cards-dueat-list-wrapper {
|
||||
max-width: 500px;
|
||||
max-width: min(500px, 65vw);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
.pop-over {
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
border-radius: 0.4vw;
|
||||
border: 1px solid #dbdbdb;
|
||||
border-bottom-color: #c2c2c2;
|
||||
box-shadow: 0 1px 6px rgba(0,0,0,0.3);
|
||||
box-shadow: 0 0.2vh 0.8vh rgba(0,0,0,0.3);
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
/* Wider default to fit full color palette */
|
||||
width: min(380px, 55vw);
|
||||
z-index: 99999;
|
||||
margin-top: 5px;
|
||||
margin-top: 0.7vh;
|
||||
}
|
||||
.pop-over hr {
|
||||
margin: 4px 0px;
|
||||
margin: 0.5vh 0px;
|
||||
}
|
||||
.pop-over p,
|
||||
.pop-over textarea,
|
||||
@@ -22,27 +23,27 @@
|
||||
}
|
||||
.pop-over select {
|
||||
width: 100%;
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 1.8vh;
|
||||
}
|
||||
.pop-over textarea {
|
||||
height: 72px;
|
||||
height: 9vh;
|
||||
}
|
||||
.pop-over form a span {
|
||||
padding: 0 0.5rem;
|
||||
padding: 0 0.7vw;
|
||||
}
|
||||
.pop-over .header {
|
||||
height: 36px;
|
||||
height: 4.5vh;
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 1vh;
|
||||
background: #f7f7f7;
|
||||
border-bottom: 1px solid #dcdcdc;
|
||||
color: #666;
|
||||
}
|
||||
.pop-over .header .header-title {
|
||||
display: block;
|
||||
line-height: 32px;
|
||||
padding-top: 4px;
|
||||
margin: 0 10px;
|
||||
line-height: 4vh;
|
||||
padding-top: 0.5vh;
|
||||
margin: 0 1.3vw;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -51,18 +52,18 @@
|
||||
.pop-over .header .back-btn {
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
width: 30px;
|
||||
width: 4vw;
|
||||
transition: width 0.2s;
|
||||
}
|
||||
.pop-over .header .back-btn i.fa {
|
||||
margin: 10px;
|
||||
margin-top: 12px;
|
||||
margin: 1.3vw;
|
||||
margin-top: 1.5vh;
|
||||
}
|
||||
.pop-over .header .back-btn.is-hidden {
|
||||
width: 0;
|
||||
}
|
||||
.pop-over .header .close-btn {
|
||||
padding: 10px 10px 10px 4px;
|
||||
padding: 1.3vh 1.3vw 1.3vh 0.5vw;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
@@ -76,23 +77,48 @@
|
||||
}
|
||||
.pop-over .content-container {
|
||||
width: 5000px;
|
||||
max-height: 550px;
|
||||
max-height: 70vh;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.pop-over .content-container .content {
|
||||
width: 280px;
|
||||
padding: 0 10px 10px;
|
||||
/* Match wider popover, leave padding */
|
||||
width: min(360px, 52vw);
|
||||
padding: 0 1.3vw 1.3vh;
|
||||
float: left;
|
||||
}
|
||||
|
||||
/* Utility: remove left gutter inside specific popups */
|
||||
.pop-over .content .flush-left {
|
||||
margin-left: -1.3vw;
|
||||
padding-left: 0;
|
||||
width: calc(100% + 1.3vw);
|
||||
}
|
||||
|
||||
/* Swimlane popups: remove left gutter, align content fully left */
|
||||
.pop-over .content form.swimlane-color-popup,
|
||||
.pop-over .content .swimlane-height-popup {
|
||||
margin-left: -1.3vw;
|
||||
padding-left: 0;
|
||||
width: calc(100% + 1.3vw);
|
||||
}
|
||||
|
||||
/* Ensure buttons don’t reserve left space; align to flow */
|
||||
.pop-over .content form.swimlane-color-popup .primary.confirm,
|
||||
.pop-over .content form.swimlane-color-popup .negate.wide.right,
|
||||
.pop-over .content .swimlane-height-popup .primary.confirm,
|
||||
.pop-over .content .swimlane-height-popup .negate.wide.right {
|
||||
float: none;
|
||||
margin-left: 0;
|
||||
}
|
||||
.pop-over .content-container .content.no-height {
|
||||
height: 20px;
|
||||
height: 2.5vh;
|
||||
}
|
||||
.pop-over .quiet {
|
||||
/* padding: 6px 6px 4px;*/
|
||||
}
|
||||
.pop-over.search-over {
|
||||
background: #f0f0f0;
|
||||
min-height: 114px;
|
||||
min-height: 14vh;
|
||||
}
|
||||
.pop-over.search-over .header {
|
||||
display: none;
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
/* Migration Progress Styles */
|
||||
.migration-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.migration-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.migration-modal {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
||||
max-width: 800px;
|
||||
width: 95%;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
animation: slideInScale 0.4s ease-out;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
@keyframes slideInScale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.migration-header {
|
||||
padding: 24px 32px 20px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.migration-header h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.migration-header h3 i {
|
||||
margin-right: 12px;
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.migration-header p {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.migration-content {
|
||||
padding: 24px 32px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.migration-overview {
|
||||
margin-bottom: 32px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.overall-progress {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
border-radius: 6px;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.4),
|
||||
transparent
|
||||
);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.current-step {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.current-step i {
|
||||
margin-right: 8px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.estimated-time {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
background-color: #fff3cd;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.estimated-time i {
|
||||
margin-right: 6px;
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
.migration-steps {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.migration-steps h4 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.steps-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.migration-step {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.migration-step:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.migration-step.completed {
|
||||
background-color: #d4edda;
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.migration-step.current {
|
||||
background-color: #cce7ff;
|
||||
border-left: 4px solid #667eea;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(102, 126, 234, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.step-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
margin-right: 12px;
|
||||
font-size: 18px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.step-icon i.fa-check-circle {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.step-icon i.fa-cog.fa-spin {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.step-icon i.fa-circle-o {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.step-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.step-description {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.step-progress {
|
||||
text-align: right;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.step-progress .progress-text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.step-progress-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.step-progress-bar .progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.migration-status {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
background-color: #e3f2fd;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #bbdefb;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.migration-status i {
|
||||
margin-right: 8px;
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.migration-footer {
|
||||
padding: 16px 32px 24px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.migration-info {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.migration-info i {
|
||||
margin-right: 6px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.migration-warning {
|
||||
text-align: center;
|
||||
color: #856404;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
background-color: #fff3cd;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.migration-warning i {
|
||||
margin-right: 6px;
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.migration-modal {
|
||||
width: 98%;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.migration-header,
|
||||
.migration-content,
|
||||
.migration-footer {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.migration-header h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.step-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.step-progress {
|
||||
text-align: left;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.steps-list {
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
template(name="migrationProgress")
|
||||
.migration-overlay(class="{{#if isMigrating}}active{{/if}}")
|
||||
.migration-modal
|
||||
.migration-header
|
||||
h3
|
||||
i.fa.fa-database
|
||||
| {{_ 'database-migration'}}
|
||||
p {{_ 'database-migration-description'}}
|
||||
|
||||
.migration-content
|
||||
.migration-overview
|
||||
.overall-progress
|
||||
.progress-bar
|
||||
.progress-fill(style="width: {{migrationProgress}}%")
|
||||
.progress-text {{migrationProgress}}%
|
||||
.progress-label {{_ 'overall-progress'}}
|
||||
|
||||
.current-step
|
||||
i.fa.fa-cog.fa-spin
|
||||
| {{migrationCurrentStep}}
|
||||
|
||||
.estimated-time(style="{{#unless migrationEstimatedTime}}display: none;{{/unless}}")
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'estimated-time-remaining'}}: {{migrationEstimatedTime}}
|
||||
|
||||
.migration-steps
|
||||
h4 {{_ 'migration-steps'}}
|
||||
.steps-list
|
||||
each migrationSteps
|
||||
.migration-step(class="{{#if completed}}completed{{/if}}" class="{{#if isCurrentStep}}current{{/if}}")
|
||||
.step-header
|
||||
.step-icon
|
||||
if completed
|
||||
i.fa.fa-check-circle
|
||||
else if isCurrentStep
|
||||
i.fa.fa-cog.fa-spin
|
||||
else
|
||||
i.fa.fa-circle-o
|
||||
.step-info
|
||||
.step-name {{name}}
|
||||
.step-description {{description}}
|
||||
.step-progress
|
||||
if completed
|
||||
.progress-text 100%
|
||||
else if isCurrentStep
|
||||
.progress-text {{progress}}%
|
||||
else
|
||||
.progress-text 0%
|
||||
if isCurrentStep
|
||||
.step-progress-bar
|
||||
.progress-fill(style="width: {{progress}}%")
|
||||
|
||||
.migration-status
|
||||
i.fa.fa-info-circle
|
||||
| {{migrationStatus}}
|
||||
|
||||
.migration-footer
|
||||
.migration-info
|
||||
i.fa.fa-lightbulb-o
|
||||
| {{_ 'migration-info-text'}}
|
||||
.migration-warning
|
||||
i.fa.fa-exclamation-triangle
|
||||
| {{_ 'migration-warning-text'}}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import {
|
||||
migrationManager,
|
||||
isMigrating,
|
||||
migrationProgress,
|
||||
migrationStatus,
|
||||
migrationCurrentStep,
|
||||
migrationEstimatedTime,
|
||||
migrationSteps
|
||||
} from '/client/lib/migrationManager';
|
||||
|
||||
Template.migrationProgress.helpers({
|
||||
isMigrating() {
|
||||
return isMigrating.get();
|
||||
},
|
||||
|
||||
migrationProgress() {
|
||||
return migrationProgress.get();
|
||||
},
|
||||
|
||||
migrationStatus() {
|
||||
return migrationStatus.get();
|
||||
},
|
||||
|
||||
migrationCurrentStep() {
|
||||
return migrationCurrentStep.get();
|
||||
},
|
||||
|
||||
migrationEstimatedTime() {
|
||||
return migrationEstimatedTime.get();
|
||||
},
|
||||
|
||||
migrationSteps() {
|
||||
const steps = migrationSteps.get();
|
||||
const currentStep = migrationCurrentStep.get();
|
||||
|
||||
return steps.map(step => ({
|
||||
...step,
|
||||
isCurrentStep: step.name === currentStep
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
Template.migrationProgress.onCreated(function() {
|
||||
// Subscribe to migration state changes
|
||||
this.autorun(() => {
|
||||
isMigrating.get();
|
||||
migrationProgress.get();
|
||||
migrationStatus.get();
|
||||
migrationCurrentStep.get();
|
||||
migrationEstimatedTime.get();
|
||||
migrationSteps.get();
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@
|
||||
#notifications-drawer .notification {
|
||||
display: flex;
|
||||
float: none;
|
||||
padding: 15px 8px 8px;
|
||||
padding: 2vh 1vw 1vh;
|
||||
color: #000;
|
||||
border-bottom: 1px solid #dbdbdb;
|
||||
}
|
||||
@@ -12,18 +12,18 @@
|
||||
display: none;
|
||||
}
|
||||
#notifications-drawer .notification .read-status {
|
||||
width: 30px;
|
||||
padding: 0px 10px 0px 0px;
|
||||
width: 4vw;
|
||||
padding: 0 1.3vw 0 0;
|
||||
}
|
||||
#notifications-drawer .notification .read-status input {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 3vw;
|
||||
height: 3vw;
|
||||
}
|
||||
#notifications-drawer .notification .read-status .activity-type {
|
||||
margin: 16px 0 0;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
font-size: 17px;
|
||||
margin: 2vh 0 0;
|
||||
width: 2.2vw;
|
||||
height: 2.2vw;
|
||||
font-size: clamp(14px, 2.5vw, 17px);
|
||||
display: block;
|
||||
color: #bbb;
|
||||
}
|
||||
@@ -32,20 +32,20 @@
|
||||
padding: 0px;
|
||||
}
|
||||
#notifications-drawer .notification .details .activity a.member svg {
|
||||
padding: 3px;
|
||||
padding: 0.4vw;
|
||||
}
|
||||
#notifications-drawer .notification .details .activity .activity-desc {
|
||||
margin: 0px 0px 0px 5px;
|
||||
margin: 0 0 0 0.7vw;
|
||||
}
|
||||
#notifications-drawer .notification .details .activity .activity-comment {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
border-radius: 0.4vw;
|
||||
background: #fff;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
margin-top: 5px;
|
||||
padding: 5px;
|
||||
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.2);
|
||||
margin-top: 0.7vh;
|
||||
padding: 0.7vh 0.7vw;
|
||||
}
|
||||
#notifications-drawer .notification .details .activity .activity-meta {
|
||||
display: block;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
.rules-list {
|
||||
overflow: hidden;
|
||||
overflow-y: scroll;
|
||||
max-height: 400px;
|
||||
padding-right: 5px;
|
||||
max-height: 50vh;
|
||||
padding-right: 0.7vw;
|
||||
}
|
||||
.rules-lists-item {
|
||||
display: block;
|
||||
@@ -33,14 +33,14 @@
|
||||
.rules-add {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 2vh;
|
||||
margin-bottom: 0.7vh;
|
||||
}
|
||||
.rules-add input {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin: auto;
|
||||
margin-right: 10px;
|
||||
margin-right: 1.3vw;
|
||||
}
|
||||
.rules-add button {
|
||||
display: inline-block;
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
template(name="attachmentSettings")
|
||||
ul#attachment-setting.setting-detail
|
||||
li
|
||||
h3 {{_ 'attachment-storage-configuration'}}
|
||||
.form-group
|
||||
label {{_ 'writable-path'}}
|
||||
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'filesystem-path-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 'attachments-path'}}
|
||||
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'attachments-path-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 'avatars-path'}}
|
||||
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'avatars-path-description'}}
|
||||
|
||||
li
|
||||
h3 {{_ 'mongodb-gridfs-storage'}}
|
||||
.form-group
|
||||
label {{_ 'gridfs-enabled'}}
|
||||
input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
|
||||
|
||||
li
|
||||
h3 {{_ 's3-minio-storage'}}
|
||||
.form-group
|
||||
label {{_ 's3-enabled'}}
|
||||
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 's3-enabled-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-endpoint'}}
|
||||
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-endpoint-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-bucket'}}
|
||||
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-bucket-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-region'}}
|
||||
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-region-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-access-key'}}
|
||||
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-access-key-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-secret-key'}}
|
||||
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
|
||||
small.form-text.text-muted {{_ 's3-secret-key-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-ssl-enabled'}}
|
||||
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-port'}}
|
||||
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-port-description'}}
|
||||
|
||||
.form-group
|
||||
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
|
||||
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
|
||||
|
||||
template(name="storageSettings")
|
||||
.storage-settings
|
||||
h3 {{_ 'attachment-storage-configuration'}}
|
||||
|
||||
.storage-config-section
|
||||
h4 {{_ 'filesystem-storage'}}
|
||||
.form-group
|
||||
label {{_ 'writable-path'}}
|
||||
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'filesystem-path-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 'attachments-path'}}
|
||||
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'attachments-path-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 'avatars-path'}}
|
||||
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'avatars-path-description'}}
|
||||
|
||||
.storage-config-section
|
||||
h4 {{_ 'mongodb-gridfs-storage'}}
|
||||
.form-group
|
||||
label {{_ 'gridfs-enabled'}}
|
||||
input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
|
||||
|
||||
.storage-config-section
|
||||
h4 {{_ 's3-minio-storage'}}
|
||||
.form-group
|
||||
label {{_ 's3-enabled'}}
|
||||
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 's3-enabled-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-endpoint'}}
|
||||
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-endpoint-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-bucket'}}
|
||||
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-bucket-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-region'}}
|
||||
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-region-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-access-key'}}
|
||||
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-access-key-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-secret-key'}}
|
||||
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
|
||||
small.form-text.text-muted {{_ 's3-secret-key-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-ssl-enabled'}}
|
||||
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-port'}}
|
||||
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-port-description'}}
|
||||
|
||||
.storage-actions
|
||||
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
|
||||
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
|
||||
|
||||
template(name="attachmentMigration")
|
||||
.attachment-migration
|
||||
h3 {{_ 'attachment-migration'}}
|
||||
|
||||
.migration-controls
|
||||
.form-group
|
||||
label {{_ 'migration-batch-size'}}
|
||||
input.wekan-form-control#migration-batch-size(type="number" value="{{migrationBatchSize}}" min="1" max="100")
|
||||
small.form-text.text-muted {{_ 'migration-batch-size-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 'migration-delay-ms'}}
|
||||
input.wekan-form-control#migration-delay-ms(type="number" value="{{migrationDelayMs}}" min="100" max="10000")
|
||||
small.form-text.text-muted {{_ 'migration-delay-ms-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 'migration-cpu-threshold'}}
|
||||
input.wekan-form-control#migration-cpu-threshold(type="number" value="{{migrationCpuThreshold}}" min="10" max="90")
|
||||
small.form-text.text-muted {{_ 'migration-cpu-threshold-description'}}
|
||||
|
||||
.migration-actions
|
||||
.migration-buttons
|
||||
button.js-migrate-all-to-filesystem.btn.btn-primary {{_ 'migrate-all-to-filesystem'}}
|
||||
button.js-migrate-all-to-gridfs.btn.btn-primary {{_ 'migrate-all-to-gridfs'}}
|
||||
button.js-migrate-all-to-s3.btn.btn-primary {{_ 'migrate-all-to-s3'}}
|
||||
|
||||
.migration-controls
|
||||
button.js-pause-migration.btn.btn-warning {{_ 'pause-migration'}}
|
||||
button.js-resume-migration.btn.btn-success {{_ 'resume-migration'}}
|
||||
button.js-stop-migration.btn.btn-danger {{_ 'stop-migration'}}
|
||||
|
||||
.migration-progress
|
||||
h4 {{_ 'migration-progress'}}
|
||||
.progress
|
||||
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
|
||||
| {{migrationProgress}}%
|
||||
|
||||
.migration-stats
|
||||
.stat-item
|
||||
span.label {{_ 'total-attachments'}}:
|
||||
span.value {{totalAttachments}}
|
||||
.stat-item
|
||||
span.label {{_ 'migrated-attachments'}}:
|
||||
span.value {{migratedAttachments}}
|
||||
.stat-item
|
||||
span.label {{_ 'remaining-attachments'}}:
|
||||
span.value {{remainingAttachments}}
|
||||
.stat-item
|
||||
span.label {{_ 'migration-status'}}:
|
||||
span.value {{migrationStatus}}
|
||||
|
||||
.migration-log
|
||||
h4 {{_ 'migration-log'}}
|
||||
.log-container
|
||||
pre#migration-log-content {{migrationLog}}
|
||||
|
||||
template(name="attachmentMonitoring")
|
||||
.attachment-monitoring
|
||||
h3 {{_ 'attachment-monitoring'}}
|
||||
|
||||
.monitoring-stats
|
||||
.stats-grid
|
||||
.stat-card
|
||||
h5 {{_ 'total-attachments'}}
|
||||
.stat-value {{totalAttachments}}
|
||||
.stat-card
|
||||
h5 {{_ 'filesystem-attachments'}}
|
||||
.stat-value {{filesystemAttachments}}
|
||||
.stat-card
|
||||
h5 {{_ 'gridfs-attachments'}}
|
||||
.stat-value {{gridfsAttachments}}
|
||||
.stat-card
|
||||
h5 {{_ 's3-attachments'}}
|
||||
.stat-value {{s3Attachments}}
|
||||
.stat-card
|
||||
h5 {{_ 'total-size'}}
|
||||
.stat-value {{totalSize}}
|
||||
.stat-card
|
||||
h5 {{_ 'filesystem-size'}}
|
||||
.stat-value {{filesystemSize}}
|
||||
.stat-card
|
||||
h5 {{_ 'gridfs-size'}}
|
||||
.stat-value {{gridfsSize}}
|
||||
.stat-card
|
||||
h5 {{_ 's3-size'}}
|
||||
.stat-value {{s3Size}}
|
||||
|
||||
.monitoring-charts
|
||||
h4 {{_ 'storage-distribution'}}
|
||||
.chart-container
|
||||
canvas#storage-distribution-chart
|
||||
|
||||
.monitoring-actions
|
||||
button.js-refresh-monitoring.btn.btn-secondary {{_ 'refresh-monitoring'}}
|
||||
button.js-export-monitoring.btn.btn-primary {{_ 'export-monitoring'}}
|
||||
@@ -0,0 +1,864 @@
|
||||
/* Cron Settings Styles */
|
||||
.cron-settings-content {
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.cron-migrations {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.migration-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.migration-header h2 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.migration-header h2 i {
|
||||
margin-right: 10px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.migration-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.migration-controls .btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.migration-controls .btn-primary {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.migration-controls .btn-primary:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.migration-controls .btn-warning {
|
||||
background-color: #ffc107;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.migration-controls .btn-warning:hover {
|
||||
background-color: #e0a800;
|
||||
}
|
||||
|
||||
.migration-controls .btn-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.migration-controls .btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.migration-progress {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.progress-overview {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
border-radius: 6px;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.4),
|
||||
transparent
|
||||
);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.current-step {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.current-step i {
|
||||
margin-right: 8px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.migration-status {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
background-color: #e3f2fd;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #bbdefb;
|
||||
}
|
||||
|
||||
.migration-status i {
|
||||
margin-right: 8px;
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.migration-steps {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.migration-steps h3 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.steps-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.migration-step {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.migration-step:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.migration-step.completed {
|
||||
background-color: #d4edda;
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.migration-step.current {
|
||||
background-color: #cce7ff;
|
||||
border-left: 4px solid #667eea;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(102, 126, 234, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.step-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
margin-right: 12px;
|
||||
font-size: 18px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.step-icon i.fa-check-circle {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.step-icon i.fa-cog.fa-spin {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.step-icon i.fa-circle-o {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.step-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.step-description {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.step-progress {
|
||||
text-align: right;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.step-progress .progress-text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.step-progress-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.step-progress-bar .progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Cron Jobs Styles */
|
||||
.cron-jobs {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.jobs-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.jobs-header h2 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.jobs-header h2 i {
|
||||
margin-right: 10px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.jobs-controls .btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.jobs-controls .btn-success {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.jobs-controls .btn-success:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.jobs-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.table thead {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.table td {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge.status-running {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-badge.status-stopped {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.status-badge.status-paused {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-badge.status-completed {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.status-badge.status-error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-group .btn-success {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-group .btn-success:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.btn-group .btn-warning {
|
||||
background-color: #ffc107;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.btn-group .btn-warning:hover {
|
||||
background-color: #e0a800;
|
||||
}
|
||||
|
||||
.btn-group .btn-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-group .btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
/* Add Job Form Styles */
|
||||
.cron-add-job {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.add-job-header {
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.add-job-header h2 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.add-job-header h2 i {
|
||||
margin-right: 10px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.add-job-form {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.form-control[type="number"] {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.form-actions .btn {
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-actions .btn-primary {
|
||||
background-color: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-actions .btn-primary:hover {
|
||||
background-color: #5a6fd8;
|
||||
}
|
||||
|
||||
.form-actions .btn-default {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-actions .btn-default:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
|
||||
/* Board Operations Styles */
|
||||
.cron-board-operations {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.board-operations-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.board-operations-header h2 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.board-operations-header h2 i {
|
||||
margin-right: 10px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.board-operations-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.board-operations-controls .btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.board-operations-controls .btn-success {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.board-operations-controls .btn-success:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.board-operations-controls .btn-primary {
|
||||
background-color: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.board-operations-controls .btn-primary:hover {
|
||||
background-color: #5a6fd8;
|
||||
}
|
||||
|
||||
.board-operations-stats {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.system-resources {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.resource-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.resource-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.resource-label {
|
||||
min-width: 120px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.resource-bar {
|
||||
flex: 1;
|
||||
height: 12px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin: 0 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.resource-fill {
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.resource-item:nth-child(1) .resource-fill {
|
||||
background: linear-gradient(90deg, #28a745, #20c997);
|
||||
}
|
||||
|
||||
.resource-item:nth-child(2) .resource-fill {
|
||||
background: linear-gradient(90deg, #007bff, #6f42c1);
|
||||
}
|
||||
|
||||
.resource-value {
|
||||
min-width: 50px;
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.board-operations-search {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.search-box .form-control {
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.board-operations-list {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.operations-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.operations-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.operations-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.operations-table .table {
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.operations-table .table th {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.operations-table .table td {
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.board-id {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
background: #f8f9fa;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.operation-type {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.progress-container .progress-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-container .progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-container .progress-text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
min-width: 35px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.pagination .btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.pagination .btn:hover {
|
||||
background: #f8f9fa;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.pagination .btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.migration-header,
|
||||
.jobs-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.migration-controls,
|
||||
.jobs-controls {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.add-job-form {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
template(name="cronSettings")
|
||||
ul#cron-setting.setting-detail
|
||||
li
|
||||
h3 {{_ 'cron-migrations'}}
|
||||
.form-group
|
||||
label {{_ 'migration-status'}}
|
||||
.status-indicator
|
||||
span.status-label {{_ 'status'}}:
|
||||
span.status-value {{migrationStatus}}
|
||||
.progress-section
|
||||
.progress
|
||||
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
|
||||
| {{migrationProgress}}%
|
||||
.progress-text
|
||||
| {{migrationProgress}}% {{_ 'complete'}}
|
||||
|
||||
.form-group
|
||||
button.js-start-all-migrations.btn.btn-primary {{_ 'start-all-migrations'}}
|
||||
button.js-pause-all-migrations.btn.btn-warning {{_ 'pause-all-migrations'}}
|
||||
button.js-stop-all-migrations.btn.btn-danger {{_ 'stop-all-migrations'}}
|
||||
|
||||
li
|
||||
h3 {{_ 'board-operations'}}
|
||||
.form-group
|
||||
label {{_ 'scheduled-board-operations'}}
|
||||
button.js-schedule-board-cleanup.btn.btn-primary {{_ 'schedule-board-cleanup'}}
|
||||
button.js-schedule-board-archive.btn.btn-warning {{_ 'schedule-board-archive'}}
|
||||
button.js-schedule-board-backup.btn.btn-info {{_ 'schedule-board-backup'}}
|
||||
|
||||
li
|
||||
h3 {{_ 'cron-jobs'}}
|
||||
.form-group
|
||||
label {{_ 'active-cron-jobs'}}
|
||||
each cronJobs
|
||||
.job-item
|
||||
.job-info
|
||||
.job-name {{name}}
|
||||
.job-schedule {{schedule}}
|
||||
.job-description {{description}}
|
||||
.job-actions
|
||||
button.js-pause-job.btn.btn-sm.btn-warning(data-job-id="{{_id}}") {{_ 'pause'}}
|
||||
button.js-delete-job.btn.btn-sm.btn-danger(data-job-id="{{_id}}") {{_ 'delete'}}
|
||||
.add-job-section
|
||||
button.js-add-cron-job.btn.btn-success {{_ 'add-cron-job'}}
|
||||
|
||||
template(name="cronMigrations")
|
||||
.cron-migrations
|
||||
.migration-header
|
||||
h2
|
||||
i.fa.fa-database
|
||||
| {{_ 'database-migrations'}}
|
||||
.migration-controls
|
||||
button.btn.btn-primary.js-start-all-migrations
|
||||
i.fa.fa-play
|
||||
| {{_ 'start-all-migrations'}}
|
||||
button.btn.btn-warning.js-pause-all-migrations
|
||||
i.fa.fa-pause
|
||||
| {{_ 'pause-all-migrations'}}
|
||||
button.btn.btn-danger.js-stop-all-migrations
|
||||
i.fa.fa-stop
|
||||
| {{_ 'stop-all-migrations'}}
|
||||
|
||||
.migration-progress
|
||||
.progress-overview
|
||||
.progress-bar
|
||||
.progress-fill(style="width: {{migrationProgress}}%")
|
||||
.progress-text {{migrationProgress}}%
|
||||
.progress-label {{_ 'overall-progress'}}
|
||||
|
||||
.current-step
|
||||
i.fa.fa-cog.fa-spin
|
||||
| {{migrationCurrentStep}}
|
||||
|
||||
.migration-status
|
||||
i.fa.fa-info-circle
|
||||
| {{migrationStatus}}
|
||||
|
||||
.migration-steps
|
||||
h3 {{_ 'migration-steps'}}
|
||||
.steps-list
|
||||
each migrationSteps
|
||||
.migration-step(class="{{#if completed}}completed{{/if}}" class="{{#if isCurrentStep}}current{{/if}}")
|
||||
.step-header
|
||||
.step-icon
|
||||
if completed
|
||||
i.fa.fa-check-circle
|
||||
else if isCurrentStep
|
||||
i.fa.fa-cog.fa-spin
|
||||
else
|
||||
i.fa.fa-circle-o
|
||||
.step-info
|
||||
.step-name {{name}}
|
||||
.step-description {{description}}
|
||||
.step-progress
|
||||
if completed
|
||||
.progress-text 100%
|
||||
else if isCurrentStep
|
||||
.progress-text {{progress}}%
|
||||
else
|
||||
.progress-text 0%
|
||||
if isCurrentStep
|
||||
.step-progress-bar
|
||||
.progress-fill(style="width: {{progress}}%")
|
||||
|
||||
template(name="cronBoardOperations")
|
||||
.cron-board-operations
|
||||
.board-operations-header
|
||||
h2
|
||||
i.fa.fa-tasks
|
||||
| {{_ 'board-operations'}}
|
||||
.board-operations-controls
|
||||
button.btn.btn-success.js-refresh-board-operations
|
||||
i.fa.fa-refresh
|
||||
| {{_ 'refresh'}}
|
||||
button.btn.btn-primary.js-start-test-operation
|
||||
i.fa.fa-play
|
||||
| {{_ 'start-test-operation'}}
|
||||
button.btn.btn-info.js-force-board-scan
|
||||
i.fa.fa-search
|
||||
| {{_ 'force-board-scan'}}
|
||||
|
||||
.board-operations-stats
|
||||
.stats-grid
|
||||
.stat-item
|
||||
.stat-value {{operationStats.total}}
|
||||
.stat-label {{_ 'total-operations'}}
|
||||
.stat-item
|
||||
.stat-value {{operationStats.running}}
|
||||
.stat-label {{_ 'running'}}
|
||||
.stat-item
|
||||
.stat-value {{operationStats.completed}}
|
||||
.stat-label {{_ 'completed'}}
|
||||
.stat-item
|
||||
.stat-value {{operationStats.error}}
|
||||
.stat-label {{_ 'errors'}}
|
||||
.stat-item
|
||||
.stat-value {{queueStats.pending}}
|
||||
.stat-label {{_ 'pending'}}
|
||||
.stat-item
|
||||
.stat-value {{queueStats.maxConcurrent}}
|
||||
.stat-label {{_ 'max-concurrent'}}
|
||||
.stat-item
|
||||
.stat-value {{boardMigrationStats.unmigratedCount}}
|
||||
.stat-label {{_ 'unmigrated-boards'}}
|
||||
.stat-item
|
||||
.stat-value {{boardMigrationStats.isScanning}}
|
||||
.stat-label {{_ 'scanning-status'}}
|
||||
|
||||
.system-resources
|
||||
.resource-item
|
||||
.resource-label {{_ 'cpu-usage'}}
|
||||
.resource-bar
|
||||
.resource-fill(style="width: {{systemResources.cpuUsage}}%")
|
||||
.resource-value {{systemResources.cpuUsage}}%
|
||||
.resource-item
|
||||
.resource-label {{_ 'memory-usage'}}
|
||||
.resource-bar
|
||||
.resource-fill(style="width: {{systemResources.memoryUsage}}%")
|
||||
.resource-value {{systemResources.memoryUsage}}%
|
||||
.resource-item
|
||||
.resource-label {{_ 'cpu-cores'}}
|
||||
.resource-value {{systemResources.cpuCores}}
|
||||
|
||||
.board-operations-search
|
||||
.search-box
|
||||
input.form-control.js-search-board-operations(type="text" placeholder="{{_ 'search-boards-or-operations'}}")
|
||||
i.fa.fa-search.search-icon
|
||||
|
||||
.board-operations-list
|
||||
.operations-header
|
||||
h3 {{_ 'board-operations'}} ({{pagination.total}})
|
||||
.pagination-info
|
||||
| {{_ 'showing'}} {{pagination.start}} - {{pagination.end}} {{_ 'of'}} {{pagination.total}}
|
||||
|
||||
.operations-table
|
||||
table.table.table-striped
|
||||
thead
|
||||
tr
|
||||
th {{_ 'board-id'}}
|
||||
th {{_ 'operation-type'}}
|
||||
th {{_ 'status'}}
|
||||
th {{_ 'progress'}}
|
||||
th {{_ 'start-time'}}
|
||||
th {{_ 'duration'}}
|
||||
th {{_ 'actions'}}
|
||||
tbody
|
||||
each boardOperations
|
||||
tr
|
||||
td
|
||||
.board-id {{boardId}}
|
||||
td
|
||||
.operation-type {{operationType}}
|
||||
td
|
||||
span.status-badge(class="status-{{status}}") {{status}}
|
||||
td
|
||||
.progress-container
|
||||
.progress-bar
|
||||
.progress-fill(style="width: {{progress}}%")
|
||||
.progress-text {{progress}}%
|
||||
td {{formatDateTime startTime}}
|
||||
td {{formatDuration startTime endTime}}
|
||||
td
|
||||
.btn-group
|
||||
if isRunning
|
||||
button.btn.btn-sm.btn-warning.js-pause-operation(data-operation="{{id}}")
|
||||
i.fa.fa-pause
|
||||
else
|
||||
button.btn.btn-sm.btn-success.js-resume-operation(data-operation="{{id}}")
|
||||
i.fa.fa-play
|
||||
button.btn.btn-sm.btn-danger.js-stop-operation(data-operation="{{id}}")
|
||||
i.fa.fa-stop
|
||||
button.btn.btn-sm.btn-info.js-view-details(data-operation="{{id}}")
|
||||
i.fa.fa-info-circle
|
||||
|
||||
.pagination
|
||||
if pagination.hasPrev
|
||||
button.btn.btn-sm.btn-default.js-prev-page
|
||||
i.fa.fa-chevron-left
|
||||
| {{_ 'previous'}}
|
||||
.page-info
|
||||
| {{_ 'page'}} {{pagination.page}} {{_ 'of'}} {{pagination.totalPages}}
|
||||
if pagination.hasNext
|
||||
button.btn.btn-sm.btn-default.js-next-page
|
||||
| {{_ 'next'}}
|
||||
i.fa.fa-chevron-right
|
||||
|
||||
template(name="cronJobs")
|
||||
.cron-jobs
|
||||
.jobs-header
|
||||
h2
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'cron-jobs'}}
|
||||
.jobs-controls
|
||||
button.btn.btn-success.js-refresh-jobs
|
||||
i.fa.fa-refresh
|
||||
| {{_ 'refresh'}}
|
||||
|
||||
.jobs-list
|
||||
table.table.table-striped
|
||||
thead
|
||||
tr
|
||||
th {{_ 'job-name'}}
|
||||
th {{_ 'schedule'}}
|
||||
th {{_ 'status'}}
|
||||
th {{_ 'last-run'}}
|
||||
th {{_ 'next-run'}}
|
||||
th {{_ 'actions'}}
|
||||
tbody
|
||||
each cronJobs
|
||||
tr
|
||||
td {{name}}
|
||||
td {{schedule}}
|
||||
td
|
||||
span.status-badge(class="status-{{status}}") {{status}}
|
||||
td {{formatDate lastRun}}
|
||||
td {{formatDate nextRun}}
|
||||
td
|
||||
.btn-group
|
||||
if isRunning
|
||||
button.btn.btn-sm.btn-warning.js-pause-job(data-job="{{name}}")
|
||||
i.fa.fa-pause
|
||||
else
|
||||
button.btn.btn-sm.btn-success.js-start-job(data-job="{{name}}")
|
||||
i.fa.fa-play
|
||||
button.btn.btn-sm.btn-danger.js-stop-job(data-job="{{name}}")
|
||||
i.fa.fa-stop
|
||||
button.btn.btn-sm.btn-danger.js-remove-job(data-job="{{name}}")
|
||||
i.fa.fa-trash
|
||||
|
||||
template(name="cronAddJob")
|
||||
.cron-add-job
|
||||
.add-job-header
|
||||
h2
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-cron-job'}}
|
||||
|
||||
.add-job-form
|
||||
form.js-add-cron-job-form
|
||||
.form-group
|
||||
label(for="job-name") {{_ 'job-name'}}
|
||||
input.form-control#job-name(type="text" name="name" required)
|
||||
|
||||
.form-group
|
||||
label(for="job-description") {{_ 'job-description'}}
|
||||
textarea.form-control#job-description(name="description" rows="3")
|
||||
|
||||
.form-group
|
||||
label(for="job-schedule") {{_ 'schedule'}}
|
||||
select.form-control#job-schedule(name="schedule")
|
||||
option(value="every 1 minute") {{_ 'every-1-minute'}}
|
||||
option(value="every 5 minutes") {{_ 'every-5-minutes'}}
|
||||
option(value="every 10 minutes") {{_ 'every-10-minutes'}}
|
||||
option(value="every 30 minutes") {{_ 'every-30-minutes'}}
|
||||
option(value="every 1 hour") {{_ 'every-1-hour'}}
|
||||
option(value="every 6 hours") {{_ 'every-6-hours'}}
|
||||
option(value="every 1 day") {{_ 'every-1-day'}}
|
||||
option(value="once") {{_ 'run-once'}}
|
||||
|
||||
.form-group
|
||||
label(for="job-weight") {{_ 'weight'}}
|
||||
input.form-control#job-weight(type="number" name="weight" value="1" min="1" max="10")
|
||||
|
||||
.form-actions
|
||||
button.btn.btn-primary(type="submit")
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-job'}}
|
||||
button.btn.btn-default.js-cancel-add-job
|
||||
i.fa.fa-times
|
||||
| {{_ 'cancel'}}
|
||||
@@ -0,0 +1,47 @@
|
||||
.text-red {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
td i.fa-lock.text-red,
|
||||
li i.fa-lock.text-red {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.locked-users-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.locked-users-table th,
|
||||
.locked-users-table td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.locked-users-table th {
|
||||
background-color: #f2f2f2;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.locked-users-table tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-indicator i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.locked-users-settings {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
button.js-unlock-all-users {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import LockoutSettings from '/models/lockoutSettings';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.lockedUsers = new ReactiveVar([]);
|
||||
this.isLoadingLockedUsers = new ReactiveVar(false);
|
||||
|
||||
// Don't load immediately to prevent unnecessary spinner
|
||||
// The data will be loaded when the tab is selected in peopleBody.js switchMenu
|
||||
},
|
||||
|
||||
refreshLockedUsers() {
|
||||
// Set loading state initially, but we'll hide it if no users are found
|
||||
this.isLoadingLockedUsers.set(true);
|
||||
|
||||
Meteor.call('getLockedUsers', (err, users) => {
|
||||
if (err) {
|
||||
this.isLoadingLockedUsers.set(false);
|
||||
const reason = err.reason || '';
|
||||
const message = `${TAPi18n.__(err.error)}\n${reason}`;
|
||||
alert(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// If no users are locked, don't show loading spinner and set empty array
|
||||
if (!users || users.length === 0) {
|
||||
this.isLoadingLockedUsers.set(false);
|
||||
this.lockedUsers.set([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Format the remaining time to be more human-readable
|
||||
users.forEach(user => {
|
||||
if (user.remainingLockTime > 60) {
|
||||
const minutes = Math.floor(user.remainingLockTime / 60);
|
||||
const seconds = user.remainingLockTime % 60;
|
||||
user.remainingTimeFormatted = `${minutes}m ${seconds}s`;
|
||||
} else {
|
||||
user.remainingTimeFormatted = `${user.remainingLockTime}s`;
|
||||
}
|
||||
});
|
||||
|
||||
this.lockedUsers.set(users);
|
||||
this.isLoadingLockedUsers.set(false);
|
||||
});
|
||||
},
|
||||
|
||||
unlockUser(event) {
|
||||
const userId = $(event.currentTarget).data('user-id');
|
||||
if (!userId) return;
|
||||
|
||||
if (confirm(TAPi18n.__('accounts-lockout-confirm-unlock'))) {
|
||||
Meteor.call('unlockUser', userId, (err, result) => {
|
||||
if (err) {
|
||||
const reason = err.reason || '';
|
||||
const message = `${TAPi18n.__(err.error)}\n${reason}`;
|
||||
alert(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
alert(TAPi18n.__('accounts-lockout-user-unlocked'));
|
||||
this.refreshLockedUsers();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
unlockAllUsers() {
|
||||
if (confirm(TAPi18n.__('accounts-lockout-confirm-unlock-all'))) {
|
||||
Meteor.call('unlockAllUsers', (err, result) => {
|
||||
if (err) {
|
||||
const reason = err.reason || '';
|
||||
const message = `${TAPi18n.__(err.error)}\n${reason}`;
|
||||
alert(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
alert(TAPi18n.__('accounts-lockout-user-unlocked'));
|
||||
this.refreshLockedUsers();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
saveLockoutSettings() {
|
||||
// Get values from form
|
||||
const knownFailuresBeforeLockout = parseInt($('#known-failures-before-lockout').val(), 10) || 3;
|
||||
const knownLockoutPeriod = parseInt($('#known-lockout-period').val(), 10) || 60;
|
||||
const knownFailureWindow = parseInt($('#known-failure-window').val(), 10) || 15;
|
||||
|
||||
const unknownFailuresBeforeLockout = parseInt($('#unknown-failures-before-lockout').val(), 10) || 3;
|
||||
const unknownLockoutPeriod = parseInt($('#unknown-lockout-period').val(), 10) || 60;
|
||||
const unknownFailureWindow = parseInt($('#unknown-failure-window').val(), 10) || 15;
|
||||
|
||||
// Update the database
|
||||
LockoutSettings.update('known-failuresBeforeLockout', {
|
||||
$set: { value: knownFailuresBeforeLockout },
|
||||
});
|
||||
LockoutSettings.update('known-lockoutPeriod', {
|
||||
$set: { value: knownLockoutPeriod },
|
||||
});
|
||||
LockoutSettings.update('known-failureWindow', {
|
||||
$set: { value: knownFailureWindow },
|
||||
});
|
||||
|
||||
LockoutSettings.update('unknown-failuresBeforeLockout', {
|
||||
$set: { value: unknownFailuresBeforeLockout },
|
||||
});
|
||||
LockoutSettings.update('unknown-lockoutPeriod', {
|
||||
$set: { value: unknownLockoutPeriod },
|
||||
});
|
||||
LockoutSettings.update('unknown-failureWindow', {
|
||||
$set: { value: unknownFailureWindow },
|
||||
});
|
||||
|
||||
// Reload the AccountsLockout configuration
|
||||
Meteor.call('reloadAccountsLockout', (err, ret) => {
|
||||
if (!err && ret) {
|
||||
const message = TAPi18n.__('accounts-lockout-settings-updated');
|
||||
alert(message);
|
||||
} else {
|
||||
const reason = err?.reason || '';
|
||||
const message = `${TAPi18n.__(err?.error || 'error-updating-settings')}\n${reason}`;
|
||||
alert(message);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
knownFailuresBeforeLockout() {
|
||||
return LockoutSettings.findOne('known-failuresBeforeLockout')?.value || 3;
|
||||
},
|
||||
|
||||
knownLockoutPeriod() {
|
||||
return LockoutSettings.findOne('known-lockoutPeriod')?.value || 60;
|
||||
},
|
||||
|
||||
knownFailureWindow() {
|
||||
return LockoutSettings.findOne('known-failureWindow')?.value || 15;
|
||||
},
|
||||
|
||||
unknownFailuresBeforeLockout() {
|
||||
return LockoutSettings.findOne('unknown-failuresBeforeLockout')?.value || 3;
|
||||
},
|
||||
|
||||
unknownLockoutPeriod() {
|
||||
return LockoutSettings.findOne('unknown-lockoutPeriod')?.value || 60;
|
||||
},
|
||||
|
||||
unknownFailureWindow() {
|
||||
return LockoutSettings.findOne('unknown-failureWindow')?.value || 15;
|
||||
},
|
||||
|
||||
lockedUsers() {
|
||||
return this.lockedUsers.get();
|
||||
},
|
||||
|
||||
isLoadingLockedUsers() {
|
||||
return this.isLoadingLockedUsers.get();
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click button.js-refresh-locked-users': this.refreshLockedUsers,
|
||||
'click button#refreshLockedUsers': this.refreshLockedUsers,
|
||||
'click button.js-unlock-user': this.unlockUser,
|
||||
'click button.js-unlock-all-users': this.unlockAllUsers,
|
||||
'click button.js-lockout-save': this.saveLockoutSettings,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('lockedUsersGeneral');
|
||||
@@ -1,11 +1,16 @@
|
||||
.main-body {
|
||||
overflow: scroll;
|
||||
}
|
||||
/* Scrollbar styles are now handled by settingBody.css for all admin pages */
|
||||
table {
|
||||
color: #000;
|
||||
min-width: 1200px !important;
|
||||
width: max-content !important;
|
||||
table-layout: auto !important;
|
||||
}
|
||||
|
||||
/* Force People/Organizations/Teams tables to always trigger horizontal scrolling */
|
||||
table td,
|
||||
table th {
|
||||
white-space: nowrap;
|
||||
min-width: 120px;
|
||||
border: 1px solid #d2d0d0;
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
@@ -89,3 +94,94 @@ table tr:nth-child(even) {
|
||||
#deleteAction {
|
||||
margin-left: 5% !important;
|
||||
}
|
||||
|
||||
.divLockedUsersFilter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
.divLockedUsersFilter .flex-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.divLockedUsersFilter .people-filter {
|
||||
margin-bottom: 0;
|
||||
color: #777;
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
.divLockedUsersFilter .user-filter {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
padding: 4px 8px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.unlock-all-btn {
|
||||
margin-left: 15px;
|
||||
background-color: white;
|
||||
color: #4d4d4d;
|
||||
border: 1px solid rgba(0,0,0,.15);
|
||||
border-radius: 2px;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
height: 28px;
|
||||
min-width: 90px;
|
||||
/* box-shadow: 0 1px 6px rgba(0,0,0,.3); */
|
||||
}
|
||||
|
||||
.unlock-all-btn:hover {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.account-active-status {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.js-toggle-active-status {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.unlock-all-success {
|
||||
position: fixed;
|
||||
top: 10%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: #27ae60;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
animation: fadeOut 3s ease-in forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0% { opacity: 1; }
|
||||
70% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.account-status {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-green {
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
.js-toggle-lock-status {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.unlock-all-btn .fa {
|
||||
color: #4d4d4d;
|
||||
}
|
||||
@@ -38,12 +38,28 @@ template(name="people")
|
||||
button#searchButton
|
||||
i.fa.fa-search
|
||||
| {{_ 'search'}}
|
||||
.divLockedUsersFilter
|
||||
.flex-container
|
||||
span.people-filter {{_ 'admin-people-filter-show'}}
|
||||
select.user-filter#userFilterSelect
|
||||
option(value="all") {{_ 'admin-people-filter-all'}}
|
||||
option(value="locked") {{_ 'admin-people-filter-locked'}}
|
||||
option(value="active") {{_ 'admin-people-filter-active'}}
|
||||
option(value="inactive") {{_ 'admin-people-filter-inactive'}}
|
||||
button#unlockAllUsers.unlock-all-btn
|
||||
i.fa.fa-unlock
|
||||
| {{_ 'accounts-lockout-unlock-all'}}
|
||||
.ext-box-right
|
||||
span {{#unless isMiniScreen}}{{_ 'people-number'}}{{/unless}} #{peopleNumber}
|
||||
.divAddOrRemoveTeam#divAddOrRemoveTeam
|
||||
button#addOrRemoveTeam
|
||||
i.fa.fa-edit
|
||||
| {{_ 'add'}} / {{_ 'delete'}} {{_ 'teams'}}
|
||||
else if lockedUsersSetting.get
|
||||
span
|
||||
i.fa.fa-lock.text-red
|
||||
unless isMiniScreen
|
||||
| {{_ 'accounts-lockout-locked-users'}}
|
||||
|
||||
.content-body
|
||||
.side-menu
|
||||
@@ -60,6 +76,10 @@ template(name="people")
|
||||
a.js-people-menu(data-id="people-setting")
|
||||
i.fa.fa-user
|
||||
| {{_ 'people'}}
|
||||
li
|
||||
a.js-locked-users-menu(data-id="locked-users-setting")
|
||||
i.fa.fa-lock.text-red
|
||||
| {{_ 'accounts-lockout-locked-users'}}
|
||||
.main-body
|
||||
if loading.get
|
||||
+spinner
|
||||
@@ -69,6 +89,8 @@ template(name="people")
|
||||
+teamGeneral
|
||||
else if peopleSetting.get
|
||||
+peopleGeneral
|
||||
else if lockedUsersSetting.get
|
||||
+lockedUsersGeneral
|
||||
|
||||
|
||||
template(name="orgGeneral")
|
||||
@@ -114,6 +136,8 @@ template(name="peopleGeneral")
|
||||
tr
|
||||
th
|
||||
+selectAllUser
|
||||
th {{_ 'accounts-lockout-status'}}
|
||||
th {{_ 'admin-people-active-status'}}
|
||||
th {{_ 'username'}}
|
||||
th {{_ 'fullname'}}
|
||||
th {{_ 'initials'}}
|
||||
@@ -232,8 +256,20 @@ template(name="peopleRow")
|
||||
else
|
||||
td
|
||||
input.selectUserChkBox(type="checkbox", id="{{userData._id}}")
|
||||
td.account-status
|
||||
if isUserLocked
|
||||
i.fa.fa-lock.text-red.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="true", title="{{_ 'accounts-lockout-click-to-unlock'}}")
|
||||
else
|
||||
i.fa.fa-unlock.text-green.js-toggle-lock-status(data-user-id=userData._id, data-is-locked="false", title="{{_ 'accounts-lockout-user-unlocked'}}")
|
||||
td.account-active-status
|
||||
if userData.loginDisabled
|
||||
i.fa.fa-ban.text-red.js-toggle-active-status(data-user-id=userData._id, data-is-active="false", title="{{_ 'admin-people-user-inactive'}}")
|
||||
else
|
||||
i.fa.fa-check-circle.text-green.js-toggle-active-status(data-user-id=userData._id, data-is-active="true", title="{{_ 'admin-people-user-active'}}")
|
||||
if userData.loginDisabled
|
||||
td.username <s>{{ userData.username }}</s>
|
||||
else if isUserLocked
|
||||
td.username {{ userData.username }}
|
||||
else
|
||||
td.username {{ userData.username }}
|
||||
if userData.loginDisabled
|
||||
@@ -645,3 +681,32 @@ template(name="settingsUserPopup")
|
||||
// that does now remove member from board, card members and assignees correctly,
|
||||
// but that should be used to remove user from all boards similarly
|
||||
// - wekan/models/users.js Delete is not enabled
|
||||
|
||||
template(name="lockedUsersGeneral")
|
||||
.locked-users-settings
|
||||
h3 {{_ 'accounts-lockout-settings'}}
|
||||
p {{_ 'accounts-lockout-info'}}
|
||||
|
||||
h4 {{_ 'accounts-lockout-known-users'}}
|
||||
.title {{_ 'accounts-lockout-failures-before'}}
|
||||
.form-group
|
||||
input.wekan-form-control#known-failures-before-lockout(type="number", min="1", max="10", placeholder="3" value="{{knownFailuresBeforeLockout}}")
|
||||
.title {{_ 'accounts-lockout-period'}}
|
||||
.form-group
|
||||
input.wekan-form-control#known-lockout-period(type="number", min="10", max="600", placeholder="60" value="{{knownLockoutPeriod}}")
|
||||
.title {{_ 'accounts-lockout-failure-window'}}
|
||||
.form-group
|
||||
input.wekan-form-control#known-failure-window(type="number", min="1", max="60", placeholder="15" value="{{knownFailureWindow}}")
|
||||
|
||||
h4 {{_ 'accounts-lockout-unknown-users'}}
|
||||
.title {{_ 'accounts-lockout-failures-before'}}
|
||||
.form-group
|
||||
input.wekan-form-control#unknown-failures-before-lockout(type="number", min="1", max="10", placeholder="3" value="{{unknownFailuresBeforeLockout}}")
|
||||
.title {{_ 'accounts-lockout-period'}}
|
||||
.form-group
|
||||
input.wekan-form-control#unknown-lockout-period(type="number", min="10", max="600", placeholder="60" value="{{unknownLockoutPeriod}}")
|
||||
.title {{_ 'accounts-lockout-failure-window'}}
|
||||
.form-group
|
||||
input.wekan-form-control#unknown-failure-window(type="number", min="1", max="60", placeholder="15" value="{{unknownFailureWindow}}")
|
||||
|
||||
button.js-lockout-save.primary {{_ 'save'}}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import LockoutSettings from '/models/lockoutSettings';
|
||||
|
||||
const orgsPerPage = 25;
|
||||
const teamsPerPage = 25;
|
||||
@@ -14,14 +15,16 @@ BlazeComponent.extendComponent({
|
||||
this.error = new ReactiveVar('');
|
||||
this.loading = new ReactiveVar(false);
|
||||
this.orgSetting = new ReactiveVar(true);
|
||||
this.teamSetting = new ReactiveVar(true);
|
||||
this.peopleSetting = new ReactiveVar(true);
|
||||
this.teamSetting = new ReactiveVar(false);
|
||||
this.peopleSetting = new ReactiveVar(false);
|
||||
this.lockedUsersSetting = new ReactiveVar(false);
|
||||
this.findOrgsOptions = new ReactiveVar({});
|
||||
this.findTeamsOptions = new ReactiveVar({});
|
||||
this.findUsersOptions = new ReactiveVar({});
|
||||
this.numberOrgs = new ReactiveVar(0);
|
||||
this.numberTeams = new ReactiveVar(0);
|
||||
this.numberPeople = new ReactiveVar(0);
|
||||
this.userFilterType = new ReactiveVar('all');
|
||||
|
||||
this.page = new ReactiveVar(1);
|
||||
this.loadNextPageLocked = false;
|
||||
@@ -92,6 +95,34 @@ BlazeComponent.extendComponent({
|
||||
this.filterPeople();
|
||||
}
|
||||
},
|
||||
'change #userFilterSelect'(event) {
|
||||
const filterType = $(event.target).val();
|
||||
this.userFilterType.set(filterType);
|
||||
this.filterPeople();
|
||||
},
|
||||
'click #unlockAllUsers'(event) {
|
||||
event.preventDefault();
|
||||
if (confirm(TAPi18n.__('accounts-lockout-confirm-unlock-all'))) {
|
||||
Meteor.call('unlockAllUsers', (error) => {
|
||||
if (error) {
|
||||
console.error('Error unlocking all users:', error);
|
||||
} else {
|
||||
// Show a brief success message
|
||||
const message = document.createElement('div');
|
||||
message.className = 'unlock-all-success';
|
||||
message.textContent = TAPi18n.__('accounts-lockout-all-users-unlocked');
|
||||
document.body.appendChild(message);
|
||||
|
||||
// Remove the message after a short delay
|
||||
setTimeout(() => {
|
||||
if (message.parentNode) {
|
||||
message.parentNode.removeChild(message);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
'click #newOrgButton'() {
|
||||
Popup.open('newOrg');
|
||||
},
|
||||
@@ -104,23 +135,50 @@ BlazeComponent.extendComponent({
|
||||
'click a.js-org-menu': this.switchMenu,
|
||||
'click a.js-team-menu': this.switchMenu,
|
||||
'click a.js-people-menu': this.switchMenu,
|
||||
'click a.js-locked-users-menu': this.switchMenu,
|
||||
},
|
||||
];
|
||||
},
|
||||
filterPeople() {
|
||||
const value = $('#searchInput').first().val();
|
||||
if (value === '') {
|
||||
this.findUsersOptions.set({});
|
||||
} else {
|
||||
const filterType = this.userFilterType.get();
|
||||
const currentTime = Number(new Date());
|
||||
|
||||
let query = {};
|
||||
|
||||
// Apply text search filter if there's a search value
|
||||
if (value !== '') {
|
||||
const regex = new RegExp(value, 'i');
|
||||
this.findUsersOptions.set({
|
||||
query = {
|
||||
$or: [
|
||||
{ username: regex },
|
||||
{ 'profile.fullname': regex },
|
||||
{ 'emails.address': regex },
|
||||
],
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Apply filter based on selected option
|
||||
switch (filterType) {
|
||||
case 'locked':
|
||||
// Show only locked users
|
||||
query['services.accounts-lockout.unlockTime'] = { $gt: currentTime };
|
||||
break;
|
||||
case 'active':
|
||||
// Show only active users (loginDisabled is false or undefined)
|
||||
query['loginDisabled'] = { $ne: true };
|
||||
break;
|
||||
case 'inactive':
|
||||
// Show only inactive users (loginDisabled is true)
|
||||
query['loginDisabled'] = true;
|
||||
break;
|
||||
case 'all':
|
||||
default:
|
||||
// Show all users, no additional filter
|
||||
break;
|
||||
}
|
||||
|
||||
this.findUsersOptions.set(query);
|
||||
},
|
||||
loadNextPage() {
|
||||
if (this.loadNextPageLocked === false) {
|
||||
@@ -145,26 +203,35 @@ BlazeComponent.extendComponent({
|
||||
this.loading.set(w);
|
||||
},
|
||||
orgList() {
|
||||
const limitOrgs = this.page.get() * orgsPerPage;
|
||||
const orgs = ReactiveCache.getOrgs(this.findOrgsOptions.get(), {
|
||||
sort: { orgDisplayName: 1 },
|
||||
limit: limitOrgs,
|
||||
fields: { _id: true },
|
||||
});
|
||||
// Count only the items currently loaded to browser, not total from database
|
||||
this.numberOrgs.set(orgs.length);
|
||||
return orgs;
|
||||
},
|
||||
teamList() {
|
||||
const limitTeams = this.page.get() * teamsPerPage;
|
||||
const teams = ReactiveCache.getTeams(this.findTeamsOptions.get(), {
|
||||
sort: { teamDisplayName: 1 },
|
||||
limit: limitTeams,
|
||||
fields: { _id: true },
|
||||
});
|
||||
// Count only the items currently loaded to browser, not total from database
|
||||
this.numberTeams.set(teams.length);
|
||||
return teams;
|
||||
},
|
||||
peopleList() {
|
||||
const limitUsers = this.page.get() * usersPerPage;
|
||||
const users = ReactiveCache.getUsers(this.findUsersOptions.get(), {
|
||||
sort: { username: 1 },
|
||||
limit: limitUsers,
|
||||
fields: { _id: true },
|
||||
});
|
||||
// Count only the items currently loaded to browser, not total from database
|
||||
this.numberPeople.set(users.length);
|
||||
return users;
|
||||
},
|
||||
@@ -186,6 +253,16 @@ BlazeComponent.extendComponent({
|
||||
this.orgSetting.set('org-setting' === targetID);
|
||||
this.teamSetting.set('team-setting' === targetID);
|
||||
this.peopleSetting.set('people-setting' === targetID);
|
||||
this.lockedUsersSetting.set('locked-users-setting' === targetID);
|
||||
|
||||
// When switching to locked users tab, refresh the locked users list
|
||||
if ('locked-users-setting' === targetID) {
|
||||
// Find the lockedUsersGeneral component and call refreshLockedUsers
|
||||
const lockedUsersComponent = Blaze.getView($('.main-body')[0])._templateInstance;
|
||||
if (lockedUsersComponent && lockedUsersComponent.refreshLockedUsers) {
|
||||
lockedUsersComponent.refreshLockedUsers();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}).register('people');
|
||||
@@ -206,8 +283,36 @@ Template.peopleRow.helpers({
|
||||
userData() {
|
||||
return ReactiveCache.getUser(this.userId);
|
||||
},
|
||||
isUserLocked() {
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user) return false;
|
||||
|
||||
// Check if user has accounts-lockout with unlockTime property
|
||||
if (user.services &&
|
||||
user.services['accounts-lockout'] &&
|
||||
user.services['accounts-lockout'].unlockTime) {
|
||||
|
||||
// Check if unlockTime is in the future
|
||||
const currentTime = Number(new Date());
|
||||
return user.services['accounts-lockout'].unlockTime > currentTime;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize filter dropdown
|
||||
Template.people.rendered = function() {
|
||||
const template = this;
|
||||
|
||||
// Set the initial value of the dropdown
|
||||
Tracker.afterFlush(function() {
|
||||
if (template.findAll('#userFilterSelect').length) {
|
||||
$('#userFilterSelect').val('all');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Template.editUserPopup.onCreated(function () {
|
||||
this.authenticationMethods = new ReactiveVar([]);
|
||||
this.errorMessage = new ReactiveVar('');
|
||||
@@ -415,6 +520,49 @@ BlazeComponent.extendComponent({
|
||||
else
|
||||
document.getElementById("divAddOrRemoveTeam").style.display = 'none';
|
||||
},
|
||||
'click .js-toggle-active-status': function(ev) {
|
||||
ev.preventDefault();
|
||||
const userId = this.userId;
|
||||
const user = ReactiveCache.getUser(userId);
|
||||
|
||||
if (!user) return;
|
||||
|
||||
// Toggle loginDisabled status
|
||||
const isActive = !(user.loginDisabled === true);
|
||||
|
||||
// Update the user's active status
|
||||
Users.update(userId, {
|
||||
$set: {
|
||||
loginDisabled: isActive
|
||||
}
|
||||
});
|
||||
},
|
||||
'click .js-toggle-lock-status': function(ev){
|
||||
ev.preventDefault();
|
||||
const userId = this.userId;
|
||||
const user = ReactiveCache.getUser(userId);
|
||||
|
||||
if (!user) return;
|
||||
|
||||
// Check if user is currently locked
|
||||
const isLocked = user.services &&
|
||||
user.services['accounts-lockout'] &&
|
||||
user.services['accounts-lockout'].unlockTime &&
|
||||
user.services['accounts-lockout'].unlockTime > Number(new Date());
|
||||
|
||||
if (isLocked) {
|
||||
// Unlock the user
|
||||
Meteor.call('unlockUser', userId, (error) => {
|
||||
if (error) {
|
||||
console.error('Error unlocking user:', error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Lock the user - this is optional, you may want to only allow unlocking
|
||||
// If you want to implement locking too, you would need a server method for it
|
||||
// For now, we'll leave this as a no-op
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
@@ -1130,23 +1278,30 @@ Template.settingsUserPopup.events({
|
||||
},
|
||||
'click #deleteButton'(event) {
|
||||
event.preventDefault();
|
||||
Users.remove(this.userId);
|
||||
/*
|
||||
// Delete user is enabled, but you should remove user from all boards
|
||||
// before deleting user, because there is possibility of leaving empty user avatars
|
||||
// to boards. You can remove non-existing user ids manually from database,
|
||||
// if that happens.
|
||||
//. See:
|
||||
// - wekan/client/components/settings/peopleBody.jade deleteButton
|
||||
// - wekan/client/components/settings/peopleBody.js deleteButton
|
||||
// - wekan/client/components/sidebar/sidebar.js Popup.afterConfirm('removeMember'
|
||||
// that does now remove member from board, card members and assignees correctly,
|
||||
// but that should be used to remove user from all boards similarly
|
||||
// - wekan/models/users.js Delete is not enabled
|
||||
//
|
||||
//
|
||||
*/
|
||||
Popup.back();
|
||||
|
||||
// Use secure server method instead of direct client-side removal
|
||||
Meteor.call('removeUser', this.userId, (error, result) => {
|
||||
if (error) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.error('Error removing user:', error);
|
||||
}
|
||||
// Show error message to user
|
||||
if (error.error === 'not-authorized') {
|
||||
alert('You are not authorized to delete this user.');
|
||||
} else if (error.error === 'user-not-found') {
|
||||
alert('User not found.');
|
||||
} else if (error.error === 'not-authorized' && error.reason === 'Cannot delete the last administrator') {
|
||||
alert('Cannot delete the last administrator.');
|
||||
} else {
|
||||
alert('Error deleting user: ' + error.reason);
|
||||
}
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('User deleted successfully:', result);
|
||||
}
|
||||
Popup.back();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -17,34 +17,34 @@
|
||||
position: absolute;
|
||||
}
|
||||
.setting-content .content-title {
|
||||
font-size: 20px;
|
||||
font-size: clamp(16px, 3.5vw, 22px);
|
||||
}
|
||||
.setting-content .content-body {
|
||||
display: flex;
|
||||
padding-top: 15px;
|
||||
padding-top: 2vh;
|
||||
height: 100%;
|
||||
gap: 10px;
|
||||
gap: 1.3vw;
|
||||
}
|
||||
.setting-content .content-body .side-menu {
|
||||
background-color: #f7f7f7;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
width: 250px;
|
||||
box-shadow: inset -1px -1px 3px rgba(0,0,0,0.05);
|
||||
border-radius: 0.5vw;
|
||||
width: min(250px, 32vw);
|
||||
box-shadow: inset -0.2vh -0.2vh 0.4vh rgba(0,0,0,0.05);
|
||||
}
|
||||
.setting-content .content-body .side-menu ul li {
|
||||
margin: 0.1rem 0.2rem;
|
||||
margin: 0.2vh 0.3vw;
|
||||
}
|
||||
.setting-content .content-body .side-menu ul li.active {
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
|
||||
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.15);
|
||||
}
|
||||
.setting-content .content-body .side-menu ul li:hover {
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
|
||||
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.15);
|
||||
}
|
||||
.setting-content .content-body .side-menu ul li a {
|
||||
padding: 1rem 0 1rem 1rem;
|
||||
padding: 1.3vh 0 1.3vh 1.3vw;
|
||||
width: 95%;
|
||||
}
|
||||
.setting-content .content-body .side-menu ul li a span {
|
||||
@@ -59,7 +59,79 @@
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
overflow-x: scroll !important;
|
||||
overflow-y: scroll !important;
|
||||
scrollbar-gutter: stable;
|
||||
/* Force horizontal scrollbar to always be visible */
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Ensure scrollbars are always visible with proper styling for all admin pages */
|
||||
.setting-content .content-body .main-body::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.setting-content .content-body .main-body::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 6px;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.setting-content .content-body .main-body::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 6px;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.setting-content .content-body .main-body::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.setting-content .content-body .main-body::-webkit-scrollbar-corner {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
/* Ensure tables and content are wide enough to trigger horizontal scrolling */
|
||||
.setting-content .content-body .main-body table {
|
||||
min-width: 1200px !important;
|
||||
width: max-content !important;
|
||||
table-layout: auto !important;
|
||||
}
|
||||
|
||||
/* Force tables to always trigger horizontal scrolling */
|
||||
.setting-content .content-body .main-body table td,
|
||||
.setting-content .content-body .main-body table th {
|
||||
white-space: nowrap;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* Ensure there's enough content to trigger vertical scrolling */
|
||||
.setting-content .content-body .main-body {
|
||||
min-height: calc(100vh - 200px);
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
/* Force horizontal scrollbar to always be visible at bottom */
|
||||
.setting-content .content-body .main-body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Add invisible content to force horizontal scrolling when needed */
|
||||
.setting-content .content-body .main-body::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 1px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.setting-content .content-body .main-body ul li {
|
||||
padding: 0.5rem 0.5rem;
|
||||
|
||||
@@ -3,64 +3,225 @@ template(name="setting")
|
||||
unless currentUser.isAdmin
|
||||
| {{_ 'error-notAuthorized'}}
|
||||
else
|
||||
.content-title
|
||||
i.fa.fa-cog
|
||||
span {{_ 'settings'}}
|
||||
.content-title.ext-box
|
||||
if isGeneralSetting
|
||||
span
|
||||
i.fa.fa-sign-in
|
||||
| {{_ 'registration'}}
|
||||
else if isEmailSetting
|
||||
span
|
||||
i.fa.fa-envelope
|
||||
| {{_ 'email'}}
|
||||
else if isAccountSetting
|
||||
span
|
||||
i.fa.fa-users
|
||||
| {{_ 'accounts'}}
|
||||
else if isTableVisibilityModeSetting
|
||||
span
|
||||
i.fa.fa-eye
|
||||
| {{_ 'tableVisibilityMode'}}
|
||||
else if isAnnouncementSetting
|
||||
span
|
||||
i.fa.fa-bullhorn
|
||||
| {{_ 'admin-announcement'}}
|
||||
else if isAccessibilitySetting
|
||||
span
|
||||
i.fa.fa-universal-access
|
||||
| {{_ 'accessibility'}}
|
||||
else if isLayoutSetting
|
||||
span
|
||||
i.fa.fa-object-group
|
||||
| {{_ 'layout'}}
|
||||
else if isWebhookSetting
|
||||
span
|
||||
i.fa.fa-globe
|
||||
| {{_ 'global-webhook'}}
|
||||
else if isAttachmentSettings
|
||||
span
|
||||
i.fa.fa-paperclip
|
||||
| {{_ 'attachments'}}
|
||||
else if isCronSettings
|
||||
span
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'cron'}}
|
||||
.content-body
|
||||
.side-menu
|
||||
ul
|
||||
li.active
|
||||
li(class="{{#if isGeneralSetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="registration-setting")
|
||||
i.fa.fa-sign-in
|
||||
| {{_ 'registration'}}
|
||||
unless isSandstorm
|
||||
li
|
||||
li(class="{{#if isEmailSetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="email-setting")
|
||||
i.fa.fa-envelope
|
||||
| {{_ 'email'}}
|
||||
li
|
||||
li(class="{{#if isAccountSetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="account-setting")
|
||||
i.fa.fa-users
|
||||
| {{_ 'accounts'}}
|
||||
li
|
||||
li(class="{{#if isTableVisibilityModeSetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="tableVisibilityMode-setting")
|
||||
i.fa.fa-eye
|
||||
| {{_ 'tableVisibilityMode'}}
|
||||
li
|
||||
li(class="{{#if isAnnouncementSetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="announcement-setting")
|
||||
i.fa.fa-bullhorn
|
||||
| {{_ 'admin-announcement'}}
|
||||
//li
|
||||
// a.js-setting-menu(data-id="accessibility-setting")
|
||||
// i.fa.fa-universal-access
|
||||
// | {{_ 'accessibility'}}
|
||||
li
|
||||
li(class="{{#if isAccessibilitySetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="accessibility-setting")
|
||||
i.fa.fa-universal-access
|
||||
| {{_ 'accessibility'}}
|
||||
li(class="{{#if isLayoutSetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="layout-setting")
|
||||
i.fa.fa-object-group
|
||||
| {{_ 'layout'}}
|
||||
li
|
||||
li(class="{{#if isWebhookSetting}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="webhook-setting")
|
||||
i.fa.fa-globe
|
||||
| {{_ 'global-webhook'}}
|
||||
li(class="{{#if isAttachmentSettings}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="attachment-settings")
|
||||
i.fa.fa-paperclip
|
||||
| {{_ 'attachments'}}
|
||||
li(class="{{#if isCronSettings}}active{{/if}}")
|
||||
a.js-setting-menu(data-id="cron-settings")
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'cron'}}
|
||||
.main-body
|
||||
if loading.get
|
||||
if isLoading
|
||||
+spinner
|
||||
else if generalSetting.get
|
||||
else if isAttachmentSettings
|
||||
ul#attachment-setting.setting-detail
|
||||
li
|
||||
h3 {{_ 'attachment-storage-configuration'}}
|
||||
.form-group
|
||||
label {{_ 'writable-path'}}
|
||||
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'filesystem-path-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 'attachments-path'}}
|
||||
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'attachments-path-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 'avatars-path'}}
|
||||
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'avatars-path-description'}}
|
||||
|
||||
li
|
||||
h3 {{_ 'mongodb-gridfs-storage'}}
|
||||
.form-group
|
||||
label {{_ 'gridfs-enabled'}}
|
||||
input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
|
||||
|
||||
li
|
||||
h3 {{_ 's3-minio-storage'}}
|
||||
.form-group
|
||||
label {{_ 's3-enabled'}}
|
||||
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 's3-enabled-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-endpoint'}}
|
||||
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-endpoint-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-bucket'}}
|
||||
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-bucket-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-region'}}
|
||||
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-region-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-access-key'}}
|
||||
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-access-key-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-secret-key'}}
|
||||
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
|
||||
small.form-text.text-muted {{_ 's3-secret-key-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-ssl-enabled'}}
|
||||
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-port'}}
|
||||
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-port-description'}}
|
||||
|
||||
.form-group
|
||||
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
|
||||
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
|
||||
else if isCronSettings
|
||||
ul#cron-setting.setting-detail
|
||||
li
|
||||
h3 {{_ 'cron-migrations'}}
|
||||
.form-group
|
||||
label {{_ 'migration-status'}}
|
||||
.status-indicator
|
||||
span.status-label {{_ 'status'}}:
|
||||
span.status-value {{migrationStatus}}
|
||||
.progress-section
|
||||
.progress
|
||||
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
|
||||
| {{migrationProgress}}%
|
||||
.progress-text
|
||||
| {{migrationProgress}}% {{_ 'complete'}}
|
||||
|
||||
.form-group
|
||||
button.js-start-all-migrations.btn.btn-primary {{_ 'start-all-migrations'}}
|
||||
button.js-pause-all-migrations.btn.btn-warning {{_ 'pause-all-migrations'}}
|
||||
button.js-stop-all-migrations.btn.btn-danger {{_ 'stop-all-migrations'}}
|
||||
|
||||
li
|
||||
h3 {{_ 'board-operations'}}
|
||||
.form-group
|
||||
label {{_ 'scheduled-board-operations'}}
|
||||
button.js-schedule-board-cleanup.btn.btn-primary {{_ 'schedule-board-cleanup'}}
|
||||
button.js-schedule-board-archive.btn.btn-warning {{_ 'schedule-board-archive'}}
|
||||
button.js-schedule-board-backup.btn.btn-info {{_ 'schedule-board-backup'}}
|
||||
|
||||
li
|
||||
h3 {{_ 'cron-jobs'}}
|
||||
.form-group
|
||||
label {{_ 'active-cron-jobs'}}
|
||||
each cronJobs
|
||||
.job-item
|
||||
.job-info
|
||||
.job-name {{name}}
|
||||
.job-schedule {{schedule}}
|
||||
.job-description {{description}}
|
||||
.job-actions
|
||||
button.js-pause-job.btn.btn-sm.btn-warning(data-job-id="{{_id}}") {{_ 'pause'}}
|
||||
button.js-delete-job.btn.btn-sm.btn-danger(data-job-id="{{_id}}") {{_ 'delete'}}
|
||||
.add-job-section
|
||||
button.js-add-cron-job.btn.btn-success {{_ 'add-cron-job'}}
|
||||
else if isGeneralSetting
|
||||
+general
|
||||
else if emailSetting.get
|
||||
else if isEmailSetting
|
||||
unless isSandstorm
|
||||
+email
|
||||
else if accountSetting.get
|
||||
else if isAccountSetting
|
||||
+accountSettings
|
||||
else if tableVisibilityModeSetting.get
|
||||
else if isTableVisibilityModeSetting
|
||||
+tableVisibilityModeSettings
|
||||
else if announcementSetting.get
|
||||
else if isAnnouncementSetting
|
||||
+announcementSettings
|
||||
else if accessibilitySetting.get
|
||||
else if isAccessibilitySetting
|
||||
+accessibilitySettings
|
||||
else if layoutSetting.get
|
||||
else if isLayoutSetting
|
||||
+layoutSettings
|
||||
else if webhookSetting.get
|
||||
else if isWebhookSetting
|
||||
+webhookSettings
|
||||
|
||||
template(name="webhookSettings")
|
||||
@@ -171,6 +332,8 @@ template(name='accountSettings')
|
||||
label {{_ 'no'}}
|
||||
button.js-accounts-save.primary {{_ 'save'}}
|
||||
|
||||
// Brute force lockout settings moved to People/Locked Users section
|
||||
|
||||
template(name='announcementSettings')
|
||||
ul#announcement-setting.setting-detail
|
||||
li
|
||||
@@ -189,24 +352,22 @@ template(name='announcementSettings')
|
||||
|
||||
template(name='accessibilitySettings')
|
||||
ul#accessibility-setting.setting-detail
|
||||
li
|
||||
a(href="/accessibility" style="text-decoration: underline; color: blue;") {{_ 'accessibility'}}
|
||||
li
|
||||
a.flex.js-toggle-accessibility
|
||||
.materialCheckBox(class="{{#if currentAccessibility.enabled}}is-checked{{/if}}")
|
||||
|
||||
span {{_ 'admin-accessibility-active'}}
|
||||
li
|
||||
.title {{_ 'accessibility-title'}}
|
||||
.form-group
|
||||
input.wekan-form-control#accessibility-title(type="text", placeholder="" value="{{currentSetting.accessibilityTitle}}")
|
||||
span {{_ 'accessibility-page-enabled'}}
|
||||
li
|
||||
.accessibility-content(class="{{#if currentAccessibility.enabled}}{{else}}hide{{/if}}")
|
||||
ul
|
||||
li
|
||||
.title {{_ 'admin-accessibility-title'}}
|
||||
textarea#admin-accessibility.wekan-form-control= currentAccessibility.accessibilityTitle
|
||||
.title {{_ 'accessibility-title'}}
|
||||
textarea#admin-accessibility-title.wekan-form-control= currentAccessibility.title
|
||||
li
|
||||
.title {{_ 'admin-accessibility-content'}}
|
||||
textarea#admin-accessibility.wekan-form-control= currentAccessibility.accessibilityContent
|
||||
.title {{_ 'accessibility-content'}}
|
||||
textarea#admin-accessibility-content.wekan-form-control= currentAccessibility.body
|
||||
li
|
||||
button.js-accessibility-save.primary {{_ 'save'}}
|
||||
|
||||
|
||||
@@ -1,36 +1,272 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { ALLOWED_WAIT_SPINNERS } from '/config/const';
|
||||
import LockoutSettings from '/models/lockoutSettings';
|
||||
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.error = new ReactiveVar('');
|
||||
this.loading = new ReactiveVar(false);
|
||||
this.forgotPasswordSetting = new ReactiveVar(true);
|
||||
this.forgotPasswordSetting = new ReactiveVar(false);
|
||||
this.generalSetting = new ReactiveVar(true);
|
||||
this.emailSetting = new ReactiveVar(false);
|
||||
this.accountSetting = new ReactiveVar(false);
|
||||
this.tableVisibilityModeSetting = new ReactiveVar(false);
|
||||
this.announcementSetting = new ReactiveVar(false);
|
||||
this.accessibilitySetting = new ReactiveVar(false);
|
||||
this.layoutSetting = new ReactiveVar(false);
|
||||
this.webhookSetting = new ReactiveVar(false);
|
||||
this.attachmentSettings = new ReactiveVar(false);
|
||||
this.cronSettings = new ReactiveVar(false);
|
||||
|
||||
Meteor.subscribe('setting');
|
||||
Meteor.subscribe('mailServer');
|
||||
Meteor.subscribe('accountSettings');
|
||||
Meteor.subscribe('tableVisibilityModeSettings');
|
||||
Meteor.subscribe('announcements');
|
||||
Meteor.subscribe('accessibilitySettings');
|
||||
Meteor.subscribe('globalwebhooks');
|
||||
Meteor.subscribe('lockoutSettings');
|
||||
},
|
||||
|
||||
|
||||
setError(error) {
|
||||
this.error.set(error);
|
||||
},
|
||||
|
||||
// Template helpers moved to BlazeComponent - using different names to avoid conflicts
|
||||
isGeneralSetting() {
|
||||
return this.generalSetting && this.generalSetting.get();
|
||||
},
|
||||
isEmailSetting() {
|
||||
return this.emailSetting && this.emailSetting.get();
|
||||
},
|
||||
isAccountSetting() {
|
||||
return this.accountSetting && this.accountSetting.get();
|
||||
},
|
||||
isTableVisibilityModeSetting() {
|
||||
return this.tableVisibilityModeSetting && this.tableVisibilityModeSetting.get();
|
||||
},
|
||||
isAnnouncementSetting() {
|
||||
return this.announcementSetting && this.announcementSetting.get();
|
||||
},
|
||||
isAccessibilitySetting() {
|
||||
return this.accessibilitySetting && this.accessibilitySetting.get();
|
||||
},
|
||||
isLayoutSetting() {
|
||||
return this.layoutSetting && this.layoutSetting.get();
|
||||
},
|
||||
isWebhookSetting() {
|
||||
return this.webhookSetting && this.webhookSetting.get();
|
||||
},
|
||||
isAttachmentSettings() {
|
||||
return this.attachmentSettings && this.attachmentSettings.get();
|
||||
},
|
||||
isCronSettings() {
|
||||
return this.cronSettings && this.cronSettings.get();
|
||||
},
|
||||
isLoading() {
|
||||
return this.loading && this.loading.get();
|
||||
},
|
||||
|
||||
// Attachment settings helpers
|
||||
filesystemPath() {
|
||||
return process.env.WRITABLE_PATH || '/data';
|
||||
},
|
||||
|
||||
attachmentsPath() {
|
||||
const writablePath = process.env.WRITABLE_PATH || '/data';
|
||||
return `${writablePath}/attachments`;
|
||||
},
|
||||
|
||||
avatarsPath() {
|
||||
const writablePath = process.env.WRITABLE_PATH || '/data';
|
||||
return `${writablePath}/avatars`;
|
||||
},
|
||||
|
||||
gridfsEnabled() {
|
||||
return process.env.GRIDFS_ENABLED === 'true';
|
||||
},
|
||||
|
||||
s3Enabled() {
|
||||
return process.env.S3_ENABLED === 'true';
|
||||
},
|
||||
|
||||
s3Endpoint() {
|
||||
return process.env.S3_ENDPOINT || '';
|
||||
},
|
||||
|
||||
s3Bucket() {
|
||||
return process.env.S3_BUCKET || '';
|
||||
},
|
||||
|
||||
s3Region() {
|
||||
return process.env.S3_REGION || '';
|
||||
},
|
||||
|
||||
s3SslEnabled() {
|
||||
return process.env.S3_SSL_ENABLED === 'true';
|
||||
},
|
||||
|
||||
s3Port() {
|
||||
return process.env.S3_PORT || 443;
|
||||
},
|
||||
|
||||
// Cron settings helpers
|
||||
migrationStatus() {
|
||||
return TAPi18n.__('idle'); // Placeholder
|
||||
},
|
||||
|
||||
migrationProgress() {
|
||||
return 0; // Placeholder
|
||||
},
|
||||
|
||||
cronJobs() {
|
||||
return []; // Placeholder
|
||||
},
|
||||
|
||||
setLoading(w) {
|
||||
this.loading.set(w);
|
||||
},
|
||||
|
||||
// Event handlers for attachment settings
|
||||
'click button.js-test-s3-connection'(event) {
|
||||
event.preventDefault();
|
||||
const secretKey = $('#s3-secret-key').val();
|
||||
if (!secretKey) {
|
||||
alert(TAPi18n.__('s3-secret-key-required'));
|
||||
return;
|
||||
}
|
||||
|
||||
Meteor.call('testS3Connection', { secretKey }, (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('s3-connection-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('s3-connection-success'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click button.js-save-s3-settings'(event) {
|
||||
event.preventDefault();
|
||||
const secretKey = $('#s3-secret-key').val();
|
||||
if (!secretKey) {
|
||||
alert(TAPi18n.__('s3-secret-key-required'));
|
||||
return;
|
||||
}
|
||||
|
||||
Meteor.call('saveS3Settings', { secretKey }, (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('s3-settings-save-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('s3-settings-saved'));
|
||||
$('#s3-secret-key').val(''); // Clear the password field
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Event handlers for cron settings
|
||||
'click button.js-start-all-migrations'(event) {
|
||||
event.preventDefault();
|
||||
Meteor.call('startAllMigrations', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('migration-started'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click button.js-pause-all-migrations'(event) {
|
||||
event.preventDefault();
|
||||
Meteor.call('pauseAllMigrations', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('migration-paused'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click button.js-stop-all-migrations'(event) {
|
||||
event.preventDefault();
|
||||
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
|
||||
Meteor.call('stopAllMigrations', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('migration-stopped'));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'click button.js-schedule-board-cleanup'(event) {
|
||||
event.preventDefault();
|
||||
Meteor.call('scheduleBoardCleanup', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('board-cleanup-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('board-cleanup-scheduled'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click button.js-schedule-board-archive'(event) {
|
||||
event.preventDefault();
|
||||
Meteor.call('scheduleBoardArchive', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('board-archive-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('board-archive-scheduled'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click button.js-schedule-board-backup'(event) {
|
||||
event.preventDefault();
|
||||
Meteor.call('scheduleBoardBackup', (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('board-backup-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('board-backup-scheduled'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click button.js-pause-job'(event) {
|
||||
event.preventDefault();
|
||||
const jobId = $(event.target).data('job-id');
|
||||
Meteor.call('pauseCronJob', jobId, (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('cron-job-pause-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('cron-job-paused'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click button.js-delete-job'(event) {
|
||||
event.preventDefault();
|
||||
const jobId = $(event.target).data('job-id');
|
||||
if (confirm(TAPi18n.__('cron-job-delete-confirm'))) {
|
||||
Meteor.call('deleteCronJob', jobId, (error, result) => {
|
||||
if (error) {
|
||||
alert(TAPi18n.__('cron-job-delete-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('cron-job-deleted'));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'click button.js-add-cron-job'(event) {
|
||||
event.preventDefault();
|
||||
// Placeholder for adding a new cron job (e.g., open a modal)
|
||||
alert(TAPi18n.__('add-cron-job-placeholder'));
|
||||
},
|
||||
|
||||
checkField(selector) {
|
||||
const value = $(selector).val();
|
||||
if (!value || value.trim() === '') {
|
||||
@@ -95,20 +331,62 @@ BlazeComponent.extendComponent({
|
||||
toggleDisplayAuthenticationMethod() {
|
||||
$('#display-authentication-method').toggleClass('is-checked');
|
||||
},
|
||||
|
||||
initializeAttachmentSubMenu() {
|
||||
// Set default sub-menu state for attachment settings
|
||||
// This will be handled by the attachment settings component
|
||||
console.log('Initializing attachment sub-menu');
|
||||
},
|
||||
|
||||
initializeCronSubMenu() {
|
||||
// Set default sub-menu state for cron settings
|
||||
// This will be handled by the cron settings template
|
||||
console.log('Initializing cron sub-menu');
|
||||
},
|
||||
switchMenu(event) {
|
||||
const target = $(event.target);
|
||||
if (!target.hasClass('active')) {
|
||||
$('.side-menu li.active').removeClass('active');
|
||||
target.parent().addClass('active');
|
||||
const targetID = target.data('id');
|
||||
this.forgotPasswordSetting.set('forgot-password-setting' === targetID);
|
||||
this.generalSetting.set('registration-setting' === targetID);
|
||||
this.emailSetting.set('email-setting' === targetID);
|
||||
this.accountSetting.set('account-setting' === targetID);
|
||||
this.announcementSetting.set('announcement-setting' === targetID);
|
||||
this.layoutSetting.set('layout-setting' === targetID);
|
||||
this.webhookSetting.set('webhook-setting' === targetID);
|
||||
this.tableVisibilityModeSetting.set('tableVisibilityMode-setting' === targetID);
|
||||
|
||||
// Reset all settings to false
|
||||
this.forgotPasswordSetting.set(false);
|
||||
this.generalSetting.set(false);
|
||||
this.emailSetting.set(false);
|
||||
this.accountSetting.set(false);
|
||||
this.tableVisibilityModeSetting.set(false);
|
||||
this.announcementSetting.set(false);
|
||||
this.accessibilitySetting.set(false);
|
||||
this.layoutSetting.set(false);
|
||||
this.webhookSetting.set(false);
|
||||
this.attachmentSettings.set(false);
|
||||
this.cronSettings.set(false);
|
||||
|
||||
// Set the selected setting to true
|
||||
if (targetID === 'registration-setting') {
|
||||
this.generalSetting.set(true);
|
||||
} else if (targetID === 'email-setting') {
|
||||
this.emailSetting.set(true);
|
||||
} else if (targetID === 'account-setting') {
|
||||
this.accountSetting.set(true);
|
||||
} else if (targetID === 'tableVisibilityMode-setting') {
|
||||
this.tableVisibilityModeSetting.set(true);
|
||||
} else if (targetID === 'announcement-setting') {
|
||||
this.announcementSetting.set(true);
|
||||
} else if (targetID === 'accessibility-setting') {
|
||||
this.accessibilitySetting.set(true);
|
||||
} else if (targetID === 'layout-setting') {
|
||||
this.layoutSetting.set(true);
|
||||
} else if (targetID === 'webhook-setting') {
|
||||
this.webhookSetting.set(true);
|
||||
} else if (targetID === 'attachment-settings') {
|
||||
this.attachmentSettings.set(true);
|
||||
this.initializeAttachmentSubMenu();
|
||||
} else if (targetID === 'cron-settings') {
|
||||
this.cronSettings.set(true);
|
||||
this.initializeCronSubMenu();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -187,71 +465,26 @@ BlazeComponent.extendComponent({
|
||||
this.setLoading(true);
|
||||
$('li').removeClass('has-error');
|
||||
|
||||
const productName = $('#product-name')
|
||||
.val()
|
||||
.trim();
|
||||
const customLoginLogoImageUrl = $('#custom-login-logo-image-url')
|
||||
.val()
|
||||
.trim();
|
||||
const customLoginLogoLinkUrl = $('#custom-login-logo-link-url')
|
||||
.val()
|
||||
.trim();
|
||||
const customHelpLinkUrl = $('#custom-help-link-url')
|
||||
.val()
|
||||
.trim();
|
||||
const textBelowCustomLoginLogo = $('#text-below-custom-login-logo')
|
||||
.val()
|
||||
.trim();
|
||||
const automaticLinkedUrlSchemes = $('#automatic-linked-url-schemes')
|
||||
.val()
|
||||
.trim();
|
||||
const customTopLeftCornerLogoImageUrl = $(
|
||||
'#custom-top-left-corner-logo-image-url',
|
||||
)
|
||||
.val()
|
||||
.trim();
|
||||
const customTopLeftCornerLogoLinkUrl = $(
|
||||
'#custom-top-left-corner-logo-link-url',
|
||||
)
|
||||
.val()
|
||||
.trim();
|
||||
const customTopLeftCornerLogoHeight = $(
|
||||
'#custom-top-left-corner-logo-height',
|
||||
)
|
||||
.val()
|
||||
.trim();
|
||||
const productName = ($('#product-name').val() || '').trim();
|
||||
const customLoginLogoImageUrl = ($('#custom-login-logo-image-url').val() || '').trim();
|
||||
const customLoginLogoLinkUrl = ($('#custom-login-logo-link-url').val() || '').trim();
|
||||
const customHelpLinkUrl = ($('#custom-help-link-url').val() || '').trim();
|
||||
const textBelowCustomLoginLogo = ($('#text-below-custom-login-logo').val() || '').trim();
|
||||
const automaticLinkedUrlSchemes = ($('#automatic-linked-url-schemes').val() || '').trim();
|
||||
const customTopLeftCornerLogoImageUrl = ($('#custom-top-left-corner-logo-image-url').val() || '').trim();
|
||||
const customTopLeftCornerLogoLinkUrl = ($('#custom-top-left-corner-logo-link-url').val() || '').trim();
|
||||
const customTopLeftCornerLogoHeight = ($('#custom-top-left-corner-logo-height').val() || '').trim();
|
||||
|
||||
const oidcBtnText = $(
|
||||
'#oidcBtnTextvalue',
|
||||
)
|
||||
.val()
|
||||
.trim();
|
||||
const mailDomainName = $(
|
||||
'#mailDomainNamevalue',
|
||||
)
|
||||
.val()
|
||||
.trim();
|
||||
const legalNotice = $(
|
||||
'#legalNoticevalue',
|
||||
)
|
||||
.val()
|
||||
.trim();
|
||||
const oidcBtnText = ($('#oidcBtnTextvalue').val() || '').trim();
|
||||
const mailDomainName = ($('#mailDomainNamevalue').val() || '').trim();
|
||||
const legalNotice = ($('#legalNoticevalue').val() || '').trim();
|
||||
const hideLogoChange = $('input[name=hideLogo]:checked').val() === 'true';
|
||||
const hideCardCounterListChange = $('input[name=hideCardCounterList]:checked').val() === 'true';
|
||||
const hideBoardMemberListChange = $('input[name=hideBoardMemberList]:checked').val() === 'true';
|
||||
const displayAuthenticationMethod =
|
||||
$('input[name=displayAuthenticationMethod]:checked').val() === 'true';
|
||||
const defaultAuthenticationMethod = $('#defaultAuthenticationMethod').val();
|
||||
/*
|
||||
const accessibilityPageEnabled = $('input[name=accessibilityPageEnabled]:checked').val() === 'true';
|
||||
const accessibilityTitle = $('#accessibility-title')
|
||||
.val()
|
||||
.trim();
|
||||
const accessibilityContent = $('#accessibility-content')
|
||||
.val()
|
||||
.trim();
|
||||
*/
|
||||
const spinnerName = $('#spinnerName').val();
|
||||
const spinnerName = ($('#spinnerName').val() || '').trim();
|
||||
|
||||
try {
|
||||
Settings.update(ReactiveCache.getCurrentSetting()._id, {
|
||||
@@ -276,11 +509,6 @@ BlazeComponent.extendComponent({
|
||||
legalNotice,
|
||||
},
|
||||
});
|
||||
/*
|
||||
accessibilityPageEnabled,
|
||||
accessibilityTitle,
|
||||
accessibilityContent,
|
||||
*/
|
||||
} catch (e) {
|
||||
return;
|
||||
} finally {
|
||||
@@ -317,7 +545,6 @@ BlazeComponent.extendComponent({
|
||||
'click a.js-toggle-hide-logo': this.toggleHideLogo,
|
||||
'click a.js-toggle-hide-card-counter-list': this.toggleHideCardCounterList,
|
||||
'click a.js-toggle-hide-board-member-list': this.toggleHideBoardMemberList,
|
||||
'click a.js-toggle-accessibility-page-enabled': this.toggleAccessibilityPageEnabled,
|
||||
'click button.js-save-layout': this.saveLayout,
|
||||
'click a.js-toggle-display-authentication-method': this
|
||||
.toggleDisplayAuthenticationMethod,
|
||||
@@ -344,15 +571,23 @@ BlazeComponent.extendComponent({
|
||||
$set: { booleanValue: allowUserDelete },
|
||||
});
|
||||
},
|
||||
|
||||
// Brute force lockout settings method moved to lockedUsersBody.js
|
||||
|
||||
allowEmailChange() {
|
||||
return AccountSettings.findOne('accounts-allowEmailChange').booleanValue;
|
||||
return AccountSettings.findOne('accounts-allowEmailChange')?.booleanValue || false;
|
||||
},
|
||||
|
||||
allowUserNameChange() {
|
||||
return AccountSettings.findOne('accounts-allowUserNameChange').booleanValue;
|
||||
return AccountSettings.findOne('accounts-allowUserNameChange')?.booleanValue || false;
|
||||
},
|
||||
|
||||
allowUserDelete() {
|
||||
return AccountSettings.findOne('accounts-allowUserDelete').booleanValue;
|
||||
return AccountSettings.findOne('accounts-allowUserDelete')?.booleanValue || false;
|
||||
},
|
||||
|
||||
// Lockout settings helper methods moved to lockedUsersBody.js
|
||||
|
||||
allBoardsHideActivities() {
|
||||
Meteor.call('setAllBoardsHideActivities', (err, ret) => {
|
||||
if (!err && ret) {
|
||||
@@ -469,6 +704,68 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
}).register('announcementSettings');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.loading = new ReactiveVar(false);
|
||||
},
|
||||
|
||||
setLoading(w) {
|
||||
this.loading.set(w);
|
||||
},
|
||||
|
||||
currentAccessibility() {
|
||||
return AccessibilitySettings.findOne();
|
||||
},
|
||||
|
||||
saveAccessibility() {
|
||||
this.setLoading(true);
|
||||
const title = $('#admin-accessibility-title')
|
||||
.val()
|
||||
.trim();
|
||||
const content = $('#admin-accessibility-content')
|
||||
.val()
|
||||
.trim();
|
||||
|
||||
try {
|
||||
AccessibilitySettings.update(AccessibilitySettings.findOne()._id, {
|
||||
$set: {
|
||||
title: title,
|
||||
body: content
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error saving accessibility settings:', e);
|
||||
return;
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
},
|
||||
|
||||
toggleAccessibility() {
|
||||
this.setLoading(true);
|
||||
const accessibilitySetting = this.currentAccessibility();
|
||||
const isActive = accessibilitySetting.enabled;
|
||||
AccessibilitySettings.update(accessibilitySetting._id, {
|
||||
$set: { enabled: !isActive },
|
||||
});
|
||||
this.setLoading(false);
|
||||
if (isActive) {
|
||||
$('.accessibility-content').slideUp();
|
||||
} else {
|
||||
$('.accessibility-content').slideDown();
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click a.js-toggle-accessibility': this.toggleAccessibility,
|
||||
'click button.js-accessibility-save': this.saveAccessibility,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('accessibilitySettings');
|
||||
|
||||
Template.selectAuthenticationMethod.onCreated(function() {
|
||||
this.authenticationMethods = new ReactiveVar([]);
|
||||
|
||||
@@ -504,3 +801,4 @@ Template.selectSpinnerName.helpers({
|
||||
return Template.instance().data.spinnerName === match;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
.main-body {
|
||||
overflow: scroll;
|
||||
}
|
||||
/* Scrollbar styles are now handled by settingBody.css for all admin pages */
|
||||
table {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
top: 7px;
|
||||
font-size: 1em;
|
||||
line-height: 1.6em;
|
||||
color: #999;
|
||||
color: #000;
|
||||
}
|
||||
.sidebar .sidebar-shortcuts .sidebar-btn {
|
||||
margin-left: 3px;
|
||||
@@ -146,6 +146,23 @@
|
||||
font-size: 24px;
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
.sidebar-accessibility {
|
||||
color: #4d4d4d;
|
||||
padding: 5px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
margin-left: auto;
|
||||
margin-right: 30px;
|
||||
}
|
||||
.sidebar-accessibility:hover {
|
||||
background-color: #d9d9d9;
|
||||
}
|
||||
.sidebar-accessibility span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.board-sidebar.is-open .sidebar-tongue {
|
||||
left: -28px;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ template(name="sidebar")
|
||||
a.sidebar-btn.js-keyboard-shortcuts-toggle(
|
||||
title="{{#if isKeyboardShortcuts}}{{_ 'keyboard-shortcuts-enabled'}}{{else}}{{_ 'keyboard-shortcuts-disabled'}}{{/if}}")
|
||||
i.fa(class="fa-solid fa-{{#if isKeyboardShortcuts}}check-square-o{{else}}ban{{/if}}")
|
||||
if isAccessibilityEnabled
|
||||
a.sidebar-accessibility
|
||||
i.fa.fa-universal-access
|
||||
span {{_ 'accessibility'}}
|
||||
a.sidebar-xmark.js-close-sidebar ✕
|
||||
.sidebar-content.js-board-sidebar-content
|
||||
//a.hide-btn.js-hide-sidebar
|
||||
@@ -280,6 +284,12 @@ template(name="boardCardSettingsPopup")
|
||||
span
|
||||
i.fa.fa-tags
|
||||
| {{_ 'labels'}}
|
||||
div.check-div
|
||||
a.flex.js-field-has-card-show-lists-on-minicard(class="{{#if allowsShowListsOnMinicard}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if allowsShowListsOnMinicard}}is-checked{{/if}}")
|
||||
span
|
||||
i.fa.fa-list
|
||||
| {{_ 'card-show-lists-on-minicard'}}
|
||||
div.check-div
|
||||
a.flex.js-field-has-card-number(class="{{#if allowsCardNumber}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if allowsCardNumber}}is-checked{{/if}}")
|
||||
@@ -648,6 +658,7 @@ template(name="addMemberPopup")
|
||||
p.quiet {{_ 'no-results'}}
|
||||
button.js-email-invite.primary.full {{_ 'email-invite'}}
|
||||
|
||||
|
||||
template(name="changePermissionsPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
|
||||
@@ -26,6 +26,9 @@ BlazeComponent.extendComponent({
|
||||
this._hideCardCounterList = new ReactiveVar(false);
|
||||
this._hideBoardMemberList = new ReactiveVar(false);
|
||||
Sidebar = this;
|
||||
|
||||
// Subscribe to accessibility settings
|
||||
Meteor.subscribe('accessibilitySettings');
|
||||
},
|
||||
|
||||
onDestroyed() {
|
||||
@@ -62,8 +65,10 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
reachNextPeak() {
|
||||
const activitiesComponent = this.childComponents('activities')[0];
|
||||
activitiesComponent.loadNextPage();
|
||||
const activitiesChildren = this.childComponents('activities');
|
||||
if (activitiesChildren && activitiesChildren.length > 0 && activitiesChildren[0] && typeof activitiesChildren[0].loadNextPage === 'function') {
|
||||
activitiesChildren[0].loadNextPage();
|
||||
}
|
||||
},
|
||||
|
||||
isTongueHidden() {
|
||||
@@ -115,6 +120,11 @@ BlazeComponent.extendComponent({
|
||||
return user && user.isVerticalScrollbars();
|
||||
},
|
||||
|
||||
isAccessibilityEnabled() {
|
||||
const setting = AccessibilitySettings.findOne({});
|
||||
return setting && setting.enabled;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
@@ -145,6 +155,10 @@ BlazeComponent.extendComponent({
|
||||
'click .js-show-week-of-year-toggle'() {
|
||||
ReactiveCache.getCurrentUser().toggleShowWeekOfYear();
|
||||
},
|
||||
'click .sidebar-accessibility'() {
|
||||
FlowRouter.go('accessibility');
|
||||
Sidebar.toggle();
|
||||
},
|
||||
'click .js-close-sidebar'() {
|
||||
Sidebar.toggle()
|
||||
},
|
||||
@@ -984,10 +998,15 @@ BlazeComponent.extendComponent({
|
||||
return this.currentBoard.allowsShowLists;
|
||||
},
|
||||
|
||||
|
||||
allowsLabels() {
|
||||
return this.currentBoard.allowsLabels;
|
||||
},
|
||||
|
||||
allowsShowListsOnMinicard() {
|
||||
return this.currentBoard.allowsShowListsOnMinicard;
|
||||
},
|
||||
|
||||
allowsChecklists() {
|
||||
return this.currentBoard.allowsChecklists;
|
||||
},
|
||||
@@ -1270,13 +1289,29 @@ BlazeComponent.extendComponent({
|
||||
this.currentBoard.setAllowsLabels(this.currentBoard.allowsLabels);
|
||||
$(`.js-field-has-labels ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
this.currentBoard.allowsAssignee,
|
||||
this.currentBoard.allowsLabels,
|
||||
);
|
||||
$('.js-field-has-labels').toggleClass(
|
||||
CKCLS,
|
||||
this.currentBoard.allowsLabels,
|
||||
);
|
||||
},
|
||||
'click .js-field-has-card-show-lists-on-minicard'(evt) {
|
||||
evt.preventDefault();
|
||||
this.currentBoard.allowsShowListsOnMinicard = !this.currentBoard
|
||||
.allowsShowListsOnMinicard;
|
||||
this.currentBoard.setAllowsShowListsOnMinicard(
|
||||
this.currentBoard.allowsShowListsOnMinicard,
|
||||
);
|
||||
$(`.js-field-has-card-show-lists-on-minicard ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
this.currentBoard.allowsShowListsOnMinicard,
|
||||
);
|
||||
$('.js-field-has-card-show-lists-on-minicard').toggleClass(
|
||||
CKCLS,
|
||||
this.currentBoard.allowsShowListsOnMinicard,
|
||||
);
|
||||
},
|
||||
'click .js-field-has-description-title'(evt) {
|
||||
evt.preventDefault();
|
||||
this.currentBoard.allowsDescriptionTitle = !this.currentBoard
|
||||
@@ -1936,3 +1971,4 @@ Template.changePermissionsPopup.helpers({
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -89,22 +89,28 @@ template(name="swimlaneAddPopup")
|
||||
a.js-swimlane-template {{_ 'template'}}
|
||||
|
||||
template(name="setSwimlaneColorPopup")
|
||||
form.edit-label
|
||||
.palette-colors: each colors
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
form.edit-label.swimlane-color-popup
|
||||
// Align content to left and remove default gutter
|
||||
.flush-left
|
||||
.palette-colors(style="margin-left:0; padding-left:0; justify-content:flex-start;")
|
||||
each colors
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
// Buttons aligned left too
|
||||
.flush-left
|
||||
button.primary.confirm.js-submit(style="margin-left:0") {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right(style="margin-left:8px") {{_ 'unset-color'}}
|
||||
|
||||
template(name="setSwimlaneHeightPopup")
|
||||
#js-swimlane-height-edit
|
||||
label a) {{_ 'set-swimlane-height-value'}}
|
||||
label b) -1
|
||||
p
|
||||
input.swimlane-height-value(type="number" value="{{ swimlaneHeightValue }}" min="100")
|
||||
input.swimlane-height-apply(type="submit" value="{{_ 'apply'}}")
|
||||
input.swimlane-height-error
|
||||
.flush-left.swimlane-height-popup
|
||||
#js-swimlane-height-edit
|
||||
label a) {{_ 'set-swimlane-height-value'}}
|
||||
label b) -1
|
||||
p
|
||||
input.swimlane-height-value(type="number" value="{{ swimlaneHeightValue }}" min="100")
|
||||
input.swimlane-height-apply(type="submit" value="{{_ 'apply'}}")
|
||||
input.swimlane-height-error
|
||||
|
||||
template(name="swimlaneHeightErrorPopup")
|
||||
.swimlane-height-invalid
|
||||
|
||||
@@ -13,15 +13,15 @@
|
||||
font-size: 50%;
|
||||
color: #a6a6a6;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 100px;
|
||||
top: 0.7vh;
|
||||
left: 13vw;
|
||||
}
|
||||
.swimlane-header-menu .swimlane-header-collapse-up {
|
||||
font-size: 50%;
|
||||
color: #a6a6a6;
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 100px;
|
||||
bottom: 0.7vh;
|
||||
left: 13vw;
|
||||
}
|
||||
.swimlane-header-menu .swimlane-header-uncollapse-up {
|
||||
font-size: 50%;
|
||||
@@ -35,10 +35,10 @@
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
height: 100px;
|
||||
height: 15vh;
|
||||
}
|
||||
.swimlane.ui-sortable-helper {
|
||||
box-shadow: -2px 2px 8px rgba(0,0,0,0.3), 0 0 1px rgba(0,0,0,0.5);
|
||||
box-shadow: -0.3vw 0.3vh 1vh rgba(0,0,0,0.3), 0 0 0.1vw rgba(0,0,0,0.5);
|
||||
transform: rotate(2deg);
|
||||
cursor: grabbing;
|
||||
}
|
||||
@@ -50,23 +50,33 @@
|
||||
flex-direction: row;
|
||||
flex: 1 0 100%;
|
||||
background-color: #ccc;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 33px;
|
||||
}
|
||||
.swimlane .swimlane-header-wrap .swimlane-header {
|
||||
font-size: 14px;
|
||||
padding: 5px 5px;
|
||||
font-weight: bold;
|
||||
min-height: 9px;
|
||||
min-height: 33px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
-o-text-overflow: ellipsis;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.swimlane .swimlane-header-wrap .swimlane-header-menu {
|
||||
position: absolute;
|
||||
padding: 5px 5px;
|
||||
font-size: 22px;
|
||||
z-index: 20;
|
||||
pointer-events: auto;
|
||||
}
|
||||
@media print {
|
||||
.swimlane .swimlane-header-wrap .swimlane-header-menu {
|
||||
@@ -86,9 +96,15 @@
|
||||
position: absolute;
|
||||
padding: 7px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 230px;
|
||||
font-size: 18px;
|
||||
font-size: clamp(16px, 3vw, 20px);
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: move;
|
||||
z-index: 15;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.swimlane .swimlane-header-wrap .swimlane-header-miniscreen-handle {
|
||||
position: absolute;
|
||||
@@ -97,6 +113,16 @@
|
||||
transform: translateY(-50%);
|
||||
left: 87vw;
|
||||
font-size: 24px;
|
||||
cursor: move;
|
||||
z-index: 15;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Safety: ensure wrapper is interactive and above list content */
|
||||
.swimlane .swimlane-header-wrap {
|
||||
position: relative;
|
||||
z-index: 9;
|
||||
pointer-events: auto;
|
||||
}
|
||||
#js-swimlane-height-edit .swimlane-height-error {
|
||||
display: none;
|
||||
|
||||
@@ -63,7 +63,7 @@ function initSortable(boardComponent, $listsDom) {
|
||||
};
|
||||
|
||||
$listsDom.sortable({
|
||||
connectWith: '.board-canvas',
|
||||
connectWith: '.js-swimlane, .js-lists',
|
||||
tolerance: 'pointer',
|
||||
helper: 'clone',
|
||||
items: '.js-list:not(.js-list-composer)',
|
||||
@@ -82,10 +82,31 @@ function initSortable(boardComponent, $listsDom) {
|
||||
const nextListDom = ui.item.next('.js-list').get(0);
|
||||
const sortIndex = calculateIndex(prevListDom, nextListDom, 1);
|
||||
|
||||
$listsDom.sortable('cancel');
|
||||
const listDomElement = ui.item.get(0);
|
||||
const list = Blaze.getData(listDomElement);
|
||||
|
||||
// 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
|
||||
targetSwimlaneId = targetSwimlaneDom.attr('id').replace('swimlane-', '');
|
||||
} 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);
|
||||
|
||||
/*
|
||||
Reverted incomplete change list width,
|
||||
removed from below Lists.update:
|
||||
@@ -95,10 +116,44 @@ function initSortable(boardComponent, $listsDom) {
|
||||
height: list._id.height(),
|
||||
*/
|
||||
|
||||
// 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;
|
||||
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
|
||||
const cardsInList = ReactiveCache.getCards({
|
||||
listId: list._id,
|
||||
archived: false
|
||||
});
|
||||
|
||||
cardsInList.forEach(card => {
|
||||
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
|
||||
// The DOM move should be allowed to complete
|
||||
} else {
|
||||
// If staying in the same swimlane, cancel the sortable to prevent DOM manipulation issues
|
||||
$listsDom.sortable('cancel');
|
||||
}
|
||||
|
||||
Lists.update(list._id, {
|
||||
$set: {
|
||||
sort: sortIndex.base,
|
||||
},
|
||||
$set: updateData,
|
||||
});
|
||||
|
||||
boardComponent.setIsDragging(false);
|
||||
@@ -109,10 +164,12 @@ function initSortable(boardComponent, $listsDom) {
|
||||
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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -228,7 +285,7 @@ BlazeComponent.extendComponent({
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const swimlane = Template.currentData();
|
||||
const height = user.getSwimlaneHeight(swimlane.boardId, swimlane._id);
|
||||
return height == -1 ? "auto" : (height + "px");
|
||||
return height == -1 ? "auto" : (height + 5 + "px");
|
||||
},
|
||||
}).register('swimlane');
|
||||
|
||||
@@ -281,7 +338,7 @@ BlazeComponent.extendComponent({
|
||||
boardId: Session.get('currentBoard'),
|
||||
sort: sortIndex,
|
||||
type: this.isListTemplatesSwimlane ? 'template-list' : 'list',
|
||||
swimlaneId: this.currentBoard.isTemplatesBoard() ? this.currentSwimlane._id : '',
|
||||
swimlaneId: this.currentSwimlane._id, // Always set swimlaneId for per-swimlane list titles
|
||||
});
|
||||
|
||||
titleInput.value = '';
|
||||
|
||||
@@ -23,7 +23,7 @@ Template.userAvatar.helpers({
|
||||
/*
|
||||
presenceStatusClassName() {
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
const userPresence = presences.findOne({ userId: this.userId });
|
||||
const userPresence = Presences.findOne({ userId: this.userId });
|
||||
if (user && user.isInvitedTo(Session.get('currentBoard'))) return 'pending';
|
||||
else if (!userPresence) return 'disconnected';
|
||||
else if (Session.equals('currentBoard', userPresence.state.currentBoardId))
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
.auth-layout .at-form-landing-logo {
|
||||
width: 249px;
|
||||
width: min(249px, 32vw);
|
||||
margin: auto;
|
||||
margin-top: 50px;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 6vh;
|
||||
margin-bottom: 2.5vh;
|
||||
}
|
||||
.auth-layout .auth-dialog {
|
||||
width: 275px;
|
||||
padding: 25px;
|
||||
width: min(275px, 36vw);
|
||||
padding: 3vh 3vw;
|
||||
margin: auto;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 2.5vh;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
border-radius: 0.4vw;
|
||||
border: 1px solid #dbdbdb;
|
||||
border-bottom-color: #c2c2c2;
|
||||
box-shadow: 0 1px 6px rgba(0,0,0,0.3);
|
||||
box-shadow: 0 0.2vh 0.8vh rgba(0,0,0,0.3);
|
||||
}
|
||||
.auth-layout .auth-dialog .at-form .at-link {
|
||||
color: #17683a;
|
||||
}
|
||||
.auth-layout .auth-dialog .at-form label {
|
||||
margin-bottom: 3px;
|
||||
margin-bottom: 0.4vh;
|
||||
}
|
||||
.auth-layout .auth-dialog .at-form input {
|
||||
width: 100%;
|
||||
@@ -31,9 +31,9 @@
|
||||
}
|
||||
.auth-layout .auth-dialog .at-form .at-title {
|
||||
background: #f7f7f7;
|
||||
margin: -25px;
|
||||
padding: 15px 25px 5px;
|
||||
margin-bottom: 20px;
|
||||
margin: -3vh -3vw;
|
||||
padding: 2vh 3vw 0.7vh;
|
||||
margin-bottom: 2.5vh;
|
||||
border-bottom: 1px solid #dcdcdc;
|
||||
color: #4d4d4d;
|
||||
font-weight: bold;
|
||||
@@ -42,7 +42,7 @@
|
||||
.auth-layout .auth-dialog .at-form .at-signin-link,
|
||||
.auth-layout .auth-dialog .at-form .at-forgotPwd {
|
||||
font-size: 0.9em;
|
||||
margin-top: 15px;
|
||||
margin-top: 2vh;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
.auth-layout .auth-dialog .at-form .at-signup-link .at-signUp,
|
||||
|
||||
@@ -12,6 +12,11 @@ template(name="headerUserBar")
|
||||
|
||||
template(name="memberMenuPopup")
|
||||
ul.pop-over-list
|
||||
// Bookmarks at the very top
|
||||
li
|
||||
a.js-open-bookmarks
|
||||
i.fa.fa-bookmark
|
||||
| {{_ 'bookmarks'}}
|
||||
with currentUser
|
||||
li
|
||||
a.js-my-cards(href="{{pathFor 'my-cards'}}")
|
||||
@@ -37,6 +42,15 @@ template(name="memberMenuPopup")
|
||||
a.board-header-btn.js-open-archived-board
|
||||
i.fa.fa-archive
|
||||
span {{_ 'archives'}}
|
||||
li
|
||||
a.js-notifications-drawer-toggle
|
||||
i.fa.fa-bell
|
||||
| {{_ 'notifications'}}
|
||||
if currentSetting.customHelpLinkUrl
|
||||
li
|
||||
a(href="{{currentSetting.customHelpLinkUrl}}", title="{{_ 'help'}}", target="_blank", rel="noopener noreferrer")
|
||||
i.fa.fa-question
|
||||
| {{_ 'help'}}
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
li
|
||||
|
||||
@@ -62,6 +62,15 @@ Template.memberMenuPopup.helpers({
|
||||
});
|
||||
|
||||
Template.memberMenuPopup.events({
|
||||
'click .js-open-bookmarks'(e) {
|
||||
e.preventDefault();
|
||||
if (Utils.isMiniScreen()) {
|
||||
FlowRouter.go('bookmarks');
|
||||
Popup.back();
|
||||
} else {
|
||||
Popup.open('bookmarksPopup')(e);
|
||||
}
|
||||
},
|
||||
'click .js-my-cards'() {
|
||||
Popup.back();
|
||||
},
|
||||
@@ -78,6 +87,9 @@ Template.memberMenuPopup.events({
|
||||
'click .js-change-password': Popup.open('changePassword'),
|
||||
'click .js-change-language': Popup.open('changeLanguage'),
|
||||
'click .js-support': Popup.open('support'),
|
||||
'click .js-notifications-drawer-toggle'() {
|
||||
Session.set('showNotificationsDrawer', !Session.get('showNotificationsDrawer'));
|
||||
},
|
||||
'click .js-logout'(event) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -241,8 +253,21 @@ Template.editProfilePopup.events({
|
||||
},
|
||||
'click #deleteButton': Popup.afterConfirm('userDelete', function() {
|
||||
Popup.back();
|
||||
Users.remove(Meteor.userId());
|
||||
AccountsTemplates.logout();
|
||||
|
||||
// Use secure server method for self-deletion
|
||||
Meteor.call('removeUser', Meteor.userId(), (error, result) => {
|
||||
if (error) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.error('Error removing user:', error);
|
||||
}
|
||||
alert('Error deleting account: ' + error.reason);
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('User deleted successfully:', result);
|
||||
}
|
||||
AccountsTemplates.logout();
|
||||
}
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Attachment Migration Manager
|
||||
* Handles migration of attachments from old structure to new structure
|
||||
* with UI feedback and spinners for unconverted attachments
|
||||
*/
|
||||
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
|
||||
// Reactive variables for attachment migration progress
|
||||
export const attachmentMigrationProgress = new ReactiveVar(0);
|
||||
export const attachmentMigrationStatus = new ReactiveVar('');
|
||||
export const isMigratingAttachments = new ReactiveVar(false);
|
||||
export const unconvertedAttachments = new ReactiveVar([]);
|
||||
|
||||
// Global tracking of migrated boards (persistent across component reinitializations)
|
||||
const globalMigratedBoards = new Set();
|
||||
|
||||
class AttachmentMigrationManager {
|
||||
constructor() {
|
||||
this.migrationCache = new Map(); // Cache migrated attachment IDs
|
||||
this.migratedBoards = new Set(); // Track boards that have been migrated
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an attachment needs migration
|
||||
* @param {string} attachmentId - The attachment ID to check
|
||||
* @returns {boolean} - True if attachment needs migration
|
||||
*/
|
||||
needsMigration(attachmentId) {
|
||||
if (this.migrationCache.has(attachmentId)) {
|
||||
return false; // Already migrated
|
||||
}
|
||||
|
||||
try {
|
||||
const attachment = ReactiveCache.getAttachment(attachmentId);
|
||||
if (!attachment) return false;
|
||||
|
||||
// Check if attachment has old structure (no meta field or missing required fields)
|
||||
return !attachment.meta ||
|
||||
!attachment.meta.cardId ||
|
||||
!attachment.meta.boardId ||
|
||||
!attachment.meta.listId;
|
||||
} catch (error) {
|
||||
console.error('Error checking if attachment needs migration:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a board has been migrated
|
||||
* @param {string} boardId - The board ID
|
||||
* @returns {boolean} - True if board has been migrated
|
||||
*/
|
||||
isBoardMigrated(boardId) {
|
||||
return globalMigratedBoards.has(boardId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a board has been migrated (server-side check)
|
||||
* @param {string} boardId - The board ID
|
||||
* @returns {Promise<boolean>} - True if board has been migrated
|
||||
*/
|
||||
async isBoardMigratedServer(boardId) {
|
||||
return new Promise((resolve) => {
|
||||
Meteor.call('attachmentMigration.isBoardMigrated', boardId, (error, result) => {
|
||||
if (error) {
|
||||
console.error('Error checking board migration status:', error);
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unconverted attachments for a board
|
||||
* @param {string} boardId - The board ID
|
||||
* @returns {Array} - Array of unconverted attachments
|
||||
*/
|
||||
getUnconvertedAttachments(boardId) {
|
||||
try {
|
||||
const attachments = ReactiveCache.getAttachments({
|
||||
'meta.boardId': boardId
|
||||
});
|
||||
|
||||
return attachments.filter(attachment => this.needsMigration(attachment._id));
|
||||
} catch (error) {
|
||||
console.error('Error getting unconverted attachments:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start migration for attachments in a board
|
||||
* @param {string} boardId - The board ID
|
||||
*/
|
||||
async startAttachmentMigration(boardId) {
|
||||
if (isMigratingAttachments.get()) {
|
||||
return; // Already migrating
|
||||
}
|
||||
|
||||
// Check if this board has already been migrated (client-side check first)
|
||||
if (globalMigratedBoards.has(boardId)) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has already been migrated (client-side), skipping`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Double-check with server-side check
|
||||
const serverMigrated = await this.isBoardMigratedServer(boardId);
|
||||
if (serverMigrated) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has already been migrated (server-side), skipping`);
|
||||
}
|
||||
globalMigratedBoards.add(boardId); // Sync client-side tracking
|
||||
return;
|
||||
}
|
||||
|
||||
isMigratingAttachments.set(true);
|
||||
attachmentMigrationStatus.set('Starting attachment migration...');
|
||||
attachmentMigrationProgress.set(0);
|
||||
|
||||
try {
|
||||
const unconverted = this.getUnconvertedAttachments(boardId);
|
||||
unconvertedAttachments.set(unconverted);
|
||||
|
||||
if (unconverted.length === 0) {
|
||||
attachmentMigrationStatus.set('All attachments are already migrated');
|
||||
attachmentMigrationProgress.set(100);
|
||||
isMigratingAttachments.set(false);
|
||||
globalMigratedBoards.add(boardId); // Mark board as migrated
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has no attachments to migrate, marked as migrated`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Start server-side migration
|
||||
Meteor.call('attachmentMigration.migrateBoardAttachments', boardId, (error, result) => {
|
||||
if (error) {
|
||||
console.error('Failed to start attachment migration:', error);
|
||||
const errorMessage = error.message || error.reason || error.toString();
|
||||
attachmentMigrationStatus.set(`Migration failed: ${errorMessage}`);
|
||||
isMigratingAttachments.set(false);
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Attachment migration started for board:', boardId);
|
||||
}
|
||||
this.pollAttachmentMigrationProgress(boardId);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error starting attachment migration:', error);
|
||||
attachmentMigrationStatus.set(`Migration failed: ${error.message}`);
|
||||
isMigratingAttachments.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll for attachment migration progress
|
||||
* @param {string} boardId - The board ID
|
||||
*/
|
||||
pollAttachmentMigrationProgress(boardId) {
|
||||
const pollInterval = setInterval(() => {
|
||||
Meteor.call('attachmentMigration.getProgress', boardId, (error, result) => {
|
||||
if (error) {
|
||||
console.error('Error getting migration progress:', error);
|
||||
clearInterval(pollInterval);
|
||||
isMigratingAttachments.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
attachmentMigrationProgress.set(result.progress);
|
||||
attachmentMigrationStatus.set(result.status);
|
||||
unconvertedAttachments.set(result.unconvertedAttachments || []);
|
||||
|
||||
// Stop polling if migration is complete
|
||||
if (result.progress >= 100 || result.status === 'completed') {
|
||||
clearInterval(pollInterval);
|
||||
isMigratingAttachments.set(false);
|
||||
this.migrationCache.clear(); // Clear cache to refresh data
|
||||
globalMigratedBoards.add(boardId); // Mark board as migrated
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} migration completed and marked as migrated`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an attachment is currently being migrated
|
||||
* @param {string} attachmentId - The attachment ID
|
||||
* @returns {boolean} - True if attachment is being migrated
|
||||
*/
|
||||
isAttachmentBeingMigrated(attachmentId) {
|
||||
const unconverted = unconvertedAttachments.get();
|
||||
return unconverted.some(attachment => attachment._id === attachmentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get migration status for an attachment
|
||||
* @param {string} attachmentId - The attachment ID
|
||||
* @returns {string} - Migration status ('migrated', 'migrating', 'unmigrated')
|
||||
*/
|
||||
getAttachmentMigrationStatus(attachmentId) {
|
||||
if (this.migrationCache.has(attachmentId)) {
|
||||
return 'migrated';
|
||||
}
|
||||
|
||||
if (this.isAttachmentBeingMigrated(attachmentId)) {
|
||||
return 'migrating';
|
||||
}
|
||||
|
||||
return this.needsMigration(attachmentId) ? 'unmigrated' : 'migrated';
|
||||
}
|
||||
}
|
||||
|
||||
export const attachmentMigrationManager = new AttachmentMigrationManager();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* Board Conversion Service
|
||||
* Handles conversion of boards from old database structure to new structure
|
||||
* without running migrations that could cause downtime
|
||||
*/
|
||||
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import Lists from '/models/lists';
|
||||
|
||||
// Reactive variables for conversion progress
|
||||
export const conversionProgress = new ReactiveVar(0);
|
||||
export const conversionStatus = new ReactiveVar('');
|
||||
export const conversionEstimatedTime = new ReactiveVar('');
|
||||
export const isConverting = new ReactiveVar(false);
|
||||
|
||||
// Global tracking of converted boards (persistent across component reinitializations)
|
||||
const globalConvertedBoards = new Set();
|
||||
|
||||
class BoardConverter {
|
||||
constructor() {
|
||||
this.conversionCache = new Map(); // Cache converted board IDs
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a board has been converted
|
||||
* @param {string} boardId - The board ID
|
||||
* @returns {boolean} - True if board has been converted
|
||||
*/
|
||||
isBoardConverted(boardId) {
|
||||
return globalConvertedBoards.has(boardId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a board needs conversion
|
||||
* @param {string} boardId - The board ID to check
|
||||
* @returns {boolean} - True if board needs conversion
|
||||
*/
|
||||
needsConversion(boardId) {
|
||||
if (this.conversionCache.has(boardId)) {
|
||||
return false; // Already converted
|
||||
}
|
||||
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) return false;
|
||||
|
||||
// Check if any lists in this board don't have swimlaneId
|
||||
const lists = ReactiveCache.getLists({
|
||||
boardId: boardId,
|
||||
$or: [
|
||||
{ swimlaneId: { $exists: false } },
|
||||
{ swimlaneId: '' },
|
||||
{ swimlaneId: null }
|
||||
]
|
||||
});
|
||||
|
||||
return lists.length > 0;
|
||||
} catch (error) {
|
||||
console.error('Error checking if board needs conversion:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a board from old structure to new structure
|
||||
* @param {string} boardId - The board ID to convert
|
||||
* @returns {Promise<boolean>} - True if conversion was successful
|
||||
*/
|
||||
async convertBoard(boardId) {
|
||||
// Check if board has already been converted
|
||||
if (this.isBoardConverted(boardId)) {
|
||||
console.log(`Board ${boardId} has already been converted, skipping`);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.conversionCache.has(boardId)) {
|
||||
return true; // Already converted
|
||||
}
|
||||
|
||||
isConverting.set(true);
|
||||
conversionProgress.set(0);
|
||||
conversionStatus.set('Starting board conversion...');
|
||||
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) {
|
||||
throw new Error('Board not found');
|
||||
}
|
||||
|
||||
// Get the default swimlane for this board
|
||||
const defaultSwimlane = board.getDefaultSwimline();
|
||||
if (!defaultSwimlane) {
|
||||
throw new Error('No default swimlane found for board');
|
||||
}
|
||||
|
||||
// Get all lists that need conversion
|
||||
const listsToConvert = ReactiveCache.getLists({
|
||||
boardId: boardId,
|
||||
$or: [
|
||||
{ swimlaneId: { $exists: false } },
|
||||
{ swimlaneId: '' },
|
||||
{ swimlaneId: null }
|
||||
]
|
||||
});
|
||||
|
||||
if (listsToConvert.length === 0) {
|
||||
this.conversionCache.set(boardId, true);
|
||||
globalConvertedBoards.add(boardId); // Mark board as converted
|
||||
isConverting.set(false);
|
||||
console.log(`Board ${boardId} has no lists to convert, marked as converted`);
|
||||
return true;
|
||||
}
|
||||
|
||||
conversionStatus.set(`Converting ${listsToConvert.length} lists...`);
|
||||
|
||||
const startTime = Date.now();
|
||||
const totalLists = listsToConvert.length;
|
||||
let convertedLists = 0;
|
||||
|
||||
// Convert lists in batches to avoid blocking the UI
|
||||
const batchSize = 10;
|
||||
for (let i = 0; i < listsToConvert.length; i += batchSize) {
|
||||
const batch = listsToConvert.slice(i, i + batchSize);
|
||||
|
||||
// Process batch
|
||||
await this.processBatch(batch, defaultSwimlane._id);
|
||||
|
||||
convertedLists += batch.length;
|
||||
const progress = Math.round((convertedLists / totalLists) * 100);
|
||||
conversionProgress.set(progress);
|
||||
|
||||
// Calculate estimated time remaining
|
||||
const elapsed = Date.now() - startTime;
|
||||
const rate = convertedLists / elapsed; // lists per millisecond
|
||||
const remaining = totalLists - convertedLists;
|
||||
const estimatedMs = remaining / rate;
|
||||
|
||||
conversionStatus.set(`Converting list ${convertedLists} of ${totalLists}...`);
|
||||
conversionEstimatedTime.set(this.formatTime(estimatedMs));
|
||||
|
||||
// Allow UI to update
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
// Mark as converted
|
||||
this.conversionCache.set(boardId, true);
|
||||
globalConvertedBoards.add(boardId); // Mark board as converted
|
||||
|
||||
conversionStatus.set('Board conversion completed!');
|
||||
conversionProgress.set(100);
|
||||
console.log(`Board ${boardId} conversion completed and marked as converted`);
|
||||
|
||||
// Clear status after a delay
|
||||
setTimeout(() => {
|
||||
isConverting.set(false);
|
||||
conversionStatus.set('');
|
||||
conversionProgress.set(0);
|
||||
conversionEstimatedTime.set('');
|
||||
}, 2000);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error converting board:', error);
|
||||
conversionStatus.set(`Conversion failed: ${error.message}`);
|
||||
isConverting.set(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a batch of lists for conversion
|
||||
* @param {Array} batch - Array of lists to convert
|
||||
* @param {string} defaultSwimlaneId - Default swimlane ID
|
||||
*/
|
||||
async processBatch(batch, defaultSwimlaneId) {
|
||||
const updates = batch.map(list => ({
|
||||
_id: list._id,
|
||||
swimlaneId: defaultSwimlaneId
|
||||
}));
|
||||
|
||||
// Update lists in batch
|
||||
updates.forEach(update => {
|
||||
Lists.update(update._id, {
|
||||
$set: { swimlaneId: update.swimlaneId }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time in milliseconds to human readable format
|
||||
* @param {number} ms - Time in milliseconds
|
||||
* @returns {string} - Formatted time string
|
||||
*/
|
||||
formatTime(ms) {
|
||||
if (ms < 1000) {
|
||||
return `${Math.round(ms)}ms`;
|
||||
}
|
||||
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
const remainingMinutes = minutes % 60;
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`;
|
||||
} else if (minutes > 0) {
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
} else {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear conversion cache (useful for testing)
|
||||
*/
|
||||
clearCache() {
|
||||
this.conversionCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const boardConverter = new BoardConverter();
|
||||
@@ -0,0 +1,798 @@
|
||||
/**
|
||||
* Migration Manager
|
||||
* Handles all database migrations as steps during board loading
|
||||
* with detailed progress tracking and background persistence
|
||||
*/
|
||||
|
||||
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 migrationCurrentStep = new ReactiveVar('');
|
||||
export const migrationSteps = new ReactiveVar([]);
|
||||
export const isMigrating = new ReactiveVar(false);
|
||||
export const migrationEstimatedTime = new ReactiveVar('');
|
||||
|
||||
class MigrationManager {
|
||||
constructor() {
|
||||
this.migrationCache = new Map(); // Cache completed migrations
|
||||
this.steps = this.initializeMigrationSteps();
|
||||
this.currentStepIndex = 0;
|
||||
this.startTime = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all migration steps with their details
|
||||
*/
|
||||
initializeMigrationSteps() {
|
||||
return [
|
||||
{
|
||||
id: 'board-background-color',
|
||||
name: 'Board Background Colors',
|
||||
description: 'Setting up board background colors',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-cardcounterlist-allowed',
|
||||
name: 'Card Counter List Settings',
|
||||
description: 'Adding card counter list permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-boardmemberlist-allowed',
|
||||
name: 'Board Member List Settings',
|
||||
description: 'Adding board member list permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'lowercase-board-permission',
|
||||
name: 'Board Permission Standardization',
|
||||
description: 'Converting board permissions to lowercase',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'change-attachments-type-for-non-images',
|
||||
name: 'Attachment Type Standardization',
|
||||
description: 'Updating attachment types for non-images',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'card-covers',
|
||||
name: 'Card Covers System',
|
||||
description: 'Setting up card cover functionality',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'use-css-class-for-boards-colors',
|
||||
name: 'Board Color CSS Classes',
|
||||
description: 'Converting board colors to CSS classes',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'denormalize-star-number-per-board',
|
||||
name: 'Board Star Counts',
|
||||
description: 'Calculating star counts per board',
|
||||
weight: 3,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-member-isactive-field',
|
||||
name: 'Member Activity Status',
|
||||
description: 'Adding member activity tracking',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-sort-checklists',
|
||||
name: 'Checklist Sorting',
|
||||
description: 'Adding sort order to checklists',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-swimlanes',
|
||||
name: 'Swimlanes System',
|
||||
description: 'Setting up swimlanes functionality',
|
||||
weight: 4,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-views',
|
||||
name: 'Board Views',
|
||||
description: 'Adding board view options',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-checklist-items',
|
||||
name: 'Checklist Items',
|
||||
description: 'Setting up checklist items system',
|
||||
weight: 3,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-card-types',
|
||||
name: 'Card Types',
|
||||
description: 'Adding card type functionality',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-custom-fields-to-cards',
|
||||
name: 'Custom Fields',
|
||||
description: 'Adding custom fields to cards',
|
||||
weight: 3,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-requester-field',
|
||||
name: 'Requester Field',
|
||||
description: 'Adding requester field to cards',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-assigner-field',
|
||||
name: 'Assigner Field',
|
||||
description: 'Adding assigner field to cards',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-parent-field-to-cards',
|
||||
name: 'Card Parent Relationships',
|
||||
description: 'Adding parent field to cards',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-subtasks-boards',
|
||||
name: 'Subtasks Boards',
|
||||
description: 'Setting up subtasks board functionality',
|
||||
weight: 3,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-subtasks-sort',
|
||||
name: 'Subtasks Sorting',
|
||||
description: 'Adding sort order to subtasks',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-subtasks-allowed',
|
||||
name: 'Subtasks Permissions',
|
||||
description: 'Adding subtasks permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-authenticationMethod',
|
||||
name: 'Authentication Methods',
|
||||
description: 'Adding authentication method tracking',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'remove-tag',
|
||||
name: 'Remove Tag Field',
|
||||
description: 'Removing deprecated tag field',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'remove-customFields-references-broken',
|
||||
name: 'Fix Custom Fields References',
|
||||
description: 'Fixing broken custom field references',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-product-name',
|
||||
name: 'Product Name Settings',
|
||||
description: 'Adding product name configuration',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-hide-logo',
|
||||
name: 'Hide Logo Setting',
|
||||
description: 'Adding hide logo option',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-hide-card-counter-list',
|
||||
name: 'Hide Card Counter Setting',
|
||||
description: 'Adding hide card counter option',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-hide-board-member-list',
|
||||
name: 'Hide Board Member List Setting',
|
||||
description: 'Adding hide board member list option',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-displayAuthenticationMethod',
|
||||
name: 'Display Authentication Method',
|
||||
description: 'Adding authentication method display option',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-defaultAuthenticationMethod',
|
||||
name: 'Default Authentication Method',
|
||||
description: 'Setting default authentication method',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-templates',
|
||||
name: 'Board Templates',
|
||||
description: 'Setting up board templates system',
|
||||
weight: 3,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'fix-circular-reference_',
|
||||
name: 'Fix Circular References',
|
||||
description: 'Fixing circular references in cards',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'mutate-boardIds-in-customfields',
|
||||
name: 'Custom Fields Board IDs',
|
||||
description: 'Updating board IDs in custom fields',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-missing-created-and-modified',
|
||||
name: 'Missing Timestamps',
|
||||
description: 'Adding missing created and modified timestamps',
|
||||
weight: 4,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'fix-incorrect-dates',
|
||||
name: 'Fix Incorrect Dates',
|
||||
description: 'Correcting incorrect date values',
|
||||
weight: 3,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-assignee',
|
||||
name: 'Assignee Field',
|
||||
description: 'Adding assignee field to cards',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-profile-showDesktopDragHandles',
|
||||
name: 'Desktop Drag Handles',
|
||||
description: 'Adding desktop drag handles preference',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-profile-hiddenMinicardLabelText',
|
||||
name: 'Hidden Minicard Labels',
|
||||
description: 'Adding hidden minicard label text preference',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-receiveddate-allowed',
|
||||
name: 'Received Date Permissions',
|
||||
description: 'Adding received date permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-startdate-allowed',
|
||||
name: 'Start Date Permissions',
|
||||
description: 'Adding start date permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-duedate-allowed',
|
||||
name: 'Due Date Permissions',
|
||||
description: 'Adding due date permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-enddate-allowed',
|
||||
name: 'End Date Permissions',
|
||||
description: 'Adding end date permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-members-allowed',
|
||||
name: 'Members Permissions',
|
||||
description: 'Adding members permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-assignee-allowed',
|
||||
name: 'Assignee Permissions',
|
||||
description: 'Adding assignee permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-labels-allowed',
|
||||
name: 'Labels Permissions',
|
||||
description: 'Adding labels permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-checklists-allowed',
|
||||
name: 'Checklists Permissions',
|
||||
description: 'Adding checklists permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-attachments-allowed',
|
||||
name: 'Attachments Permissions',
|
||||
description: 'Adding attachments permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-comments-allowed',
|
||||
name: 'Comments Permissions',
|
||||
description: 'Adding comments permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-assigned-by-allowed',
|
||||
name: 'Assigned By Permissions',
|
||||
description: 'Adding assigned by permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-requested-by-allowed',
|
||||
name: 'Requested By Permissions',
|
||||
description: 'Adding requested by permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-activities-allowed',
|
||||
name: 'Activities Permissions',
|
||||
description: 'Adding activities permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-description-title-allowed',
|
||||
name: 'Description Title Permissions',
|
||||
description: 'Adding description title permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-description-text-allowed',
|
||||
name: 'Description Text Permissions',
|
||||
description: 'Adding description text permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-description-text-allowed-on-minicard',
|
||||
name: 'Minicard Description Permissions',
|
||||
description: 'Adding minicard description permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-sort-field-to-boards',
|
||||
name: 'Board Sort Field',
|
||||
description: 'Adding sort field to boards',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-default-profile-view',
|
||||
name: 'Default Profile View',
|
||||
description: 'Setting default profile view',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-hide-logo-by-default',
|
||||
name: 'Hide Logo Default',
|
||||
description: 'Setting hide logo as default',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-hide-card-counter-list-by-default',
|
||||
name: 'Hide Card Counter Default',
|
||||
description: 'Setting hide card counter as default',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-hide-board-member-list-by-default',
|
||||
name: 'Hide Board Member List Default',
|
||||
description: 'Setting hide board member list as default',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-card-number-allowed',
|
||||
name: 'Card Number Permissions',
|
||||
description: 'Adding card number permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'assign-boardwise-card-numbers',
|
||||
name: 'Board Card Numbers',
|
||||
description: 'Assigning board-wise card numbers',
|
||||
weight: 3,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-card-details-show-lists',
|
||||
name: 'Card Details Show Lists',
|
||||
description: 'Adding card details show lists option',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'migrate-attachments-collectionFS-to-ostrioFiles',
|
||||
name: 'Migrate Attachments to Meteor-Files',
|
||||
description: 'Migrating attachments from CollectionFS to Meteor-Files',
|
||||
weight: 8,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'migrate-avatars-collectionFS-to-ostrioFiles',
|
||||
name: 'Migrate Avatars to Meteor-Files',
|
||||
description: 'Migrating avatars from CollectionFS to Meteor-Files',
|
||||
weight: 6,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'migrate-attachment-drop-index-cardId',
|
||||
name: 'Drop Attachment Index',
|
||||
description: 'Dropping old attachment index',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'migrate-attachment-migration-fix-source-import',
|
||||
name: 'Fix Attachment Source Import',
|
||||
description: 'Fixing attachment source import field',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'attachment-cardCopy-fix-boardId-etc',
|
||||
name: 'Fix Attachment Card Copy',
|
||||
description: 'Fixing attachment card copy board IDs',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'remove-unused-planning-poker',
|
||||
name: 'Remove Planning Poker',
|
||||
description: 'Removing unused planning poker fields',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'remove-user-profile-hiddenSystemMessages',
|
||||
name: 'Remove Hidden System Messages',
|
||||
description: 'Removing hidden system messages field',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'remove-user-profile-hideCheckedItems',
|
||||
name: 'Remove Hide Checked Items',
|
||||
description: 'Removing hide checked items field',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'migrate-lists-to-per-swimlane',
|
||||
name: 'Migrate Lists to Per-Swimlane',
|
||||
description: 'Migrating lists to per-swimlane structure',
|
||||
weight: 5,
|
||||
completed: false,
|
||||
progress: 0
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any migrations need to be run for a specific board
|
||||
*/
|
||||
needsMigration(boardId = null) {
|
||||
if (boardId) {
|
||||
// Check if specific board needs migration based on version
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
return !board || !board.migrationVersion || board.migrationVersion < 1;
|
||||
}
|
||||
|
||||
// Check if any migration step is not completed (global migrations)
|
||||
return this.steps.some(step => !step.completed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total weight of all migrations
|
||||
*/
|
||||
getTotalWeight() {
|
||||
return this.steps.reduce((total, step) => total + step.weight, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get completed weight
|
||||
*/
|
||||
getCompletedWeight() {
|
||||
return this.steps.reduce((total, step) => {
|
||||
return total + (step.completed ? step.weight : step.progress * step.weight / 100);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a board as migrated
|
||||
*/
|
||||
markBoardAsMigrated(boardId) {
|
||||
try {
|
||||
Meteor.call('boardMigration.markAsMigrated', boardId, 'full_board_migration', (error, result) => {
|
||||
if (error) {
|
||||
console.error('Failed to mark board as migrated:', error);
|
||||
} else {
|
||||
console.log('Board marked as migrated:', boardId);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error marking board as migrated:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start migration process using cron system
|
||||
*/
|
||||
async startMigration() {
|
||||
if (isMigrating.get()) {
|
||||
return; // Already migrating
|
||||
}
|
||||
|
||||
isMigrating.set(true);
|
||||
migrationSteps.set([...this.steps]);
|
||||
this.startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Start server-side cron migrations
|
||||
Meteor.call('cron.startAllMigrations', (error, result) => {
|
||||
if (error) {
|
||||
console.error('Failed to start cron migrations:', error);
|
||||
migrationStatus.set(`Migration failed: ${error.message}`);
|
||||
isMigrating.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Poll for progress updates
|
||||
this.pollCronMigrationProgress();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
migrationStatus.set(`Migration failed: ${error.message}`);
|
||||
isMigrating.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll for cron migration progress updates
|
||||
*/
|
||||
pollCronMigrationProgress() {
|
||||
const pollInterval = setInterval(() => {
|
||||
Meteor.call('cron.getMigrationProgress', (error, result) => {
|
||||
if (error) {
|
||||
console.error('Failed to get cron migration progress:', error);
|
||||
clearInterval(pollInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
migrationProgress.set(result.progress);
|
||||
migrationStatus.set(result.status);
|
||||
migrationCurrentStep.set(result.currentStep);
|
||||
migrationSteps.set(result.steps);
|
||||
isMigrating.set(result.isMigrating);
|
||||
|
||||
// Update local steps
|
||||
if (result.steps) {
|
||||
this.steps = result.steps;
|
||||
}
|
||||
|
||||
// If migration is complete, stop polling
|
||||
if (!result.isMigrating && result.progress === 100) {
|
||||
clearInterval(pollInterval);
|
||||
|
||||
// Clear status after delay
|
||||
setTimeout(() => {
|
||||
migrationStatus.set('');
|
||||
migrationProgress.set(0);
|
||||
migrationEstimatedTime.set('');
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 1000); // Poll every second
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single migration step
|
||||
*/
|
||||
async runMigrationStep(step) {
|
||||
// Simulate migration progress
|
||||
const steps = 10;
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
step.progress = (i / steps) * 100;
|
||||
this.updateProgress();
|
||||
|
||||
// Simulate work
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
// In a real implementation, this would call the actual migration
|
||||
// For now, we'll simulate the migration
|
||||
// Running migration step
|
||||
}
|
||||
|
||||
/**
|
||||
* Update progress variables
|
||||
*/
|
||||
updateProgress() {
|
||||
const totalWeight = this.getTotalWeight();
|
||||
const completedWeight = this.getCompletedWeight();
|
||||
const progress = Math.round((completedWeight / totalWeight) * 100);
|
||||
|
||||
migrationProgress.set(progress);
|
||||
migrationSteps.set([...this.steps]);
|
||||
|
||||
// Calculate estimated time remaining
|
||||
if (this.startTime && progress > 0) {
|
||||
const elapsed = Date.now() - this.startTime;
|
||||
const rate = progress / elapsed; // progress per millisecond
|
||||
const remaining = 100 - progress;
|
||||
const estimatedMs = remaining / rate;
|
||||
migrationEstimatedTime.set(this.formatTime(estimatedMs));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time in milliseconds to human readable format
|
||||
*/
|
||||
formatTime(ms) {
|
||||
if (ms < 1000) {
|
||||
return `${Math.round(ms)}ms`;
|
||||
}
|
||||
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
const remainingMinutes = minutes % 60;
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`;
|
||||
} else if (minutes > 0) {
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
} else {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear migration cache (for testing)
|
||||
*/
|
||||
clearCache() {
|
||||
this.migrationCache.clear();
|
||||
this.steps.forEach(step => {
|
||||
step.completed = false;
|
||||
step.progress = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const migrationManager = new MigrationManager();
|
||||
@@ -0,0 +1,121 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
// Centralized secure DOMPurify configuration to prevent XSS and CSS injection attacks
|
||||
export function getSecureDOMPurifyConfig() {
|
||||
return {
|
||||
// Block dangerous elements that can cause XSS and CSS injection
|
||||
FORBID_TAGS: [
|
||||
'svg', 'defs', 'use', 'g', 'symbol', 'marker', 'pattern', 'mask', 'clipPath',
|
||||
'linearGradient', 'radialGradient', 'stop', 'animate', 'animateTransform',
|
||||
'animateMotion', 'set', 'switch', 'foreignObject', 'script', 'style', 'link',
|
||||
'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,
|
||||
// Remove dangerous protocols
|
||||
ALLOW_UNKNOWN_PROTOCOLS: false,
|
||||
// Sanitize URLs to prevent malicious content loading
|
||||
SANITIZE_DOM: true,
|
||||
// Remove dangerous elements completely
|
||||
KEEP_CONTENT: false,
|
||||
// Additional security measures
|
||||
ADD_ATTR: [],
|
||||
// Block data URIs that could contain malicious content
|
||||
ALLOW_DATA_ATTR: false,
|
||||
// Custom hook to further sanitize content
|
||||
HOOKS: {
|
||||
uponSanitizeElement: function(node, data) {
|
||||
// Block any remaining dangerous elements
|
||||
const dangerousTags = ['svg', 'style', 'script', 'link', 'meta', 'iframe', 'object', 'embed', 'applet'];
|
||||
if (node.tagName && dangerousTags.includes(node.tagName.toLowerCase())) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('Blocked potentially dangerous element:', node.tagName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Block img tags with SVG data URIs
|
||||
if (node.tagName && node.tagName.toLowerCase() === 'img') {
|
||||
const src = node.getAttribute('src');
|
||||
if (src && (src.startsWith('data:image/svg') || src.endsWith('.svg'))) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('Blocked potentially malicious SVG image:', src);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Block elements with dangerous attributes
|
||||
const dangerousAttrs = ['style', 'onload', 'onerror', 'onclick', 'onmouseover', 'onfocus', 'onblur'];
|
||||
for (const attr of dangerousAttrs) {
|
||||
if (node.hasAttribute && node.hasAttribute(attr)) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('Blocked element with dangerous attribute:', node.tagName, attr);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
uponSanitizeAttribute: function(node, data) {
|
||||
// Block style attributes completely
|
||||
if (data.attrName === 'style') {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('Blocked style attribute');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Block class and id attributes that might be used for CSS injection
|
||||
if (data.attrName === 'class' || data.attrName === 'id') {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('Blocked class/id attribute:', data.attrName, data.attrValue);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Block data attributes
|
||||
if (data.attrName && data.attrName.startsWith('data-')) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('Blocked data attribute:', data.attrName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Convenience function for secure sanitization
|
||||
export function sanitizeHTML(html) {
|
||||
return DOMPurify.sanitize(html, getSecureDOMPurifyConfig());
|
||||
}
|
||||
|
||||
// Convenience function for sanitizing text (no HTML)
|
||||
export function sanitizeText(text) {
|
||||
return DOMPurify.sanitize(text, {
|
||||
ALLOWED_TAGS: [],
|
||||
ALLOWED_ATTR: [],
|
||||
KEEP_CONTENT: true
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
|
||||
/**
|
||||
* Global upload progress manager for drag-and-drop file uploads
|
||||
* Tracks upload progress across all cards and provides reactive data
|
||||
*/
|
||||
class UploadProgressManager {
|
||||
constructor() {
|
||||
// Map of cardId -> array of upload objects
|
||||
this.cardUploads = new ReactiveVar(new Map());
|
||||
|
||||
// Map of uploadId -> upload object for easy lookup
|
||||
this.uploadMap = new ReactiveVar(new Map());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new upload to track
|
||||
* @param {string} cardId - The card ID
|
||||
* @param {Object} uploader - The uploader object from Attachments.insert
|
||||
* @param {File} file - The file being uploaded
|
||||
* @returns {string} uploadId - Unique identifier for this upload
|
||||
*/
|
||||
addUpload(cardId, uploader, file) {
|
||||
const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const upload = {
|
||||
id: uploadId,
|
||||
cardId: cardId,
|
||||
file: file,
|
||||
uploader: uploader,
|
||||
progress: new ReactiveVar(0),
|
||||
status: new ReactiveVar('uploading'), // 'uploading', 'completed', 'error'
|
||||
error: new ReactiveVar(null),
|
||||
startTime: Date.now(),
|
||||
endTime: null
|
||||
};
|
||||
|
||||
// Update card uploads
|
||||
const currentCardUploads = this.cardUploads.get();
|
||||
const cardUploads = currentCardUploads.get(cardId) || [];
|
||||
cardUploads.push(upload);
|
||||
currentCardUploads.set(cardId, cardUploads);
|
||||
this.cardUploads.set(currentCardUploads);
|
||||
|
||||
// Update upload map
|
||||
const currentUploadMap = this.uploadMap.get();
|
||||
currentUploadMap.set(uploadId, upload);
|
||||
this.uploadMap.set(currentUploadMap);
|
||||
|
||||
// Set up uploader event listeners
|
||||
uploader.on('progress', (progress) => {
|
||||
upload.progress.set(progress);
|
||||
});
|
||||
|
||||
uploader.on('uploaded', (error, fileRef) => {
|
||||
upload.status.set(error ? 'error' : 'completed');
|
||||
upload.endTime = Date.now();
|
||||
upload.error.set(error);
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Upload ${uploadId} completed:`, error ? 'error' : 'success');
|
||||
}
|
||||
|
||||
// Remove from tracking after a delay to show completion
|
||||
setTimeout(() => {
|
||||
this.removeUpload(uploadId);
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
uploader.on('error', (error) => {
|
||||
upload.status.set('error');
|
||||
upload.endTime = Date.now();
|
||||
upload.error.set(error);
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Upload ${uploadId} failed:`, error);
|
||||
}
|
||||
|
||||
// Remove from tracking after a delay to show error
|
||||
setTimeout(() => {
|
||||
this.removeUpload(uploadId);
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Added upload ${uploadId} for card ${cardId}: ${file.name}`);
|
||||
}
|
||||
|
||||
return uploadId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an upload from tracking
|
||||
* @param {string} uploadId - The upload ID to remove
|
||||
*/
|
||||
removeUpload(uploadId) {
|
||||
const upload = this.uploadMap.get().get(uploadId);
|
||||
if (!upload) return;
|
||||
|
||||
const cardId = upload.cardId;
|
||||
|
||||
// Remove from card uploads
|
||||
const currentCardUploads = this.cardUploads.get();
|
||||
const cardUploads = currentCardUploads.get(cardId) || [];
|
||||
const filteredCardUploads = cardUploads.filter(u => u.id !== uploadId);
|
||||
|
||||
if (filteredCardUploads.length === 0) {
|
||||
currentCardUploads.delete(cardId);
|
||||
} else {
|
||||
currentCardUploads.set(cardId, filteredCardUploads);
|
||||
}
|
||||
this.cardUploads.set(currentCardUploads);
|
||||
|
||||
// Remove from upload map
|
||||
const currentUploadMap = this.uploadMap.get();
|
||||
currentUploadMap.delete(uploadId);
|
||||
this.uploadMap.set(currentUploadMap);
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Removed upload ${uploadId} from tracking`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all uploads for a specific card
|
||||
* @param {string} cardId - The card ID
|
||||
* @returns {Array} Array of upload objects
|
||||
*/
|
||||
getUploadsForCard(cardId) {
|
||||
return this.cardUploads.get().get(cardId) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload count for a specific card
|
||||
* @param {string} cardId - The card ID
|
||||
* @returns {number} Number of active uploads
|
||||
*/
|
||||
getUploadCountForCard(cardId) {
|
||||
return this.getUploadsForCard(cardId).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a card has any active uploads
|
||||
* @param {string} cardId - The card ID
|
||||
* @returns {boolean} True if card has active uploads
|
||||
*/
|
||||
hasActiveUploads(cardId) {
|
||||
return this.getUploadCountForCard(cardId) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all uploads across all cards
|
||||
* @returns {Array} Array of all upload objects
|
||||
*/
|
||||
getAllUploads() {
|
||||
const allUploads = [];
|
||||
this.cardUploads.get().forEach(cardUploads => {
|
||||
allUploads.push(...cardUploads);
|
||||
});
|
||||
return allUploads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all uploads (useful for cleanup)
|
||||
*/
|
||||
clearAllUploads() {
|
||||
this.cardUploads.set(new Map());
|
||||
this.uploadMap.set(new Map());
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
const uploadProgressManager = new UploadProgressManager();
|
||||
|
||||
export default uploadProgressManager;
|
||||
|
||||
+187
-10
@@ -51,6 +51,146 @@ Utils = {
|
||||
const ret = ReactiveCache.getCard(cardId);
|
||||
return ret;
|
||||
},
|
||||
|
||||
// Zoom and mobile mode utilities
|
||||
getZoomLevel() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && user.profile && user.profile.zoomLevel !== undefined) {
|
||||
return user.profile.zoomLevel;
|
||||
}
|
||||
// For non-logged-in users, check localStorage
|
||||
const stored = localStorage.getItem('wekan-zoom-level');
|
||||
return stored ? parseFloat(stored) : 1.0;
|
||||
},
|
||||
|
||||
setZoomLevel(level) {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user) {
|
||||
// Update user profile
|
||||
user.setZoomLevel(level);
|
||||
} else {
|
||||
// Store in localStorage for non-logged-in users
|
||||
localStorage.setItem('wekan-zoom-level', level.toString());
|
||||
}
|
||||
Utils.applyZoomLevel(level);
|
||||
|
||||
// Trigger reactive updates for UI components
|
||||
Session.set('wekan-zoom-level', level);
|
||||
},
|
||||
|
||||
getMobileMode() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && user.profile && user.profile.mobileMode !== undefined) {
|
||||
return user.profile.mobileMode;
|
||||
}
|
||||
// For non-logged-in users, check localStorage
|
||||
const stored = localStorage.getItem('wekan-mobile-mode');
|
||||
return stored ? stored === 'true' : false;
|
||||
},
|
||||
|
||||
setMobileMode(enabled) {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user) {
|
||||
// Update user profile
|
||||
user.setMobileMode(enabled);
|
||||
} else {
|
||||
// Store in localStorage for non-logged-in users
|
||||
localStorage.setItem('wekan-mobile-mode', enabled.toString());
|
||||
}
|
||||
Utils.applyMobileMode(enabled);
|
||||
// Trigger reactive updates for UI components
|
||||
Session.set('wekan-mobile-mode', enabled);
|
||||
},
|
||||
|
||||
applyZoomLevel(level) {
|
||||
const boardWrapper = document.querySelector('.board-wrapper');
|
||||
const body = document.body;
|
||||
const isMobileMode = body.classList.contains('mobile-mode');
|
||||
|
||||
if (boardWrapper) {
|
||||
if (isMobileMode) {
|
||||
// On mobile mode, only apply zoom to text and icons, not the entire layout
|
||||
// Remove any existing transform from board-wrapper
|
||||
boardWrapper.style.transform = '';
|
||||
boardWrapper.style.transformOrigin = '';
|
||||
|
||||
// Apply zoom to text and icon elements instead
|
||||
const textElements = boardWrapper.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, div, .minicard, .list-header-name, .board-header-btn, .fa, .icon');
|
||||
textElements.forEach(element => {
|
||||
element.style.transform = `scale(${level})`;
|
||||
element.style.transformOrigin = 'center';
|
||||
});
|
||||
|
||||
// Reset board-canvas height
|
||||
const boardCanvas = document.querySelector('.board-canvas');
|
||||
if (boardCanvas) {
|
||||
boardCanvas.style.height = '';
|
||||
}
|
||||
} else {
|
||||
// Desktop mode: apply zoom to entire board-wrapper as before
|
||||
boardWrapper.style.transform = `scale(${level})`;
|
||||
boardWrapper.style.transformOrigin = 'top left';
|
||||
|
||||
// If zoom is 50% or lower, make board wrapper full width like content
|
||||
if (level <= 0.5) {
|
||||
boardWrapper.style.width = '100%';
|
||||
boardWrapper.style.maxWidth = '100%';
|
||||
boardWrapper.style.margin = '0';
|
||||
} else {
|
||||
// Reset to normal width for higher zoom levels
|
||||
boardWrapper.style.width = '';
|
||||
boardWrapper.style.maxWidth = '';
|
||||
boardWrapper.style.margin = '';
|
||||
}
|
||||
|
||||
// Adjust container height to prevent scroll issues
|
||||
const boardCanvas = document.querySelector('.board-canvas');
|
||||
if (boardCanvas) {
|
||||
boardCanvas.style.height = `${100 / level}%`;
|
||||
|
||||
// For high zoom levels (200%+), enable both horizontal and vertical scrolling
|
||||
if (level >= 2.0) {
|
||||
boardCanvas.style.overflowX = 'auto';
|
||||
boardCanvas.style.overflowY = 'auto';
|
||||
// Ensure the content area can scroll both horizontally and vertically
|
||||
const content = document.querySelector('#content');
|
||||
if (content) {
|
||||
content.style.overflowX = 'auto';
|
||||
content.style.overflowY = 'auto';
|
||||
}
|
||||
} else {
|
||||
// Reset overflow for normal zoom levels
|
||||
boardCanvas.style.overflowX = '';
|
||||
boardCanvas.style.overflowY = '';
|
||||
const content = document.querySelector('#content');
|
||||
if (content) {
|
||||
content.style.overflowX = '';
|
||||
content.style.overflowY = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
applyMobileMode(enabled) {
|
||||
const body = document.body;
|
||||
if (enabled) {
|
||||
body.classList.add('mobile-mode');
|
||||
body.classList.remove('desktop-mode');
|
||||
} else {
|
||||
body.classList.add('desktop-mode');
|
||||
body.classList.remove('mobile-mode');
|
||||
}
|
||||
},
|
||||
|
||||
initializeUserSettings() {
|
||||
// Apply saved settings on page load
|
||||
const zoomLevel = Utils.getZoomLevel();
|
||||
const mobileMode = Utils.getMobileMode();
|
||||
Utils.applyZoomLevel(zoomLevel);
|
||||
Utils.applyMobileMode(mobileMode);
|
||||
},
|
||||
getCurrentList() {
|
||||
const listId = this.getCurrentListId();
|
||||
let ret = null;
|
||||
@@ -287,22 +427,50 @@ Utils = {
|
||||
},
|
||||
|
||||
windowResizeDep: new Tracker.Dependency(),
|
||||
|
||||
// in fact, what we really care is screen size
|
||||
// large mobile device like iPad or android Pad has a big screen, it should also behave like a desktop
|
||||
// in a small window (even on desktop), Wekan run in compact mode.
|
||||
// we can easily debug with a small window of desktop browser. :-)
|
||||
isMiniScreen() {
|
||||
// OLD WINDOW WIDTH DETECTION:
|
||||
this.windowResizeDep.depend();
|
||||
return $(window).width() <= 800;
|
||||
// Show mobile view when:
|
||||
// 1. Screen width is 800px or less (matches CSS media queries)
|
||||
// 2. Mobile phones in portrait mode
|
||||
// 3. iPad in very small screens (≤ 600px)
|
||||
const isSmallScreen = window.innerWidth <= 800;
|
||||
const isVerySmallScreen = window.innerWidth <= 600;
|
||||
const isPortrait = window.innerWidth < window.innerHeight || window.matchMedia("(orientation: portrait)").matches;
|
||||
const isMobilePhone = /Mobile|Android|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) && !/iPad/i.test(navigator.userAgent);
|
||||
const isIPhone = /iPhone|iPod/i.test(navigator.userAgent);
|
||||
const isIPad = /iPad/i.test(navigator.userAgent);
|
||||
const isUbuntuTouch = /Ubuntu/i.test(navigator.userAgent);
|
||||
|
||||
// For iPhone: always show mobile view regardless of orientation
|
||||
// For other mobile phones: show mobile view in portrait, desktop view in landscape
|
||||
// For iPad: show mobile view only in very small screens (≤ 600px)
|
||||
// For Ubuntu Touch: smartphones behave like mobile phones, tablets like iPad
|
||||
// For desktop: show mobile view when screen width <= 800px
|
||||
if (isIPhone) {
|
||||
return true; // iPhone: always mobile view
|
||||
} else if (isMobilePhone) {
|
||||
return isPortrait; // Other mobile phones: portrait = mobile, landscape = desktop
|
||||
} else if (isIPad) {
|
||||
return isVerySmallScreen; // iPad: only very small screens get mobile view
|
||||
} else if (isUbuntuTouch) {
|
||||
// Ubuntu Touch: smartphones (≤ 600px) behave like mobile phones, tablets (> 600px) like iPad
|
||||
if (isVerySmallScreen) {
|
||||
return isPortrait; // Ubuntu Touch smartphone: portrait = mobile, landscape = desktop
|
||||
} else {
|
||||
return isVerySmallScreen; // Ubuntu Touch tablet: only very small screens get mobile view
|
||||
}
|
||||
} else {
|
||||
return isSmallScreen; // Desktop: based on 800px screen width
|
||||
}
|
||||
},
|
||||
|
||||
isTouchScreen() {
|
||||
|
||||
// NEW TOUCH DEVICE DETECTION:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
|
||||
|
||||
var hasTouchScreen = false;
|
||||
if ("maxTouchPoints" in navigator) {
|
||||
hasTouchScreen = navigator.maxTouchPoints > 0;
|
||||
@@ -328,12 +496,19 @@ Utils = {
|
||||
|
||||
// returns if desktop drag handles are enabled
|
||||
isShowDesktopDragHandles() {
|
||||
//const currentUser = ReactiveCache.getCurrentUser();
|
||||
//if (currentUser) {
|
||||
// return (currentUser.profile || {}).showDesktopDragHandles;
|
||||
//} else if (window.localStorage.getItem('showDesktopDragHandles')) {
|
||||
if (this.isTouchScreen()) {
|
||||
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;
|
||||
}
|
||||
@@ -341,8 +516,10 @@ Utils = {
|
||||
|
||||
// returns if mini screen or desktop drag handles
|
||||
isTouchScreenOrShowDesktopDragHandles() {
|
||||
// Always enable drag handles for mobile screens (touch devices)
|
||||
return this.isTouchScreen() || this.isMiniScreen();
|
||||
//return this.isTouchScreen() || this.isShowDesktopDragHandles();
|
||||
return this.isShowDesktopDragHandles();
|
||||
//return this.isShowDesktopDragHandles();
|
||||
},
|
||||
|
||||
calculateIndexData(prevData, nextData, nItems = 1) {
|
||||
|
||||
@@ -57,6 +57,7 @@ AccountsTemplates.addFields([
|
||||
displayName: 'username',
|
||||
required: true,
|
||||
minLength: 2,
|
||||
autocomplete: 'username',
|
||||
},
|
||||
emailField,
|
||||
passwordField,
|
||||
|
||||
@@ -240,6 +240,25 @@ FlowRouter.route('/global-search', {
|
||||
},
|
||||
});
|
||||
|
||||
// Mobile Bookmarks page
|
||||
FlowRouter.route('/bookmarks', {
|
||||
name: 'bookmarks',
|
||||
triggersEnter: [AccountsTemplates.ensureSignedIn],
|
||||
action() {
|
||||
Filter.reset();
|
||||
Session.set('sortBy', '');
|
||||
EscapeActions.executeUpTo('popup-close');
|
||||
|
||||
Utils.manageCustomUI();
|
||||
Utils.manageMatomo();
|
||||
|
||||
BlazeLayout.render('defaultLayout', {
|
||||
headerBar: 'boardListHeaderBar',
|
||||
content: 'bookmarks',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
FlowRouter.route('/broken-cards', {
|
||||
name: 'broken-cards',
|
||||
action() {
|
||||
|
||||
+48
-15
@@ -1,14 +1,14 @@
|
||||
# Note: Do not add single quotes '' to variables. Having spaces still works without quotes where required.
|
||||
#---------------------------------------------------------------------------------------------------------
|
||||
# ==== CREATING USERS AND LOGGING IN TO WEKAN ====
|
||||
# https://github.com/wekan/wekan/wiki/Adding-users
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Login/Adding-users.md
|
||||
#---------------------------------------------------------------------------------------------------------
|
||||
# ==== FORGOT PASSWORD ====
|
||||
# https://github.com/wekan/wekan/wiki/Forgot-Password
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Login/Forgot-Password.md
|
||||
#---------------------------------------------------------------------------------------------------------
|
||||
# ==== Upgrading Wekan to new version =====
|
||||
# NOTE: MongoDB has changed from 3.x to 4.x, in that case you need backup/restore with --noIndexRestore
|
||||
# see https://github.com/wekan/wekan/wiki/Backup
|
||||
# see https://github.com/wekan/wekan/tree/main/docs/Backup
|
||||
# 1) Stop Wekan:
|
||||
# docker compose stop
|
||||
# 2) Remove old Wekan app (wekan-app only, not that wekan-db container that has all your data)
|
||||
@@ -46,7 +46,7 @@
|
||||
# docker compose stop
|
||||
# ----------------------------------------------------------------------------------
|
||||
# ===== INSIDE DOCKER CONTAINERS, AND BACKUP/RESTORE ====
|
||||
# https://github.com/wekan/wekan/wiki/Backup
|
||||
# https://github.com/wekan/wekan/tree/main/docs/Backup
|
||||
# If really necessary, repair MongoDB: https://github.com/wekan/wekan-mongodb/issues/6#issuecomment-424004116
|
||||
# 1) Going inside containers:
|
||||
# a) Wekan app, does not contain data
|
||||
@@ -163,9 +163,12 @@ services:
|
||||
# ap-southeast-1,ap-northeast-1,sa-east-1
|
||||
#
|
||||
#- S3='{"s3":{"key": "xxx", "secret": "xxx", "bucket": "xxx", "region": "xxx"}}'
|
||||
#- S3_SECRET_FILE=/run/secrets/s3_secret
|
||||
#-----------------------------------------------------------------
|
||||
# ==== MONGO_URL ====
|
||||
- MONGO_URL=mongodb://wekandb:27017/wekan
|
||||
#- MONGO_URL=mongodb://username:password@wekandb:27017/wekan
|
||||
#- MONGO_PASSWORD_FILE=/run/secrets/mongo_password
|
||||
#---------------------------------------------------------------
|
||||
# ==== ROOT_URL SETTING ====
|
||||
# Change ROOT_URL to your real Wekan URL, for example:
|
||||
@@ -183,7 +186,7 @@ services:
|
||||
# ==== EMAIL SETTINGS ====
|
||||
# Email settings are only at MAIL_URL and MAIL_FROM.
|
||||
# Admin Panel has test button, but it's not used for settings.
|
||||
# see https://github.com/wekan/wekan/wiki/Troubleshooting-Mail
|
||||
# see https://github.com/wekan/wekan/blob/main/docs/Email/Troubleshooting-Mail.md
|
||||
# For SSL in email, change smtp:// to smtps://
|
||||
# NOTE: Special characters need to be url-encoded in MAIL_URL.
|
||||
# You can encode those characters for example at: https://www.urlencoder.org
|
||||
@@ -194,6 +197,7 @@ services:
|
||||
#- MAIL_SERVICE=Outlook365
|
||||
#- MAIL_SERVICE_USER=firstname.lastname@hotmail.com
|
||||
#- MAIL_SERVICE_PASSWORD=SecretPassword
|
||||
#- MAIL_SERVICE_PASSWORD_FILE=/run/secrets/mail_service_password
|
||||
#---------------------------------------------------------------
|
||||
# https://github.com/wekan/wekan/issues/3585#issuecomment-1021522132
|
||||
# Add more Node heap, this is done by default at Dockerfile:
|
||||
@@ -228,7 +232,7 @@ services:
|
||||
#- APM_APP_SECRET=
|
||||
#---------------------------------------------------------------
|
||||
# ==== OPTIONAL: LOGS AND STATS ====
|
||||
# https://github.com/wekan/wekan/wiki/Logs
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Features/Logs.md
|
||||
#
|
||||
# Daily export of Wekan changes as JSON to Logstash and ElasticSearch / Kibana (ELK)
|
||||
# https://github.com/wekan/wekan-logstash
|
||||
@@ -252,7 +256,7 @@ services:
|
||||
#---------------------------------------------------------------
|
||||
# ==== WEKAN API AND EXPORT BOARD ====
|
||||
# Wekan Export Board works when WITH_API=true.
|
||||
# https://github.com/wekan/wekan/wiki/REST-API
|
||||
# https://github.com/wekan/wekan/blob/main/docs/API/REST-API.md
|
||||
# https://github.com/wekan/wekan-gogs
|
||||
# If you disable Wekan API with false, Export Board does not work.
|
||||
- WITH_API=true
|
||||
@@ -361,7 +365,7 @@ services:
|
||||
#- TRUSTED_URL=https://intra.example.com
|
||||
#-----------------------------------------------------------------
|
||||
# ==== METRICS ALLOWED IP ADDRESSES ====
|
||||
# https://github.com/wekan/wekan/wiki/Metrics
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Features/Metrics.md
|
||||
#- METRICS_ALLOWED_IP_ADDRESSES=192.168.0.100,192.168.0.200
|
||||
#-----------------------------------------------------------------
|
||||
# ==== OUTGOING WEBHOOKS ====
|
||||
@@ -372,14 +376,14 @@ services:
|
||||
#- DEBUG=true
|
||||
#---------------------------------------------
|
||||
# ==== AUTOLOGIN WITH OIDC/OAUTH2 ====
|
||||
# https://github.com/wekan/wekan/wiki/autologin
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Login/autologin.md
|
||||
#- OIDC_REDIRECTION_ENABLED=true
|
||||
#-----------------------------------------------------------------
|
||||
# ==== OAUTH2 ORACLE on premise identity manager OIM ====
|
||||
#- ORACLE_OIM_ENABLED=true
|
||||
#-----------------------------------------------------------------
|
||||
# ==== OAUTH2 AZURE ====
|
||||
# https://github.com/wekan/wekan/wiki/Azure
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Login/Azure/Azure.md
|
||||
# 1) Register the application with Azure. Make sure you capture
|
||||
# the application ID as well as generate a secret key.
|
||||
# 2) Configure the environment variables. This differs slightly
|
||||
@@ -397,6 +401,7 @@ services:
|
||||
#- OAUTH2_CLIENT_ID=xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx
|
||||
# Secret key generated during app registration:
|
||||
#- OAUTH2_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
|
||||
#- OAUTH2_SERVER_URL=https://login.microsoftonline.com/
|
||||
#- OAUTH2_AUTH_ENDPOINT=/oauth2/v2.0/authorize
|
||||
#- OAUTH2_USERINFO_ENDPOINT=https://graph.microsoft.com/oidc/userinfo
|
||||
@@ -423,6 +428,7 @@ services:
|
||||
#- OAUTH2_CLIENT_ID=xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx
|
||||
# Secret key generated during app registration:
|
||||
#- OAUTH2_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
|
||||
#- OAUTH2_SERVER_URL=https://your-nextcloud.tld
|
||||
#- OAUTH2_AUTH_ENDPOINT=/index.php/apps/oauth2/authorize
|
||||
#- OAUTH2_USERINFO_ENDPOINT=/ocs/v2.php/cloud/user?format=json
|
||||
@@ -437,7 +443,7 @@ services:
|
||||
#- OAUTH2_EMAIL_MAP=email
|
||||
#-----------------------------------------------------------------
|
||||
# ==== OAUTH2 KEYCLOAK ====
|
||||
# https://github.com/wekan/wekan/wiki/Keycloak <== MAPPING INFO, REQUIRED
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Login/Keycloak/Keycloak.md <== MAPPING INFO, REQUIRED
|
||||
#- OAUTH2_ENABLED=true
|
||||
# OAuth2 login style: popup or redirect.
|
||||
#- OAUTH2_LOGIN_STYLE=redirect
|
||||
@@ -447,6 +453,7 @@ services:
|
||||
#- OAUTH2_USERINFO_ENDPOINT=/realms/<keycloak realm>/protocol/openid-connect/userinfo
|
||||
#- OAUTH2_TOKEN_ENDPOINT=/realms/<keycloak realm>/protocol/openid-connect/token
|
||||
#- OAUTH2_SECRET=<keycloak client secret>
|
||||
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
|
||||
#- OAUTH2_ID_MAP=sub
|
||||
#- OAUTH2_USERNAME_MAP=preferred_username
|
||||
#- OAUTH2_EMAIL_MAP=email
|
||||
@@ -454,16 +461,17 @@ services:
|
||||
#-----------------------------------------------------------------
|
||||
# ==== OAUTH2 DOORKEEPER ====
|
||||
# https://github.com/wekan/wekan/issues/1874
|
||||
# https://github.com/wekan/wekan/wiki/OAuth2
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Login/OAuth2.md2
|
||||
# Enable the OAuth2 connection
|
||||
#- OAUTH2_ENABLED=true
|
||||
# OAuth2 docs: https://github.com/wekan/wekan/wiki/OAuth2
|
||||
# OAuth2 docs: https://github.com/wekan/wekan/blob/main/docs/Login/OAuth2.md
|
||||
# OAuth2 login style: popup or redirect.
|
||||
#- OAUTH2_LOGIN_STYLE=redirect
|
||||
# OAuth2 Client ID.
|
||||
#- OAUTH2_CLIENT_ID=abcde12345
|
||||
# OAuth2 Secret.
|
||||
#- OAUTH2_SECRET=54321abcde
|
||||
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
|
||||
# OAuth2 Server URL.
|
||||
#- OAUTH2_SERVER_URL=https://chat.example.com
|
||||
# OAuth2 Authorization Endpoint.
|
||||
@@ -486,8 +494,8 @@ services:
|
||||
#- OAUTH2_EMAIL_MAP=
|
||||
#-----------------------------------------------------------------
|
||||
# ==== LDAP: UNCOMMENT ALL TO ENABLE LDAP ====
|
||||
# https://github.com/wekan/wekan/wiki/LDAP
|
||||
# For Snap settings see https://github.com/wekan/wekan-snap/wiki/Supported-settings-keys
|
||||
# https://github.com/wekan/wekan/blob/main/docs/Login/LDAP.md
|
||||
# For Snap settings see https://github.com/wekan/wekan/blob/main/docs/Platforms/FOSS/Snap/Supported-settings-keys.md
|
||||
# Most settings work both on Snap and Docker below.
|
||||
# Note: Do not add single quotes '' to variables. Having spaces still works without quotes where required.
|
||||
#
|
||||
@@ -570,6 +578,7 @@ services:
|
||||
#
|
||||
# The password for the search user
|
||||
#- LDAP_AUTHENTIFICATION_PASSWORD=pwd
|
||||
#- LDAP_AUTHENTIFICATION_PASSWORD_FILE=/run/secrets/ldap_auth_password
|
||||
#
|
||||
# Enable logs for the module
|
||||
#- LDAP_LOG_ENABLED=true
|
||||
@@ -725,6 +734,13 @@ services:
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- wekan-files:/data:rw
|
||||
# Secrets are optional. Uncomment below and ensure files exist to use them.
|
||||
#secrets:
|
||||
# - ldap_auth_password
|
||||
# - oauth2_secret
|
||||
# - mail_service_password
|
||||
# - mongo_password
|
||||
# - s3_secret
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# ==== OPTIONAL: SHARE DATABASE TO OFFICE LAN AND REMOTE VPN ====
|
||||
@@ -786,3 +802,20 @@ volumes:
|
||||
networks:
|
||||
wekan-tier:
|
||||
driver: bridge
|
||||
|
||||
# Docker Compose Secrets
|
||||
# Create secret files on the host system before running docker-compose up
|
||||
# Example: echo "your_password_here" > ldap_auth_password.txt
|
||||
# Then use: docker-compose up -d
|
||||
# Secrets are optional. Uncomment to enable and ensure files exist at provided paths.
|
||||
#secrets:
|
||||
# ldap_auth_password:
|
||||
# file: ./secrets/ldap_auth_password.txt
|
||||
# oauth2_secret:
|
||||
# file: ./secrets/oauth2_secret.txt
|
||||
# mail_service_password:
|
||||
# file: ./secrets/mail_service_password.txt
|
||||
# mongo_password:
|
||||
# file: ./secrets/mongo_password.txt
|
||||
# s3_secret:
|
||||
# file: ./secrets/s3_secret.txt
|
||||
Loaded 100 of 370 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user