mirror of
https://github.com/wekan/wekan.git
synced 2026-11-08 11:25:19 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
289ff9fb17 | ||
|
|
6bfceb80d6 | ||
|
|
d0d9195c1c | ||
|
|
fd238d34c3 | ||
|
|
c6763f496c | ||
|
|
16b1b307fc | ||
|
|
e13fc2028a | ||
|
|
5ed9fc944a | ||
|
|
43b0a8de1e | ||
|
|
896f1222bd | ||
|
|
684411f139 | ||
|
|
193c799539 | ||
|
|
337f601852 | ||
|
|
0d9744bb8d | ||
|
|
4ec273068e | ||
|
|
2dbf9c7f39 | ||
|
|
e693fe5212 | ||
|
|
29bd491e85 | ||
|
|
e1e64e92b4 | ||
|
|
566190a97e | ||
|
|
1c3bb8448b | ||
|
|
bc5881ff90 | ||
|
|
1657a96943 | ||
|
|
ee4eeedaa2 | ||
|
|
20b7dd33fa | ||
|
|
53a23258cf | ||
|
|
46607d4299 | ||
|
|
4f5013ec48 | ||
|
|
e2993617bc | ||
|
|
f297cd989b | ||
|
|
d01b3a3d92 | ||
|
|
4c1bcca5f7 | ||
|
|
5dd0b11f7c | ||
|
|
d23e666aec | ||
|
|
fd41207c54 | ||
|
|
783733623e | ||
|
|
762e742b24 | ||
|
|
5c9e5045f8 | ||
|
|
0a8ee08d56 | ||
|
|
87d69358dc | ||
|
|
5c7f455285 | ||
|
|
03e8a6bf66 | ||
|
|
959f15cbe0 | ||
|
|
d87ec3954b | ||
|
|
97f74ea3b5 | ||
|
|
278f7b869a | ||
|
|
e71e20036c | ||
|
|
9aeeac51a8 | ||
|
|
522a7e88e8 | ||
|
|
593cab5911 | ||
|
|
a4eda40c89 | ||
|
|
f7da598ad2 | ||
|
|
f32a0f7e10 | ||
|
|
e478ce7309 | ||
|
|
f60df9b0be | ||
|
|
b68dd01893 | ||
|
|
7489120eb9 | ||
|
|
f238c02af7 | ||
|
|
3ecc62c960 | ||
|
|
5e951845eb | ||
|
|
3865c3fdea | ||
|
|
8acf5d947d | ||
|
|
c399aeeee6 | ||
|
|
2078c21819 | ||
|
|
25ed0c3067 | ||
|
|
0e962bba3f | ||
|
|
99570e213f | ||
|
|
1cf93f7e52 | ||
|
|
bfac55c30c | ||
|
|
18a11d1210 | ||
|
|
77d574a627 | ||
|
|
ff5a286b53 | ||
|
|
cf14bd7db9 | ||
|
|
413b4ef405 | ||
|
|
7da3334c47 | ||
|
|
63da3d3c97 | ||
|
|
1cf705484e | ||
|
|
e11a2654c7 | ||
|
|
1405047ba7 | ||
|
|
2b22dd0bef | ||
|
|
3447011b4d | ||
|
|
7a37811883 | ||
|
|
66c5b85c30 | ||
|
|
7b5dd6549d | ||
|
|
929326ba22 | ||
|
|
56f04dcab7 | ||
|
|
128e482d7a | ||
|
|
fb66fa95cf | ||
|
|
0ebfe1d775 | ||
|
|
c4c6e34ec4 | ||
|
|
9b49eb1c43 | ||
|
|
e6685eb0b4 | ||
|
|
0134b39520 | ||
|
|
79e5a93b42 | ||
|
|
0b7606194e | ||
|
|
f0ceb8d72b | ||
|
|
0b2cc01e6f | ||
|
|
87c0495710 | ||
|
|
0539ac8a23 | ||
|
|
6de53fb0c1 | ||
|
|
79d58098f4 | ||
|
|
8922a87f2a | ||
|
|
ada7e35333 | ||
|
|
1990e60396 | ||
|
|
f10daf2ae9 | ||
|
|
987c5c22b5 | ||
|
|
65c6674196 | ||
|
|
a58c572961 | ||
|
|
bea96680ee | ||
|
|
f30c32e897 | ||
|
|
32c2d95756 | ||
|
|
fc2a8da2c8 | ||
|
|
c365e2cf6a | ||
|
|
278ca10a0f | ||
|
|
d1c4a7290a | ||
|
|
f851f409d7 | ||
|
|
e114ffde89 | ||
|
|
5fcd43df39 | ||
|
|
06678678d0 | ||
|
|
3ed7dd3761 | ||
|
|
ce50929661 | ||
|
|
a43727f0bc | ||
|
|
6f14c9a8c6 | ||
|
|
59a7261e33 | ||
|
|
de0cc0ce45 | ||
|
|
10fff07af3 | ||
|
|
460263f21b | ||
|
|
ae76ba79d0 | ||
|
|
23b77ed14a | ||
|
|
f4097bfda1 | ||
|
|
31a238d91b | ||
|
|
4ac8c5d129 | ||
|
|
21319c523d | ||
|
|
c95017d560 | ||
|
|
83c8d0c2bb | ||
|
|
04df435baf | ||
|
|
c18780d5bd | ||
|
|
4e67398440 | ||
|
|
bb2d2dfebe | ||
|
|
0bd8f32316 | ||
|
|
beddec7cb9 | ||
|
|
77cf4af47e | ||
|
|
3eaeac9e7e | ||
|
|
e855bb7ede | ||
|
|
92ea2a0f0c | ||
|
|
91555657e2 | ||
|
|
9c1388a496 | ||
|
|
376ecf3043 | ||
|
|
3bf6a4c821 | ||
|
|
bf18a5b737 | ||
|
|
74e6deabe8 | ||
|
|
e20321100b | ||
|
|
8cc659ac92 | ||
|
|
217dd1e3df | ||
|
|
0fe4ecb480 | ||
|
|
d0388a2c0e | ||
|
|
6d94f4edb4 | ||
|
|
17d3a34054 | ||
|
|
0b0349d34d | ||
|
|
7bf55675f6 | ||
|
|
212a46c078 | ||
|
|
0fdeaf7994 | ||
|
|
d6e6c558d6 | ||
|
|
3a6463541c | ||
|
|
e6895b3dee | ||
|
|
49a0654bd1 | ||
|
|
196bc49df9 | ||
|
|
c249bacaa2 | ||
|
|
68d679c6c7 | ||
|
|
8bf19dada2 | ||
|
|
6909fb85e7 | ||
|
|
99e0121b0e | ||
|
|
78f2c9bf78 | ||
|
|
e24a4fe717 | ||
|
|
36c2148ecb | ||
|
|
7030814546 | ||
|
|
970d9e664b | ||
|
|
3d8b41be8c | ||
|
|
780a75a2bb | ||
|
|
d140ac347f | ||
|
|
5d53aa9904 | ||
|
|
95a3a4baff | ||
|
|
009d7f8329 | ||
|
|
f4dd67eb3b | ||
|
|
6b78c88cb9 | ||
|
|
c450740d78 | ||
|
|
0eac733cdb | ||
|
|
0f9f13d1d9 | ||
|
|
0ef3ca6dd6 | ||
|
|
238e331278 | ||
|
|
08e7092c36 | ||
|
|
d954922c90 | ||
|
|
83546ed248 | ||
|
|
b30134be78 | ||
|
|
40a20207bd | ||
|
|
4e09c23b21 | ||
|
|
088b01587f | ||
|
|
828800a301 | ||
|
|
45a568aea7 | ||
|
|
2fa5a88449 | ||
|
|
e50072bc32 | ||
|
|
876d879596 | ||
|
|
eb1add1f21 | ||
|
|
79b144ae76 | ||
|
|
8296c12a57 | ||
|
|
9173c62599 | ||
|
|
c73362aae6 | ||
|
|
b0fd098cea | ||
|
|
c91e56a504 | ||
|
|
1ae4305237 | ||
|
|
0896117b5a | ||
|
|
4a8323c96f | ||
|
|
242c1f27ba | ||
|
|
b658832f59 | ||
|
|
8b6c21ff90 | ||
|
|
7e49057262 | ||
|
|
1196bebaab | ||
|
|
79708827c7 | ||
|
|
71a15613c7 | ||
|
|
f81c9ab58f | ||
|
|
8560f3d7c2 | ||
|
|
b0bef07079 | ||
|
|
a69ed23251 | ||
|
|
a56cccfeb3 | ||
|
|
f64f9c98d3 | ||
|
|
4a25311bf7 | ||
|
|
fc61397455 | ||
|
|
edb736acf0 | ||
|
|
002f8bc7bb | ||
|
|
fcc461b156 | ||
|
|
b8bbd8fa39 | ||
|
|
69c1145a87 | ||
|
|
22615501d1 | ||
|
|
46cccccd3a | ||
|
|
a6dd320fd2 | ||
|
|
d73a200439 | ||
|
|
a03aa027cb | ||
|
|
d44beb0228 | ||
|
|
284fcff144 | ||
|
|
d39930eb77 | ||
|
|
0651f745bc | ||
|
|
387e10980e | ||
|
|
b8ebfc67e3 | ||
|
|
194f4556aa | ||
|
|
e4ae5173fc | ||
|
|
4a26ff5412 | ||
|
|
f1b12cc668 | ||
|
|
c0f430b0b3 | ||
|
|
b4586ba970 | ||
|
|
33300f303e | ||
|
|
c59789dfb5 | ||
|
|
e27abadf67 | ||
|
|
3d2c249f21 | ||
|
|
6d8f830a70 | ||
|
|
ee6607dc9d | ||
|
|
9f5f2d1dba | ||
|
|
fbfb37e272 | ||
|
|
2bb4b88cb5 | ||
|
|
212232a5c0 | ||
|
|
6f25928145 | ||
|
|
eb797932fe | ||
|
|
fa6cbd1666 | ||
|
|
a268d6167a | ||
|
|
4936e9fc47 | ||
|
|
62ff72c607 | ||
|
|
8c324ff5e9 | ||
|
|
67632637c2 | ||
|
|
c839ae2bf3 | ||
|
|
b2caad45b7 | ||
|
|
154c3cc31f | ||
|
|
f9b716fd61 | ||
|
|
9b266cc353 | ||
|
|
77e08a8f61 | ||
|
|
a568dc4208 | ||
|
|
2530126f27 | ||
|
|
b7b7cd2de8 | ||
|
|
62956f2819 | ||
|
|
2fcf2788e5 | ||
|
|
f27cb82ca6 | ||
|
|
7db8afc76c | ||
|
|
a4ec295ffa | ||
|
|
33d1a89761 | ||
|
|
4caef872b0 | ||
|
|
be8c47c26a | ||
|
|
a222bad2d7 | ||
|
|
8858912ed4 | ||
|
|
c67a91a416 | ||
|
|
8d02a5d758 | ||
|
|
e17bb2b5e3 | ||
|
|
3e492107cb | ||
|
|
5b2c5942ef | ||
|
|
8db30cf182 | ||
|
|
0e94189604 | ||
|
|
391e7fc811 | ||
|
|
9ab9215c6b | ||
|
|
e911879b6b | ||
|
|
2ecd746378 | ||
|
|
f18a925c26 | ||
|
|
ee9228ab6d | ||
|
|
a11978016d | ||
|
|
36c8afc8ea | ||
|
|
667eb393dd | ||
|
|
3d79d5ca5b | ||
|
|
c9d838170b | ||
|
|
7d87955de3 | ||
|
|
e0bb7f2ca1 | ||
|
|
10aa310033 | ||
|
|
3caf5f6a73 | ||
|
|
b5acfdaec3 | ||
|
|
97dc41d076 | ||
|
|
a0eeee9d63 | ||
|
|
81f18f4807 | ||
|
|
88caf17515 | ||
|
|
85d994c2fb | ||
|
|
e97f8fb206 | ||
|
|
54f008fc5d | ||
|
|
56ce505d0c | ||
|
|
78fda5345a | ||
|
|
52836b241d | ||
|
|
38cde52187 | ||
|
|
7bc3b73b05 | ||
|
|
a229d53471 | ||
|
|
4d74f7080f | ||
|
|
3830365ecd | ||
|
|
b82d5761ed | ||
|
|
e8d971489b | ||
|
|
1b35cd8b4f | ||
|
|
68c82d941c | ||
|
|
09b62ffa40 | ||
|
|
a31eebdc39 | ||
|
|
8480bee00b | ||
|
|
c4f049e320 | ||
|
|
8a66f4c746 | ||
|
|
8061759d2c | ||
|
|
052937ff0a | ||
|
|
3e0c124d4a | ||
|
|
21e43e8b56 | ||
|
|
832e44c7e9 | ||
|
|
7c17acd340 | ||
|
|
3d47446dfe | ||
|
|
046830b355 | ||
|
|
7dcdc054a4 | ||
|
|
91d0cbf5ed | ||
|
|
9e5758a508 | ||
|
|
dad7f2ae35 | ||
|
|
99aeb90cdb | ||
|
|
10f1a4964b | ||
|
|
cfed82e70c | ||
|
|
eb056cf59f | ||
|
|
bc8988c9d7 | ||
|
|
ed2565e9b7 | ||
|
|
9e06803310 | ||
|
|
f46fb0272e | ||
|
|
e8826e71a0 | ||
|
|
4cf2381c66 | ||
|
|
201f8d0a41 | ||
|
|
b7a903473d | ||
|
|
f5d72bae8a | ||
|
|
6fd9eb88d3 | ||
|
|
4b2009229c | ||
|
|
5a62a6b3b1 | ||
|
|
e722a6c17c | ||
|
|
1aacb92a3b | ||
|
|
f9d9c454dc | ||
|
|
c476aa4a10 | ||
|
|
7deff5697c | ||
|
|
8e8150478f | ||
|
|
29f93a8886 | ||
|
|
d84b5bb9be | ||
|
|
25e6eefba0 | ||
|
|
c37bd8f962 | ||
|
|
b30fa05a93 | ||
|
|
11b3061c98 | ||
|
|
81c17c1960 | ||
|
|
7e59bb8b63 | ||
|
|
c117936edd | ||
|
|
d04457bb37 | ||
|
|
5d0583ef54 | ||
|
|
7cbef0b3c7 | ||
|
|
0ca3422a46 | ||
|
|
be501c32fa | ||
|
|
c379de7d7e | ||
|
|
e7cecc9c7f | ||
|
|
93a48d724d | ||
|
|
7c540c5f5b | ||
|
|
3658f17556 | ||
|
|
4f879334c9 | ||
|
|
29bf7aa221 | ||
|
|
84af30f9b5 | ||
|
|
f10fe2fa5b | ||
|
|
be42c5771f | ||
|
|
b60ea7bc12 | ||
|
|
d0f721b481 | ||
|
|
b9dae272e7 | ||
|
|
ef5a6c5bad | ||
|
|
d30a2f5737 | ||
|
|
efa6b0eb11 | ||
|
|
06be232017 | ||
|
|
9570c3af3f | ||
|
|
a6d77ba0e0 | ||
|
|
36f33b5304 | ||
|
|
52b73cacc5 | ||
|
|
3797e952ed | ||
|
|
ac95ce3aad | ||
|
|
c3bc95dc3f | ||
|
|
68d7f2d899 | ||
|
|
8d9facf9a8 | ||
|
|
0c34382f87 | ||
|
|
ec1f2cb219 | ||
|
|
b31065a9d0 | ||
|
|
0b259c2d3d | ||
|
|
ddc8e9fcab | ||
|
|
0f4fc5d227 | ||
|
|
1951b0417a | ||
|
|
8516ab9a72 | ||
|
|
f194046ec2 | ||
|
|
0f545fd8a7 | ||
|
|
2f8b930391 | ||
|
|
6481e8bbd6 | ||
|
|
dfbe6056fc | ||
|
|
88add30060 | ||
|
|
9ab9db9611 | ||
|
|
0567c1aacc | ||
|
|
b8bffce9c3 | ||
|
|
5211bf3152 | ||
|
|
64b716d623 | ||
|
|
7c58d6c3df | ||
|
|
4dad59ba12 | ||
|
|
bf9a929e13 | ||
|
|
b1ba19e837 | ||
|
|
3cc93cb240 | ||
|
|
e908ec0a2f | ||
|
|
41dbd148c2 | ||
|
|
64e524f27a | ||
|
|
b347233c1b | ||
|
|
66f2a8feed | ||
|
|
87323967b3 | ||
|
|
b0ad9320f2 | ||
|
|
0f73724fa8 | ||
|
|
e1c3720715 | ||
|
|
8f49df189a | ||
|
|
11fc595a00 | ||
|
|
cd895cfef7 | ||
|
|
3da9c1f4ee | ||
|
|
8505afdf9e | ||
|
|
0f393c5c8f | ||
|
|
f7b7da9c3f | ||
|
|
3fd9413b3f | ||
|
|
a026b27dc5 | ||
|
|
103457ada4 | ||
|
|
65b0fe0ab4 | ||
|
|
2c200ab5b9 | ||
|
|
1a174e357a | ||
|
|
87835255ae | ||
|
|
1b10527d1b | ||
|
|
fd0b18431f | ||
|
|
39daf05c88 | ||
|
|
047c860ea3 | ||
|
|
29121d44c8 | ||
|
|
aafcae48fb | ||
|
|
27dbc30c22 | ||
|
|
f2c6e77eb2 | ||
|
|
18e1979477 | ||
|
|
8cbcb52451 | ||
|
|
efdf49b29f | ||
|
|
9bd8e04d9f | ||
|
|
32b46051fa | ||
|
|
ecbe3856d2 | ||
|
|
6eda842fea | ||
|
|
04a4f92b45 | ||
|
|
d90707d5dc | ||
|
|
0a1ce60ed7 | ||
|
|
5f41f83ca3 | ||
|
|
9774b20c12 | ||
|
|
2a3c8ad7b2 | ||
|
|
0a333ce51a | ||
|
|
897c89740d | ||
|
|
98bbc03fcf | ||
|
|
343971eb86 | ||
|
|
d0bf777156 | ||
|
|
9a56d1720b | ||
|
|
b39e4e0ee2 | ||
|
|
3ce7ed2a04 | ||
|
|
1a64d5a1fc | ||
|
|
61be887618 | ||
|
|
f37ba5f581 | ||
|
|
89185d39ca | ||
|
|
9baa8f69df | ||
|
|
74d2228fbd | ||
|
|
7c1c4623d3 | ||
|
|
0d6f145961 | ||
|
|
8362a8a2eb | ||
|
|
2722961397 | ||
|
|
8dee03d542 | ||
|
|
53a6891d59 | ||
|
|
cc2b110ba4 | ||
|
|
4065b7c91f | ||
|
|
5434775ba1 | ||
|
|
b7837bf05b | ||
|
|
4a28218359 | ||
|
|
f259be34a4 | ||
|
|
47883aa91d | ||
|
|
327a35b82f | ||
|
|
5704470be8 | ||
|
|
2b60bc6ce0 | ||
|
|
8bc4426f53 | ||
|
|
35f74fd042 | ||
|
|
efe7146186 | ||
|
|
45d2b2b2e7 | ||
|
|
5104dbaf43 | ||
|
|
aed519ac5f | ||
|
|
40f06e1d8d | ||
|
|
6ceeace0e7 | ||
|
|
14af25915c | ||
|
|
7ba4642883 | ||
|
|
9680e2393e | ||
|
|
2e50c81154 | ||
|
|
260258983d | ||
|
|
94549d762a | ||
|
|
4f07f43d81 | ||
|
|
d7f32047fc | ||
|
|
53fba52404 | ||
|
|
884f040d64 | ||
|
|
7c6688b63e | ||
|
|
0245682e3e | ||
|
|
4e8f1b7dde | ||
|
|
723ffbd524 |
No files matched your search
@@ -38,7 +38,6 @@ ENV \
|
||||
AVATARS_UPLOAD_EXTERNAL_PROGRAM="" \
|
||||
AVATARS_UPLOAD_MIME_TYPES="" \
|
||||
AVATARS_UPLOAD_MAX_SIZE=0 \
|
||||
RICHER_CARD_COMMENT_EDITOR=false \
|
||||
CARD_OPENED_WEBHOOK_ENABLED=false \
|
||||
MAX_IMAGE_PIXEL="" \
|
||||
IMAGE_COMPRESS_RATIO="" \
|
||||
|
||||
@@ -29,7 +29,6 @@ services:
|
||||
- MONGO_URL=mongodb://wekandb-dev:27017/wekan
|
||||
- ROOT_URL=http://localhost:3000
|
||||
- WITH_API=true
|
||||
- RICHER_CARD_COMMENT_EDITOR=true
|
||||
- BROWSER_POLICY_ENABLED=true
|
||||
- WRITABLE_PATH=/data
|
||||
depends_on:
|
||||
|
||||
@@ -150,7 +150,6 @@ jobs:
|
||||
PORT=3000 \
|
||||
WRITABLE_PATH="$PWD/wekan-writable" \
|
||||
WITH_API=true \
|
||||
RICHER_CARD_COMMENT_EDITOR=false \
|
||||
node output/bundle/main.js > wekan-run.log 2>&1 &
|
||||
# The production server serves the client JS statically, so once :3000
|
||||
# responds the bundle is being served. Wait up to 10 minutes.
|
||||
@@ -269,7 +268,6 @@ jobs:
|
||||
PORT=3000 \
|
||||
WRITABLE_PATH="$PWD/wekan-writable" \
|
||||
WITH_API=true \
|
||||
RICHER_CARD_COMMENT_EDITOR=false \
|
||||
node output/bundle/main.js > wekan-run.log 2>&1 &
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf http://localhost:3000 > /dev/null; then
|
||||
|
||||
@@ -3900,6 +3900,11 @@ jobs:
|
||||
# continuation comments out the rest of the joined line, leaving
|
||||
# `docker buildx build` with no platforms and no tags. This block is a
|
||||
# shell script like any other, and it was written the wrong way first.
|
||||
# Build once for Docker Hub and GHCR, then copy the published manifest
|
||||
# to Quay in the next step. Quay can switch to read-only mode after
|
||||
# this expensive build starts; a single --push across all three
|
||||
# registries then reports the whole job failed even when the first
|
||||
# two received a complete image.
|
||||
# ONE IMAGE NAME, on three registries. wekan-ondra and wekan-gantt-gpl
|
||||
# are SNAP names - they exist because a snap name cannot be changed once
|
||||
# people have installed it - and there is nothing for them to be as
|
||||
@@ -3921,8 +3926,6 @@ jobs:
|
||||
--build-arg VERSION=${VERSION} \
|
||||
-t wekanteam/wekan:v${VERSION} \
|
||||
-t wekanteam/wekan:latest \
|
||||
-t quay.io/wekan/wekan:v${VERSION} \
|
||||
-t quay.io/wekan/wekan:latest \
|
||||
-t ghcr.io/wekan/wekan:v${VERSION} \
|
||||
-t ghcr.io/wekan/wekan:latest \
|
||||
--push \
|
||||
@@ -3960,8 +3963,7 @@ jobs:
|
||||
# the image deliberately dropped.
|
||||
want="${WANT}"
|
||||
rc=0
|
||||
for image in "wekanteam/wekan:v${VERSION}" "quay.io/wekan/wekan:v${VERSION}" \
|
||||
"ghcr.io/wekan/wekan:v${VERSION}"; do
|
||||
for image in "wekanteam/wekan:v${VERSION}" "ghcr.io/wekan/wekan:v${VERSION}"; do
|
||||
# The format MUST carry the CPU variant. buildx builds linux/arm/v7,
|
||||
# but the registry records that as architecture "arm" with variant
|
||||
# "v7" in a SEPARATE field - so an OS/Architecture-only format prints
|
||||
@@ -3990,6 +3992,45 @@ jobs:
|
||||
done
|
||||
exit $rc
|
||||
|
||||
- name: Copy published manifest to Quay
|
||||
run: |
|
||||
set -uo pipefail
|
||||
# imagetools copies the existing multi-platform manifest without
|
||||
# rebuilding every architecture. A Quay-wide read-only outage is
|
||||
# temporary and affects every user; keep the completed publications
|
||||
# on Docker Hub and GHCR. Any other refusal remains a real failure.
|
||||
if output="$(docker buildx imagetools create \
|
||||
-t "quay.io/wekan/wekan:v${VERSION}" \
|
||||
-t "quay.io/wekan/wekan:latest" \
|
||||
"ghcr.io/wekan/wekan:v${VERSION}" 2>&1)"; then
|
||||
printf '%s\n' "$output"
|
||||
echo "QUAY_PUBLISHED=true" >> "$GITHUB_ENV"
|
||||
else
|
||||
printf '%s\n' "$output"
|
||||
if printf '%s' "$output" | grep -qi 'System is currently read-only.*write operations are currently suspended'; then
|
||||
echo "::warning::Quay is in read-only mode; Docker Hub and GHCR were published and verified. Re-run this Docker job when Quay resumes writes to copy the same release manifest."
|
||||
echo "QUAY_PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "::error::Could not copy the published manifest to Quay; this is not the registry's read-only outage."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Verify Quay manifest platforms
|
||||
if: env.QUAY_PUBLISHED == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
image="quay.io/wekan/wekan:v${VERSION}"
|
||||
got="$(docker buildx imagetools inspect "$image" --format '{{range .Manifest.Manifests}}{{.Platform.OS}}/{{.Platform.Architecture}}{{if .Platform.Variant}}/{{.Platform.Variant}}{{end}} {{end}}')"
|
||||
got="$(printf '%s' "$got" | sed 's#/v8 # #g')"
|
||||
for plat in $WANT; do
|
||||
case " $got " in
|
||||
*" $plat "*) ;;
|
||||
*) echo "::error::$image has no $plat in its manifest (it has:$got)."; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
echo "OK: $image carries:$got"
|
||||
|
||||
# PUSHED IS NOT THE SAME AS PULLABLE BY ANYONE.
|
||||
#
|
||||
# The check above asks the registries with the release's own credentials, so
|
||||
@@ -4055,10 +4096,12 @@ jobs:
|
||||
}
|
||||
# The variant names are commented out with the -t lines they went with;
|
||||
# nothing pushes them, so nothing checks them.
|
||||
for image in "wekanteam/wekan:v${VERSION}" "quay.io/wekan/wekan:v${VERSION}" \
|
||||
"ghcr.io/wekan/wekan:v${VERSION}"; do
|
||||
for image in "wekanteam/wekan:v${VERSION}" "ghcr.io/wekan/wekan:v${VERSION}"; do
|
||||
anon_check "$image"
|
||||
done
|
||||
if [ "${QUAY_PUBLISHED:-false}" = true ]; then
|
||||
anon_check "quay.io/wekan/wekan:v${VERSION}"
|
||||
fi
|
||||
exit $rc
|
||||
|
||||
# Always, as the last step: the run summary then answers "what worked and what
|
||||
@@ -4344,18 +4387,7 @@ jobs:
|
||||
# one still upload cleanly.
|
||||
up=("$asset")
|
||||
[ -f "${asset}.sha256sum" ] && up+=("${asset}.sha256sum")
|
||||
if ! gh release upload --repo "${GITHUB_REPOSITORY:-wekan/wekan}" "v${VERSION}" "${up[@]}" --clobber; then
|
||||
echo "::error::Could not attach $asset to release v${VERSION}: the release must exist first (the release job), and GITHUB_TOKEN must be allowed to write releases here."
|
||||
exit 1
|
||||
fi
|
||||
# Confirm from the OTHER side. An upload that reports success but leaves no
|
||||
# asset behind is the failure nobody notices until a download 404s.
|
||||
gh release view --repo "${GITHUB_REPOSITORY:-wekan/wekan}" "v${VERSION}" --json assets --jq '.assets[].name' > release-assets.txt
|
||||
if ! grep -qxF "$(basename "$asset")" release-assets.txt; then
|
||||
echo "::error::$asset is not listed in release v${VERSION} although the upload reported success. Re-run this job."
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: $(basename "$asset") is attached to release v${VERSION} ($(($(wc -c < "$asset")/1024/1024)) MiB)."
|
||||
bash releases/github-release-upload.sh "${GITHUB_REPOSITORY:-wekan/wekan}" "v${VERSION}" "${up[@]}"
|
||||
|
||||
# Always, as the last step: the run summary then answers "what worked and what
|
||||
# did not" for the whole release at a glance, instead of eighteen job logs. A
|
||||
@@ -4412,7 +4444,7 @@ jobs:
|
||||
# never cancels the others - each publishes the moment it finishes.
|
||||
continue-on-error: true
|
||||
# GitHub's hosted-job ceiling is six hours. The remote command below stops
|
||||
# waiting after five hours on riscv64 and leaves the queued Launchpad build
|
||||
# waiting after five hours across all attempts and leaves the Launchpad build
|
||||
# alive, giving log collection and the summary a full hour to finish cleanly
|
||||
# instead of having GitHub cancel the job at 360 minutes.
|
||||
timeout-minutes: 360
|
||||
@@ -4484,36 +4516,9 @@ jobs:
|
||||
|
||||
- name: Flatten history so the Launchpad push stays small
|
||||
if: steps.bundle.outputs.available == 'true'
|
||||
# snapcraft remote-build pushes the project's git repository to
|
||||
# git.launchpad.net and builds it there. It rejects a SHALLOW clone
|
||||
# (that is why the checkout above is fetch-depth: 0), but it does NOT
|
||||
# need the history - and WeKan's full history is large enough that the
|
||||
# push times out or is refused mid-upload:
|
||||
# Git operation failed with: Could not push 'HEAD' to
|
||||
# git.launchpad.net/.../+git/snapcraft-wekan-<hash>
|
||||
# after ~4-5 minutes, three attempts running, which is how v10.55
|
||||
# riscv64 and v10.64 ppc64el failed. Re-init the repo as ONE commit of
|
||||
# the tagged tree: a COMPLETE repository (git rev-parse
|
||||
# --is-shallow-repository is false, so remote-build accepts it), just
|
||||
# without the history it pushes for nothing. The push is then the source
|
||||
# tree (tens of MB) instead of the whole history (hundreds). The snap
|
||||
# version comes from snapcraft.yaml (verified below), not git-describe,
|
||||
# so dropping the history changes nothing about what is built.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# remote-build derives its Launchpad recipe name from this commit.
|
||||
# Preserve the tagged commit's timestamp so a re-run creates the SAME
|
||||
# root commit and reconnects to the queued build instead of submitting
|
||||
# another riscv64 build behind it.
|
||||
snapshot_date="$(git show -s --format=%cI HEAD)"
|
||||
rm -rf .git
|
||||
git init -q
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add -A
|
||||
GIT_AUTHOR_DATE="$snapshot_date" GIT_COMMITTER_DATE="$snapshot_date" \
|
||||
git commit -q -m "WeKan ${VERSION} snapshot for Launchpad remote-build"
|
||||
echo "Re-initialised as one commit; shallow: $(git rev-parse --is-shallow-repository)"
|
||||
# Snapcraft hashes the project files, not only its commit. Use a complete
|
||||
# one-commit source tree with Git metadata/logs outside the hashed tree.
|
||||
run: bash releases/prepare-launchpad-source.sh "${{ matrix.arch }}"
|
||||
|
||||
- name: Install snapcraft
|
||||
if: steps.bundle.outputs.available == 'true'
|
||||
@@ -4588,6 +4593,34 @@ jobs:
|
||||
}
|
||||
echo "built=false" >> "$GITHUB_OUTPUT"
|
||||
echo "pending=false" >> "$GITHUB_OUTPUT"
|
||||
# A shared deadline, not five hours PER retry. Every architecture can
|
||||
# queue (v11.85 ppc64el/s390x), and TLS recovery must not reset the budget.
|
||||
deadline=$((SECONDS + 18000))
|
||||
export TMPDIR="$PWD/.tools/tmp"
|
||||
remote_wrapper="$PWD/releases/snapcraft-remote-compat.sh"
|
||||
remote_source="$PWD/.tools/tmp/snap-launchpad-source"
|
||||
remote_log="$PWD/.tools/tmp/remote-build-attempt.log"
|
||||
run_remote_build() {
|
||||
local remaining=$((deadline - SECONDS))
|
||||
[ "$remaining" -gt 0 ] || return 124
|
||||
# Isolate the local waiter, and kill it if it ignores TERM. Never
|
||||
# cancel the remote build; a later run can recover the same recipe.
|
||||
(
|
||||
cd "$remote_source" || { echo "::error::Prepared Launchpad source is missing: $remote_source" >&2; exit 1; }
|
||||
timeout --kill-after=30s "${remaining}s" bash "$remote_wrapper" remote-build --launchpad-accept-public-upload \
|
||||
--build-for ${{ matrix.arch }} "$@"
|
||||
) 2>&1 | tee "$remote_log"
|
||||
local remote_rc=${PIPESTATUS[0]}
|
||||
# Downloaded artifacts and build logs also change the project hash.
|
||||
# Move them out before the next recovery attempt, keeping the logs
|
||||
# where the existing diagnostics/artifact collection expect them.
|
||||
local download
|
||||
for download in "$remote_source"/*.snap "$remote_source"/snapcraft-wekan-*.txt; do
|
||||
mv "$download" .
|
||||
done
|
||||
return "$remote_rc"
|
||||
}
|
||||
|
||||
attempts=3
|
||||
for i in $(seq 1 "$attempts"); do
|
||||
# A partial file from a previous attempt must not be mistaken for
|
||||
@@ -4595,22 +4628,13 @@ jobs:
|
||||
# already has, so the truncated one would be uploaded again.
|
||||
rm -f wekan_${VERSION}_${{ matrix.arch }}*.snap *_${{ matrix.arch }}.snap
|
||||
echo "::group::Remote build ${{ matrix.arch }} attempt $i/$attempts"
|
||||
if [ "${{ matrix.arch }}" = riscv64 ]; then
|
||||
# Launchpad's riscv64 queue exceeded GitHub's six-hour job cap in
|
||||
# v11.06. Stop only the LOCAL waiter after five hours; Launchpad
|
||||
# keeps the named build, and the deterministic snapshot above lets
|
||||
# a job re-run reconnect and download it when ready.
|
||||
# Do not use --foreground here. GNU timeout then owns a separate
|
||||
# process group and returns 124 to this shell when it expires.
|
||||
# With --foreground, terminating snapcraft surfaced to Actions as
|
||||
# "The operation was canceled", so the rc=124 pending branch
|
||||
# below never ran and the otherwise healthy matrix leg was red.
|
||||
timeout 300m snapcraft remote-build \
|
||||
--launchpad-accept-public-upload \
|
||||
--build-for ${{ matrix.arch }} && rc=0 || rc=$?
|
||||
else
|
||||
snapcraft remote-build --launchpad-accept-public-upload \
|
||||
--build-for ${{ matrix.arch }} && rc=0 || rc=$?
|
||||
# Always try recovery first, including a re-run of this matrix job.
|
||||
# start_builds replaces the recipe; it does NOT resume polling after
|
||||
# an SSL EOF. Only an explicitly absent recipe/repository permits a
|
||||
# new submission. Authentication/network failures are not absence.
|
||||
run_remote_build --recover && rc=0 || rc=$?
|
||||
if [ "$rc" -ne 0 ] && grep -qE 'Could not find (snap recipe|repository at path)' "$remote_log"; then
|
||||
run_remote_build && rc=0 || rc=$?
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
# A real glob, so nullglob can empty it, plus any name Launchpad chose -
|
||||
@@ -4630,13 +4654,13 @@ jobs:
|
||||
echo "built=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if [ "${{ matrix.arch }}" = riscv64 ] && [ "$rc" -eq 124 ]; then
|
||||
if [ "$rc" -eq 124 ] || [ "$SECONDS" -ge "$deadline" ]; then
|
||||
echo "pending=true" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::Launchpad riscv64 is still queued after five hours. Ending the local wait cleanly; re-run this matrix job later to reconnect to the same build and publish its snap."
|
||||
echo "::warning::Launchpad ${{ matrix.arch }} has not produced a snap within the five-hour wait budget. Ending the local wait cleanly; re-run this matrix job later to reconnect to the same build and publish its snap."
|
||||
exit 0
|
||||
fi
|
||||
if [ "$rc" -eq 0 ] && [ "${#snaps[@]}" -gt 0 ]; then
|
||||
echo "::warning::Attempt $i produced ${snaps[0]}, but it is not a usable snap (see above). Deleting it and building again rather than handing it to the Snap Store."
|
||||
echo "::warning::Attempt $i produced ${snaps[0]}, but it is not a usable snap (see above). Refusing to publish it; recovery must retrieve a valid artifact."
|
||||
fi
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
echo "Attempt $i: snapcraft exited 0 but produced NO .snap for ${{ matrix.arch }} - the Launchpad build did not finish (a 'Stopped' build downloads logs and no artifact). Everything in the directory:"
|
||||
@@ -4675,12 +4699,12 @@ jobs:
|
||||
echo "::endgroup::"
|
||||
done
|
||||
if [ "$i" -lt "$attempts" ]; then
|
||||
# Start the next attempt from nothing. snapcraft keeps a local
|
||||
# clone of the Launchpad repository under ~/.cache, and a retry
|
||||
# that reuses a half-pushed one repeats the same failure - which is
|
||||
# what three identical attempts sixteen minutes apart look like.
|
||||
echo "Clearing snapcraft's remote-build cache before retrying."
|
||||
rm -rf "$HOME/.cache/snapcraft/remote-build"
|
||||
# Clear a half-pushed clone only when submission never reached
|
||||
# monitoring. A failed poll must keep the running build intact.
|
||||
if ! grep -qE 'Monitoring build|Recovering build' "$remote_log"; then
|
||||
echo "Clearing snapcraft's remote-build cache before retrying submission."
|
||||
rm -rf "$HOME/.cache/snapcraft/remote-build"
|
||||
fi
|
||||
echo "Retrying in 60s..."
|
||||
sleep 60
|
||||
fi
|
||||
@@ -4762,7 +4786,7 @@ jobs:
|
||||
# good builds were reported as FAILED by the step that saves their logs.
|
||||
# The timestamp is worth keeping, so the colons become dashes rather
|
||||
# than the name being thrown away.
|
||||
for f in snapcraft-wekan-*.txt "$HOME"/.local/state/snapcraft/log/*.log; do
|
||||
for f in snapcraft-wekan-*.txt .tools/tmp/remote-build-attempt.log "$HOME"/.local/state/snapcraft/log/*.log; do
|
||||
[ -f "$f" ] || continue
|
||||
base="$(basename "$f")"
|
||||
cp -f "$f" "snap-logs/${base//:/-}" || true
|
||||
@@ -4866,24 +4890,7 @@ jobs:
|
||||
# attaching, which is the half people download from.
|
||||
snaps=( $(printf '%s\n' wekan_${VERSION}_${{ matrix.arch }}*.snap *_${{ matrix.arch }}.snap | sort -u) )
|
||||
if [ "${#snaps[@]}" -gt 0 ] && [ -s "${snaps[0]}" ]; then
|
||||
if ! gh release upload --repo "${GITHUB_REPOSITORY:-wekan/wekan}" "v${VERSION}" "${snaps[@]}" --clobber; then
|
||||
echo "::error::Could not attach ${snaps[*]} to release v${VERSION}. The snap BUILT and was published to the Snap Store by the step above, so this is the attaching alone - the half people download from."
|
||||
exit 1
|
||||
fi
|
||||
# Confirm from the OTHER side, the way the native snap job does: an
|
||||
# upload that reports success but leaves no asset behind is the
|
||||
# failure nobody notices until a download 404s. v10.91 lost three
|
||||
# architectures from the Release while every job that lost one had
|
||||
# already said the snap was built and published.
|
||||
gh release view --repo "${GITHUB_REPOSITORY:-wekan/wekan}" "v${VERSION}" \
|
||||
--json assets --jq '.assets[].name' > release-assets.txt
|
||||
for snap in "${snaps[@]}"; do
|
||||
if ! grep -qxF "$(basename "$snap")" release-assets.txt; then
|
||||
echo "::error::$(basename "$snap") is not listed in release v${VERSION} although the upload reported success. Re-run this job."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "OK: ${snaps[*]} attached to release v${VERSION}."
|
||||
bash releases/github-release-upload.sh "${GITHUB_REPOSITORY:-wekan/wekan}" "v${VERSION}" "${snaps[@]}"
|
||||
else
|
||||
echo "No ${{ matrix.arch }} snap to attach - the build produced none."
|
||||
fi
|
||||
@@ -4892,14 +4899,14 @@ jobs:
|
||||
# did not" for the whole release at a glance, instead of eighteen job logs. A
|
||||
# step that failed has already printed an ::error:: saying why; this names the
|
||||
# job it was in.
|
||||
- name: Say what a queued riscv64 build leaves behind
|
||||
- name: Say what an unfinished ${{ matrix.arch }} build leaves behind
|
||||
if: steps.launchpad.outputs.pending == 'true'
|
||||
run: |
|
||||
echo "::warning::snap-launchpad riscv64 is still queued on Launchpad; this Actions job stopped waiting cleanly before GitHub's cap."
|
||||
echo "::warning::snap-launchpad ${{ matrix.arch }} is unfinished on Launchpad; this Actions job stopped waiting cleanly before GitHub's cap."
|
||||
echo "Re-run this matrix job after Launchpad finishes. Its deterministic snapshot reconnects to the same build, downloads the snap, and publishes it."
|
||||
|
||||
# An unexpected job-level cancellation is distinct from the deliberate,
|
||||
# clean riscv64 waiter timeout above.
|
||||
# clean waiter timeout above.
|
||||
- name: Say what a cancelled ${{ matrix.arch }} job leaves behind
|
||||
if: cancelled()
|
||||
run: |
|
||||
|
||||
+2839
-12
File diff suppressed because it is too large.
Load diff
+1
-2
@@ -20,7 +20,7 @@ LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
|
||||
# TARGETARCH and TARGETVARIANT are automatically provided by Docker Buildx
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
ARG VERSION=11.80
|
||||
ARG VERSION=11.87
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
|
||||
@@ -50,7 +50,6 @@ ENV \
|
||||
AVATARS_UPLOAD_EXTERNAL_PROGRAM="" \
|
||||
AVATARS_UPLOAD_MIME_TYPES="" \
|
||||
AVATARS_UPLOAD_MAX_SIZE=72000 \
|
||||
RICHER_CARD_COMMENT_EDITOR=false \
|
||||
CARD_OPENED_WEBHOOK_ENABLED=false \
|
||||
MAX_IMAGE_PIXEL="" \
|
||||
IMAGE_COMPRESS_RATIO="" \
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v11.80.0"
|
||||
appVersion: "v11.87.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -1491,7 +1491,6 @@ set "DDP_TRANSPORT=sockjs"
|
||||
set "DEBUG=true"
|
||||
set "WRITABLE_PATH=.."
|
||||
set "WITH_API=true"
|
||||
set "RICHER_CARD_COMMENT_EDITOR=false"
|
||||
exit /b 0
|
||||
|
||||
:logdir
|
||||
@@ -1827,11 +1826,10 @@ set "ROOT_URL=http://localhost:3000"
|
||||
set "PORT=3000"
|
||||
set "WRITABLE_PATH=%WRITABLE_ABS%"
|
||||
set "WITH_API=true"
|
||||
set "RICHER_CARD_COMMENT_EDITOR=false"
|
||||
set "DEFAULT_METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling"
|
||||
echo ==^> Starting the WeKan test server on http://localhost:3000 from .build\bundle ^(precompiled - no rebuild^).
|
||||
start "WekanTestServer" /MIN /D "%REPO%" cmd /c "(echo ===== WeKan test server [bundle node :3000 db :3001/meteor] started: %DATE% %TIME% =====) 1>%RUN_LOGDIR%\wekan-test-server.log 2>&1 & "%NODE_BIN%" "%REPO%\.build\bundle\main.js" 1>>%RUN_LOGDIR%\wekan-test-server.log 2>&1"
|
||||
set "MONGO_URL=" & set "ROOT_URL=" & set "PORT=" & set "WRITABLE_PATH=" & set "WITH_API=" & set "RICHER_CARD_COMMENT_EDITOR=" & set "DEFAULT_METEOR_REACTIVITY_ORDER="
|
||||
set "MONGO_URL=" & set "ROOT_URL=" & set "PORT=" & set "WRITABLE_PATH=" & set "WITH_API=" & set "DEFAULT_METEOR_REACTIVITY_ORDER="
|
||||
|
||||
REM 6) Wait for :3000 to answer (bundle boots in seconds; curl-timeout poll).
|
||||
call :wait_server_ready
|
||||
|
||||
@@ -1427,7 +1427,7 @@ function run_all_tests(){
|
||||
# different one, so every test that needs seeded data fails.
|
||||
{ echo "===== WeKan test server [bundle node :3000 db :$TEST_DB_PORT/meteor] - started $(date '+%Y-%m-%d %H:%M:%S %Z') ====="; echo; \
|
||||
MONGO_URL="$TEST_MONGO_URL" ROOT_URL="http://localhost:3000" PORT=3000 \
|
||||
WRITABLE_PATH="$WRITABLE_ABS" WITH_API=true RICHER_CARD_COMMENT_EDITOR=false \
|
||||
WRITABLE_PATH="$WRITABLE_ABS" WITH_API=true \
|
||||
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling" \
|
||||
NODE_OPTIONS="$TEST_NODE_OPTIONS" exec "$NODE_BIN" "$BUNDLE_DIR/main.js"; } >> "$RUN_LOGDIR/wekan-test-server.log" 2>&1 &
|
||||
TEST_SERVER_PID=$!
|
||||
@@ -2920,7 +2920,7 @@ for _once in 1; do
|
||||
#---------------------------------------------------------------------
|
||||
# Logging of terminal output to console and to .tools/log/wekan-log.log at end of this line: 2>&1 | tee "$(one_log dev-server)"
|
||||
#WARN_WHEN_USING_OLD_API=true NODE_OPTIONS="--trace-warnings"
|
||||
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling" DDP_TRANSPORT=sockjs DEBUG=true WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://localhost:3000 meteor run --port 3000 2>&1 | tee "$(one_log dev-server)"
|
||||
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling" DDP_TRANSPORT=sockjs DEBUG=true WRITABLE_PATH=.. WITH_API=true ROOT_URL=http://localhost:3000 meteor run --port 3000 2>&1 | tee "$(one_log dev-server)"
|
||||
#---------------------------------------------------------------------
|
||||
break
|
||||
;;
|
||||
@@ -2932,7 +2932,7 @@ for _once in 1; do
|
||||
#Not in use, could increase RAM usage: NODE_OPTIONS="--max_old_space_size=4096"
|
||||
#---------------------------------------------------------------------
|
||||
# Logging of terminal output to console and to .tools/log/wekan-log.log at end of this line: 2>&1 | tee "$(one_log dev-server)"
|
||||
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling" DDP_TRANSPORT=sockjs DEBUG=true WARN_WHEN_USING_OLD_API=true NODE_OPTIONS="--trace-warnings --max-old-space-size=$_heap_mb" WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://localhost:3000 meteor run --port 3000 2>&1 | tee "$(one_log dev-server)"
|
||||
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling" DDP_TRANSPORT=sockjs DEBUG=true WARN_WHEN_USING_OLD_API=true NODE_OPTIONS="--trace-warnings --max-old-space-size=$_heap_mb" WRITABLE_PATH=.. WITH_API=true ROOT_URL=http://localhost:3000 meteor run --port 3000 2>&1 | tee "$(one_log dev-server)"
|
||||
#---------------------------------------------------------------------
|
||||
break
|
||||
;;
|
||||
@@ -2944,7 +2944,7 @@ for _once in 1; do
|
||||
#---------------------------------------------------------------------
|
||||
#Logging of terminal output to console and to .tools/log/wekan-log.log at end of this line: 2>&1 | tee "$(one_log dev-server)"
|
||||
#WARN_WHEN_USING_OLD_API=true NODE_OPTIONS="--trace-warnings"
|
||||
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling" DDP_TRANSPORT=sockjs DEBUG=true WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://localhost:3000 meteor run --port 3000 --extra-packages bundle-visualizer --production 2>&1 | tee "$(one_log dev-server)"
|
||||
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling" DDP_TRANSPORT=sockjs DEBUG=true WRITABLE_PATH=.. WITH_API=true ROOT_URL=http://localhost:3000 meteor run --port 3000 --extra-packages bundle-visualizer --production 2>&1 | tee "$(one_log dev-server)"
|
||||
#---------------------------------------------------------------------
|
||||
break
|
||||
;;
|
||||
@@ -2963,7 +2963,7 @@ for _once in 1; do
|
||||
#---------------------------------------------------------------------
|
||||
#Logging of terminal output to console and to .tools/log/wekan-log.log at end of this line: 2>&1 | tee "$(one_log dev-server)"
|
||||
#WARN_WHEN_USING_OLD_API=true NODE_OPTIONS="--trace-warnings"
|
||||
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling" DDP_TRANSPORT=sockjs DEBUG=true WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://$IPADDRESS:3000 meteor run --port 3000 2>&1 | tee "$(one_log dev-server)"
|
||||
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling" DDP_TRANSPORT=sockjs DEBUG=true WRITABLE_PATH=.. WITH_API=true ROOT_URL=http://$IPADDRESS:3000 meteor run --port 3000 2>&1 | tee "$(one_log dev-server)"
|
||||
#---------------------------------------------------------------------
|
||||
break
|
||||
;;
|
||||
@@ -2982,7 +2982,7 @@ for _once in 1; do
|
||||
#---------------------------------------------------------------------
|
||||
#Logging of terminal output to console and to .tools/log/wekan-log.log at end of this line: 2>&1 | tee "$(one_log dev-server)"
|
||||
#WARN_WHEN_USING_OLD_API=true NODE_OPTIONS="--trace-warnings"
|
||||
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling" DDP_TRANSPORT=sockjs DEBUG=true MONGO_URL=mongodb://127.0.0.1:27019/wekan WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://$IPADDRESS:3000 meteor run --port 3000 2>&1 | tee "$(one_log dev-server)"
|
||||
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling" DDP_TRANSPORT=sockjs DEBUG=true MONGO_URL=mongodb://127.0.0.1:27019/wekan WRITABLE_PATH=.. WITH_API=true ROOT_URL=http://$IPADDRESS:3000 meteor run --port 3000 2>&1 | tee "$(one_log dev-server)"
|
||||
#---------------------------------------------------------------------
|
||||
break
|
||||
;;
|
||||
@@ -2996,7 +2996,7 @@ for _once in 1; do
|
||||
# ROOT_URL differ. Logging of terminal output to console and to
|
||||
# .tools/log/wekan-log.log at the end of the line: 2>&1 | tee "$(one_log dev-server)"
|
||||
#---------------------------------------------------------------------
|
||||
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling" DDP_TRANSPORT=sockjs DEBUG=true WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL="$DEV_ROOT_URL" meteor run --port "$DEV_PORT" 2>&1 | tee "$(one_log dev-server)"
|
||||
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling" DDP_TRANSPORT=sockjs DEBUG=true WRITABLE_PATH=.. WITH_API=true ROOT_URL="$DEV_ROOT_URL" meteor run --port "$DEV_PORT" 2>&1 | tee "$(one_log dev-server)"
|
||||
#---------------------------------------------------------------------
|
||||
break
|
||||
;;
|
||||
@@ -3015,7 +3015,7 @@ for _once in 1; do
|
||||
#---------------------------------------------------------------------
|
||||
#Logging of terminal output to console and to .tools/log/wekan-log.log at end of this line: 2>&1 | tee "$(one_log dev-server)"
|
||||
#WARN_WHEN_USING_OLD_API=true NODE_OPTIONS="--trace-warnings"
|
||||
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling" DDP_TRANSPORT=sockjs DEBUG=true WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://$IPADDRESS:$PORT meteor run --port $PORT 2>&1 | tee "$(one_log dev-server)"
|
||||
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling" DDP_TRANSPORT=sockjs DEBUG=true WRITABLE_PATH=.. WITH_API=true ROOT_URL=http://$IPADDRESS:$PORT meteor run --port $PORT 2>&1 | tee "$(one_log dev-server)"
|
||||
#---------------------------------------------------------------------
|
||||
break
|
||||
;;
|
||||
|
||||
@@ -90,7 +90,7 @@ template(name="commentReactions")
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
|
||||
i.fa.fa-smile-o(title="{{_ 'reaction' }}")
|
||||
i.fa.fa-smile-o(title="{{_ 'addReactionPopup-title' }}")
|
||||
i.fa.fa-plus(title="{{_ 'add' }}")
|
||||
|
||||
template(name="addReactionPopup")
|
||||
|
||||
@@ -67,7 +67,6 @@ Template.commentForm.onDestroyed(function () {
|
||||
// button, a route change); make sure the last keystrokes reach the draft.
|
||||
flushCommentDraftSave();
|
||||
commentFormIsOpen.set(false);
|
||||
$('.note-popover').hide();
|
||||
});
|
||||
|
||||
Template.commentForm.helpers({
|
||||
|
||||
@@ -476,7 +476,7 @@ Template.boardBody.onRendered(function () {
|
||||
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'));
|
||||
el.setAttribute('aria-label', `${TAPi18n.__('star-item')} / ${TAPi18n.__('unstar-item')}`);
|
||||
} else {
|
||||
el.setAttribute('aria-label', 'Toggle');
|
||||
}
|
||||
|
||||
@@ -774,7 +774,7 @@ Template.cardsSortPopup.events({
|
||||
createdAt: 1,
|
||||
};
|
||||
setCardsSortBy(sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('date-created-newest-first'));
|
||||
sortCardsBy.set(TAPi18n.__('created-at-newest-first'));
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-sort-created-desc'() {
|
||||
@@ -782,7 +782,7 @@ Template.cardsSortPopup.events({
|
||||
createdAt: -1,
|
||||
};
|
||||
setCardsSortBy(sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('date-created-oldest-first'));
|
||||
sortCardsBy.set(TAPi18n.__('created-at-oldest-first'));
|
||||
Popup.back();
|
||||
},
|
||||
// #3050: float highest-voted cards to the top of each list. `votes` is not
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor';
|
||||
import { Session } from 'meteor/session';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { cardDateFormat } from '/client/lib/exportLocale';
|
||||
|
||||
// ONE url builder for every chart export, every view. There used to be five
|
||||
// copies of this (gantt.js, frappeGantt.js, dhtmlxGantt.js, timeView.js,
|
||||
@@ -18,12 +19,11 @@ export function chartExportUrl(chartKey, format) {
|
||||
const boardId = Session.get('currentBoard');
|
||||
if (!boardId || !chartKey) return '';
|
||||
const path = format === 'PDF' ? 'exportPDF' : 'exportExcel';
|
||||
const user = Meteor.user();
|
||||
const params = new URLSearchParams({
|
||||
authToken: Accounts._storedLoginToken() || '',
|
||||
lang: TAPi18n.getLanguage ? TAPi18n.getLanguage() : 'en',
|
||||
tz: Intl.DateTimeFormat().resolvedOptions().timeZone || '',
|
||||
dateFormat: (user && user.profile && user.profile.dateFormat) || 'YYYY-MM-DD',
|
||||
dateFormat: cardDateFormat(),
|
||||
});
|
||||
return `/api/boards/${boardId}/charts/${chartKey}/${path}?${params.toString()}`;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ template(name="miniboard")
|
||||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
span.drag-handle(title="{{_ 'dragBoard'}}")
|
||||
span.drag-handle(title="{{_ 'drag-board'}}")
|
||||
i.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
||||
@@ -25,9 +25,9 @@
|
||||
}
|
||||
|
||||
.timeline-view .timeline-marker.is-selected {
|
||||
background: #29a3a3;
|
||||
background: #187575;
|
||||
color: #fff;
|
||||
border-color: #29a3a3;
|
||||
border-color: #187575;
|
||||
}
|
||||
|
||||
.timeline-view .timeline-view-asof {
|
||||
|
||||
@@ -44,7 +44,7 @@ template(name="timelineView")
|
||||
| {{this.memberNames}}
|
||||
if this.dueAt
|
||||
p.timeline-card-due
|
||||
| {{_ 'card-due-date'}}: {{this.formattedDueAt}}
|
||||
| {{_ 'due-date'}}: {{this.formattedDueAt}}
|
||||
if selectedTimestamp
|
||||
button.timeline-card-restore.js-restore-card-timeline.primary(type="button")
|
||||
| {{_ 'board-view-timeline-restore'}}
|
||||
|
||||
@@ -44,9 +44,13 @@
|
||||
.attachment-details-container {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.attachment-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
margin-inline-end: 25px; /* Make sure the icons are not to far to the right */
|
||||
}
|
||||
@@ -384,3 +388,17 @@
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Native viewer controls retain the previous icon-only presentation. */
|
||||
#viewer-close, #prev-attachment, #next-attachment {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
button.attachment-thumbnail-container {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
text-align: start;
|
||||
font: inherit;
|
||||
}
|
||||
@@ -33,14 +33,14 @@ template(name="attachmentDeletePopup")
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="attachmentViewer")
|
||||
#viewer-overlay.hidden
|
||||
#viewer-overlay.hidden(role="dialog" aria-modal="true" aria-labelledby="attachment-name")
|
||||
#viewer-top-bar
|
||||
span#attachment-name
|
||||
a#viewer-close
|
||||
button#viewer-close(type="button" aria-label="{{_ 'close'}}")
|
||||
i.fa.fa-times-thin
|
||||
|
||||
#viewer-container
|
||||
i.fa.fa-caret-left#prev-attachment
|
||||
button.fa.fa-caret-left#prev-attachment(type="button" aria-label="{{_ 'previous'}}")
|
||||
#viewer-content
|
||||
img#image-viewer.hidden
|
||||
video#video-viewer.hidden(controls="true")
|
||||
@@ -49,7 +49,7 @@ template(name="attachmentViewer")
|
||||
span.pdf-preview-error {{_ 'preview-pdf-not-supported' }}
|
||||
object#txt-viewer.hidden(type="text/plain")
|
||||
#office-viewer.hidden
|
||||
i.fa.fa-caret-right#next-attachment
|
||||
button.fa.fa-caret-right#next-attachment(type="button" aria-label="{{_ 'next'}}")
|
||||
|
||||
template(name="attachmentGallery")
|
||||
|
||||
@@ -62,7 +62,7 @@ template(name="attachmentGallery")
|
||||
each attachments
|
||||
|
||||
.attachment-item(class="{{#if isAttachmentMigrating _id}}migrating{{/if}}")
|
||||
.attachment-thumbnail-container.open-preview(data-attachment-id="{{_id}}" data-card-id="{{ meta.cardId }}")
|
||||
button.attachment-thumbnail-container.open-preview(type="button" aria-label="{{_ 'preview'}}: {{cleanFilename name}}" data-attachment-id="{{_id}}" data-card-id="{{ meta.cardId }}")
|
||||
if link
|
||||
if(isImage)
|
||||
img.attachment-thumbnail(src="{{link}}" title="{{cleanFilename name}}" alt="{{cleanFilename name}}")
|
||||
@@ -109,7 +109,7 @@ template(name="attachmentGallery")
|
||||
.attachment-migration-overlay
|
||||
.migration-spinner
|
||||
i.fa.fa-cog.fa-spin
|
||||
.migration-text {{_ 'migrating-attachment'}}
|
||||
.migration-text {{_ 'attachment-migration'}}
|
||||
|
||||
template(name="attachmentActionsPopup")
|
||||
ul.pop-over-list
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { trapTabKey } from '/client/lib/accessibility';
|
||||
import { formatDateForDisplay } from '/client/lib/dateDisplay';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import '/client/components/cards/attachments.jade';
|
||||
@@ -21,6 +23,7 @@ import prettyMilliseconds from 'pretty-ms';
|
||||
// We store current card ID and the ID of currently opened attachment in a
|
||||
// global var. This is used so that we know what's the next attachment to open
|
||||
// when the user clicks on the prev/next button in the attachment viewer.
|
||||
let viewerOpener = null;
|
||||
let cardId = null;
|
||||
let openAttachmentId = null;
|
||||
let slideshowAttachmentIds = null;
|
||||
@@ -29,14 +32,13 @@ let slideshowAttachmentIds = null;
|
||||
let touchStartCoords = null;
|
||||
let touchEndCoords = null;
|
||||
|
||||
// Stores link to the attachment for which attachment actions popup was opened
|
||||
let attachmentActionsLink = null;
|
||||
let officePreview = null;
|
||||
let officePreviewAbortController = null;
|
||||
let officePreviewGeneration = 0;
|
||||
|
||||
Template.attachmentGallery.events({
|
||||
'click .open-preview'(event) {
|
||||
event.preventDefault();
|
||||
|
||||
openAttachmentId = $(event.currentTarget).attr("data-attachment-id");
|
||||
cardId = $(event.currentTarget).attr("data-card-id");
|
||||
@@ -51,9 +53,6 @@ Template.attachmentGallery.events({
|
||||
event.stopPropagation();
|
||||
},
|
||||
'click .js-open-attachment-menu': Popup.open('attachmentActions'),
|
||||
'mouseover .js-open-attachment-menu'(event) { // For some reason I cannot combine handlers for "click .js-open-attachment-menu" and "mouseover .js-open-attachment-menu" events so this is a quick workaround.
|
||||
attachmentActionsLink = event.currentTarget.getAttribute("data-attachment-link");
|
||||
},
|
||||
'click .js-rename': Popup.open('attachmentRename'),
|
||||
// History.md §12.1: Delete is a SOFT delete, done by the server. The method
|
||||
// marks the attachment deleted, keeps the file, unsets the cover if this was
|
||||
@@ -197,7 +196,10 @@ function openAttachmentViewer(attachmentId) {
|
||||
// Show the cleaned name: URL-decoded, homoglyphs folded, invisible characters
|
||||
// and exploit markup removed (plain text; .text() escapes, so never HTML).
|
||||
$('#attachment-name').text(cleanFileName(attachment.name));
|
||||
const opening = !viewerOpener;
|
||||
if (opening) viewerOpener = document.activeElement;
|
||||
$('#viewer-overlay').removeClass('hidden');
|
||||
if (opening) document.getElementById('viewer-close')?.focus();
|
||||
}
|
||||
|
||||
// Admin Panel / Files uses the same viewer as an opened card, but its next and
|
||||
@@ -213,7 +215,7 @@ export function openAttachmentSlideshow(attachmentId, attachmentIds = []) {
|
||||
openAttachmentViewer(attachmentId);
|
||||
}
|
||||
|
||||
function closeAttachmentViewer() {
|
||||
function closeAttachmentViewer(restoreFocus = true) {
|
||||
officePreviewGeneration++;
|
||||
if (officePreviewAbortController) {
|
||||
officePreviewAbortController.abort();
|
||||
@@ -224,8 +226,8 @@ function closeAttachmentViewer() {
|
||||
officePreview = null;
|
||||
}
|
||||
const officeViewer = document.getElementById('office-viewer');
|
||||
officeViewer.replaceChildren();
|
||||
officeViewer.classList.add('hidden');
|
||||
officeViewer?.replaceChildren();
|
||||
officeViewer?.classList.add('hidden');
|
||||
|
||||
$("#viewer-overlay").addClass("hidden");
|
||||
|
||||
@@ -239,19 +241,23 @@ function closeAttachmentViewer() {
|
||||
$("#txt-viewer").attr("data", "");
|
||||
$("#txt-viewer").addClass("hidden");
|
||||
|
||||
$("#video-viewer").get(0).pause(); // Stop playback
|
||||
$("#video-viewer").get(0).currentTime = 0;
|
||||
$("#video-viewer").get(0)?.pause(); // Stop playback
|
||||
if ($("#video-viewer").get(0)) $("#video-viewer").get(0).currentTime = 0;
|
||||
$("#video-viewer").empty();
|
||||
$("#video-viewer").addClass("hidden");
|
||||
|
||||
$("#audio-viewer").get(0).pause(); // Stop playback
|
||||
$("#audio-viewer").get(0).currentTime = 0;
|
||||
$("#audio-viewer").get(0)?.pause(); // Stop playback
|
||||
if ($("#audio-viewer").get(0)) $("#audio-viewer").get(0).currentTime = 0;
|
||||
$("#audio-viewer").empty();
|
||||
$("#audio-viewer").addClass("hidden");
|
||||
if (restoreFocus) {
|
||||
if (viewerOpener?.isConnected) viewerOpener.focus();
|
||||
viewerOpener = null;
|
||||
}
|
||||
}
|
||||
|
||||
function openNextAttachment() {
|
||||
closeAttachmentViewer();
|
||||
closeAttachmentViewer(false);
|
||||
|
||||
let i = 0;
|
||||
// Find an attachment that can be opened
|
||||
@@ -268,7 +274,7 @@ function openNextAttachment() {
|
||||
}
|
||||
|
||||
function openPrevAttachment() {
|
||||
closeAttachmentViewer();
|
||||
closeAttachmentViewer(false);
|
||||
|
||||
let i = 0;
|
||||
// Find an attachment that can be opened
|
||||
@@ -306,7 +312,20 @@ function processTouch(){
|
||||
|
||||
}
|
||||
|
||||
Template.attachmentViewer.onDestroyed(function () {
|
||||
closeAttachmentViewer();
|
||||
});
|
||||
|
||||
Template.attachmentViewer.events({
|
||||
'keydown #viewer-overlay'(event) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
closeAttachmentViewer();
|
||||
} else {
|
||||
trapTabKey(event);
|
||||
}
|
||||
},
|
||||
'touchstart #viewer-container'(event) {
|
||||
touchStartCoords = {
|
||||
x: event.changedTouches[0].screenX,
|
||||
@@ -340,13 +359,16 @@ Template.attachmentViewer.events({
|
||||
|
||||
closeAttachmentViewer();
|
||||
},
|
||||
'click #viewer-close'() {
|
||||
'click #viewer-close'(event) {
|
||||
event.preventDefault();
|
||||
closeAttachmentViewer();
|
||||
},
|
||||
'click #next-attachment'() {
|
||||
'click #next-attachment'(event) {
|
||||
event.preventDefault();
|
||||
openNextAttachment();
|
||||
},
|
||||
'click #prev-attachment'() {
|
||||
'click #prev-attachment'(event) {
|
||||
event.preventDefault();
|
||||
openPrevAttachment();
|
||||
},
|
||||
});
|
||||
@@ -641,9 +663,8 @@ Template.attachmentActionsPopup.helpers({
|
||||
return ret;
|
||||
},
|
||||
isBackgroundImage() {
|
||||
//const currentBoard = Utils.getCurrentBoard();
|
||||
//return currentBoard.backgroundImageURL === $(".attachment-thumbnail-img").attr("src");
|
||||
return false;
|
||||
const url = getAttachmentUrl(this);
|
||||
return !!url && Utils.getCurrentBoard()?.backgroundImageURL === url;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -656,20 +677,31 @@ Template.attachmentActionsPopup.events({
|
||||
ReactiveCache.getCard(this.meta.cardId).unsetCover();
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-add-background-image'(event) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
currentBoard.setBackgroundImageURL(attachmentActionsLink);
|
||||
Utils.setBackgroundImage(attachmentActionsLink);
|
||||
Popup.back();
|
||||
async 'click .js-add-background-image'(event) {
|
||||
event.preventDefault();
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const url = getAttachmentUrl(this);
|
||||
if (!currentBoard || !url) return;
|
||||
try {
|
||||
if (await currentBoard.setBackgroundImageURL(url) === false) return;
|
||||
Utils.setBackgroundImage(url);
|
||||
Popup.back();
|
||||
} catch (error) {
|
||||
window.alert(TAPi18n.__('server-error'));
|
||||
}
|
||||
},
|
||||
'click .js-remove-background-image'(event) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
currentBoard.setBackgroundImageURL("");
|
||||
Utils.setBackgroundImage("");
|
||||
Popup.back();
|
||||
Utils.reload();
|
||||
async 'click .js-remove-background-image'(event) {
|
||||
event.preventDefault();
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
if (!currentBoard) return;
|
||||
try {
|
||||
if (await currentBoard.setBackgroundImageURL('') === false) return;
|
||||
Utils.setBackgroundImage('');
|
||||
Popup.back();
|
||||
Utils.reload();
|
||||
} catch (error) {
|
||||
window.alert(TAPi18n.__('server-error'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ const descriptionFormIsOpen = new ReactiveVar(false);
|
||||
|
||||
Template.descriptionForm.onDestroyed(function () {
|
||||
descriptionFormIsOpen.set(false);
|
||||
$('.note-popover').hide();
|
||||
});
|
||||
|
||||
Template.descriptionForm.helpers({
|
||||
|
||||
@@ -1217,7 +1217,7 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
|
||||
gone with the grouping; a field that is its own section is a group of one. */
|
||||
.card-details .card-details-section-rule {
|
||||
width: 100%;
|
||||
margin: 14px 0 6px 0;
|
||||
margin: calc(14px * min(1, var(--wekan-ui-font-scale, 1))) 0 calc(6px * min(1, var(--wekan-ui-font-scale, 1))) 0;
|
||||
}
|
||||
.card-details h3.card-details-item-title.js-toggle-card-section {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -3,8 +3,6 @@ template(name="cardDetailsPopup")
|
||||
|
||||
template(name="cardDetails")
|
||||
|
||||
+attachmentViewer
|
||||
|
||||
//- `card-details-with-handle` when drag handles are on, the same class the
|
||||
minicard carries for the same reason: with a handle, the title is all edit
|
||||
target; without one, half of it is what the window is dragged by.
|
||||
@@ -28,7 +26,7 @@ template(name="cardDetails")
|
||||
+editCardTitleForm
|
||||
else
|
||||
unless isMiniScreen
|
||||
a.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
|
||||
a.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse'}}")
|
||||
if cardCollapsed
|
||||
i.fa.fa-caret-right
|
||||
else
|
||||
@@ -96,7 +94,7 @@ template(name="cardDetails")
|
||||
| >
|
||||
a.js-parent-card(href=linkForCard) {{title}}
|
||||
// else
|
||||
{{_ 'top-level-card'}}
|
||||
{{_ 'card'}}
|
||||
if isLinkedCard
|
||||
a.linked-card-location.js-go-to-linked-card
|
||||
+viewer
|
||||
@@ -173,8 +171,9 @@ template(name="cardDetails")
|
||||
.card-checklist-attachmentGalleries
|
||||
.card-checklist-attachmentGallery.card-checklists
|
||||
if currentBoard.allowsChecklists
|
||||
+cardSectionHeader(section="checklists" icon="fa-check" label="checklists")
|
||||
if isSectionOpen "checklists"
|
||||
unless $eq currentBoard.allowsChecklistTitle false
|
||||
+cardSectionHeader(section="checklists" icon="fa-check" label="checklists")
|
||||
if checklistSectionOpen
|
||||
//- Total checklist item count (finished/total) shown above all
|
||||
checklists. The board flag is a cheap boolean, so when it is off
|
||||
the checklist counting below is never evaluated.
|
||||
@@ -324,194 +323,200 @@ template(name="cardFieldSectionLabels")
|
||||
+cardButtons(_id=_id boardId=boardId)
|
||||
|
||||
template(name="cardFieldSectionDates")
|
||||
.card-details-group.card-details-group-date-format
|
||||
hr.card-details-section-rule
|
||||
.card-details-group-body
|
||||
if currentBoard.hasAnyAllowsDate
|
||||
.card-details-item.card-details-item-date-format
|
||||
+cardSectionHeader(section="date-format" icon="fa-calendar" label="date-format" noRule=true)
|
||||
if isSectionOpen "date-format"
|
||||
.card-details-item-content
|
||||
select.js-date-format-selector
|
||||
option(value="YYYY-MM-DD" selected="{{#if isDateFormat 'YYYY-MM-DD'}}selected{{/if}}") {{_ 'date-format-yyyy-mm-dd'}}
|
||||
option(value="DD-MM-YYYY" selected="{{#if isDateFormat 'DD-MM-YYYY'}}selected{{/if}}") {{_ 'date-format-dd-mm-yyyy'}}
|
||||
option(value="MM-DD-YYYY" selected="{{#if isDateFormat 'MM-DD-YYYY'}}selected{{/if}}") {{_ 'date-format-mm-dd-yyyy'}}
|
||||
if currentBoard.hasAnyAllowsDate
|
||||
.card-details-group.card-details-group-date-format
|
||||
hr.card-details-section-rule
|
||||
.card-details-group-body
|
||||
if currentBoard.hasAnyAllowsDate
|
||||
.card-details-item.card-details-item-date-format
|
||||
+cardSectionHeader(section="date-format" icon="fa-calendar" label=dateSectionLabel noRule=true)
|
||||
if isSectionOpen "date-format"
|
||||
unless isDateFormatForced
|
||||
.card-details-item-content
|
||||
select.js-date-format-selector
|
||||
option(value="YYYY-MM-DD" selected="{{#if isDateFormat 'YYYY-MM-DD'}}selected{{/if}}") {{_ 'date-format-yyyy-mm-dd'}} · {{_ 'time'}}
|
||||
option(value="DD-MM-YYYY" selected="{{#if isDateFormat 'DD-MM-YYYY'}}selected{{/if}}") {{_ 'date-format-dd-mm-yyyy'}} · {{_ 'time'}}
|
||||
option(value="MM-DD-YYYY" selected="{{#if isDateFormat 'MM-DD-YYYY'}}selected{{/if}}") {{_ 'date-format-mm-dd-yyyy'}} · {{_ 'time'}}
|
||||
option(value="YYYY-MM-DD-date-only" selected="{{#if isDateFormat 'YYYY-MM-DD-date-only'}}selected{{/if}}") {{_ 'date'}}: {{_ 'date-format-yyyy-mm-dd'}}
|
||||
option(value="DD-MM-YYYY-date-only" selected="{{#if isDateFormat 'DD-MM-YYYY-date-only'}}selected{{/if}}") {{_ 'date'}}: {{_ 'date-format-dd-mm-yyyy'}}
|
||||
option(value="MM-DD-YYYY-date-only" selected="{{#if isDateFormat 'MM-DD-YYYY-date-only'}}selected{{/if}}") {{_ 'date'}}: {{_ 'date-format-mm-dd-yyyy'}}
|
||||
|
||||
if isSectionOpen "date-format"
|
||||
each field in orderedDatesFields
|
||||
if $eq field "receivedDate"
|
||||
if currentBoard.allowsReceivedDate
|
||||
.card-details-item.card-details-item-received
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sign-out
|
||||
| {{_ 'card-received'}}
|
||||
if getReceived
|
||||
+cardReceivedDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-received-date
|
||||
i.fa.fa-plus
|
||||
if isSectionOpen "date-format"
|
||||
each field in orderedDatesFields
|
||||
if $eq field "receivedDate"
|
||||
if currentBoard.allowsReceivedDate
|
||||
.card-details-item.card-details-item-received
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sign-out
|
||||
| {{_ 'card-received'}}
|
||||
if getReceived
|
||||
+cardReceivedDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-received-date
|
||||
i.fa.fa-plus
|
||||
|
||||
if $eq field "startDate"
|
||||
if currentBoard.allowsStartDate
|
||||
.card-details-item.card-details-item-start
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-hourglass-start
|
||||
| {{_ 'card-start'}}
|
||||
if getStart
|
||||
+cardStartDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-start-date
|
||||
i.fa.fa-plus
|
||||
if $eq field "startDate"
|
||||
if currentBoard.allowsStartDate
|
||||
.card-details-item.card-details-item-start
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-hourglass-start
|
||||
| {{_ 'card-start'}}
|
||||
if getStart
|
||||
+cardStartDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-start-date
|
||||
i.fa.fa-plus
|
||||
|
||||
if $eq field "dueDate"
|
||||
if currentBoard.allowsDueDate
|
||||
.card-details-item.card-details-item-due
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'card-due'}}
|
||||
if getDue
|
||||
+cardDueDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-due-date
|
||||
i.fa.fa-plus
|
||||
if dueDateChangeCount
|
||||
.card-details-due-date-changes(title="{{_ 'due-date-changes'}}")
|
||||
| {{_ 'due-date-changed-times' dueDateChangeCount}}
|
||||
if $eq field "dueDate"
|
||||
if currentBoard.allowsDueDate
|
||||
.card-details-item.card-details-item-due
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'card-due'}}
|
||||
if getDue
|
||||
+cardDueDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-due-date
|
||||
i.fa.fa-plus
|
||||
if dueDateChangeCount
|
||||
.card-details-due-date-changes(title="{{_ 'due-date-changes'}}")
|
||||
| {{_ 'due-date-changed-times' dueDateChangeCount}}
|
||||
|
||||
if $eq field "endDate"
|
||||
if currentBoard.allowsEndDate
|
||||
.card-details-item.card-details-item-end
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-hourglass-end
|
||||
| {{_ 'card-end'}}
|
||||
if getEnd
|
||||
+cardEndDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-end-date
|
||||
i.fa.fa-plus
|
||||
if $eq field "endDate"
|
||||
if currentBoard.allowsEndDate
|
||||
.card-details-item.card-details-item-end
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-hourglass-end
|
||||
| {{_ 'card-end'}}
|
||||
if getEnd
|
||||
+cardEndDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-end-date
|
||||
i.fa.fa-plus
|
||||
|
||||
template(name="cardFieldSectionMembers")
|
||||
.card-details-group.card-details-group-members
|
||||
hr.card-details-section-rule
|
||||
.card-details-group-body
|
||||
if currentBoard.allowsMembers
|
||||
.card-details-item.card-details-item-members
|
||||
+cardSectionHeader(section="members" icon="fa-users" label="members" noRule=true)
|
||||
if isSectionOpen "members"
|
||||
each userId in getMembers
|
||||
+userAvatar(userId=userId cardId=_id)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
if isSectionOpen "members"
|
||||
each field in orderedMembersFields
|
||||
if $eq field "assignee"
|
||||
if currentBoard.allowsAssignee
|
||||
.card-details-item.card-details-item-assignees
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user
|
||||
| {{_ 'assignee'}}
|
||||
each userId in getAssignees
|
||||
+userAvatar(userId=userId cardId=_id assignee=true)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
if canModifyCard
|
||||
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
|
||||
if currentBoard.hasAnyAllowsUser
|
||||
.card-details-group.card-details-group-members
|
||||
hr.card-details-section-rule
|
||||
.card-details-group-body
|
||||
if currentBoard.allowsMembers
|
||||
.card-details-item.card-details-item-members
|
||||
+cardSectionHeader(section="members" icon="fa-users" label="members" noRule=true)
|
||||
if isSectionOpen "members"
|
||||
each userId in getMembers
|
||||
+userAvatar(userId=userId cardId=_id)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
|
||||
i.fa.fa-plus
|
||||
if currentUser.isWorker
|
||||
unless assigneeSelected
|
||||
|
||||
if isSectionOpen "members"
|
||||
each field in orderedMembersFields
|
||||
if $eq field "assignee"
|
||||
if currentBoard.allowsAssignee
|
||||
.card-details-item.card-details-item-assignees
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user
|
||||
| {{_ 'assignee'}}
|
||||
each userId in getAssignees
|
||||
+userAvatar(userId=userId cardId=_id assignee=true)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
if canModifyCard
|
||||
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
|
||||
i.fa.fa-plus
|
||||
if currentUser.isWorker
|
||||
unless assigneeSelected
|
||||
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
if $eq field "creator"
|
||||
if currentBoard.allowsCreator
|
||||
.card-details-item.card-details-item-creator
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user
|
||||
| {{_ 'creator'}}
|
||||
if $eq field "creator"
|
||||
if currentBoard.allowsCreator
|
||||
.card-details-item.card-details-item-creator
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user
|
||||
| {{_ 'creator'}}
|
||||
|
||||
+userAvatar(userId=userId noRemove=true)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
+userAvatar(userId=userId noRemove=true)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
|
||||
|
||||
//.card-details-items
|
||||
//if assigneeSelected
|
||||
//.card-details-items
|
||||
if $eq field "requestedBy"
|
||||
if currentBoard.allowsRequestedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-shopping-cart
|
||||
| {{_ 'requested-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
+inlinedForm(classNames="js-card-details-requester")
|
||||
+editCardRequesterForm
|
||||
else
|
||||
each userId in getRequesters
|
||||
+userAvatar(userId=userId cardId=_id noRemove=true)
|
||||
if getRequestedBy
|
||||
+viewer
|
||||
= getRequestedBy
|
||||
.card-details-person-controls
|
||||
a.member.add-member.card-details-item-add-button.js-select-requester(title="{{_ 'requested-by'}}")
|
||||
i.fa.fa-plus
|
||||
a.js-open-inlined-form(
|
||||
title="{{#if getRequestedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}"
|
||||
aria-label="{{#if getRequestedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}")
|
||||
if getRequestedBy
|
||||
i.fa.fa-pencil-square-o(aria-hidden="true")
|
||||
else
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
else
|
||||
each userId in getRequesters
|
||||
+userAvatar(userId=userId noRemove=true)
|
||||
if getRequestedBy
|
||||
+viewer
|
||||
= getRequestedBy
|
||||
//.card-details-items
|
||||
//if assigneeSelected
|
||||
//.card-details-items
|
||||
if $eq field "requestedBy"
|
||||
if currentBoard.allowsRequestedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-shopping-cart
|
||||
| {{_ 'requested-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
+inlinedForm(classNames="js-card-details-requester")
|
||||
+editCardRequesterForm
|
||||
else
|
||||
each userId in getRequesters
|
||||
+userAvatar(userId=userId cardId=_id noRemove=true)
|
||||
if getRequestedBy
|
||||
+viewer
|
||||
= getRequestedBy
|
||||
.card-details-person-controls
|
||||
a.member.add-member.card-details-item-add-button.js-select-requester(title="{{_ 'requested-by'}}")
|
||||
i.fa.fa-plus
|
||||
a.js-open-inlined-form(
|
||||
title="{{#if getRequestedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}"
|
||||
aria-label="{{#if getRequestedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}")
|
||||
if getRequestedBy
|
||||
i.fa.fa-pencil-square-o(aria-hidden="true")
|
||||
else
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
else
|
||||
each userId in getRequesters
|
||||
+userAvatar(userId=userId noRemove=true)
|
||||
if getRequestedBy
|
||||
+viewer
|
||||
= getRequestedBy
|
||||
|
||||
if $eq field "assignedBy"
|
||||
if currentBoard.allowsAssignedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user-plus
|
||||
| {{_ 'assigned-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
+inlinedForm(classNames="js-card-details-assigner")
|
||||
+editCardAssignerForm
|
||||
else
|
||||
each userId in getAssigners
|
||||
+userAvatar(userId=userId cardId=_id noRemove=true)
|
||||
if getAssignedBy
|
||||
+viewer
|
||||
= getAssignedBy
|
||||
.card-details-person-controls
|
||||
a.member.add-member.card-details-item-add-button.js-select-assigner(title="{{_ 'assigned-by'}}")
|
||||
i.fa.fa-plus
|
||||
a.js-open-inlined-form(
|
||||
title="{{#if getAssignedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}"
|
||||
aria-label="{{#if getAssignedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}")
|
||||
if getAssignedBy
|
||||
i.fa.fa-pencil-square-o(aria-hidden="true")
|
||||
else
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
else
|
||||
each userId in getAssigners
|
||||
+userAvatar(userId=userId noRemove=true)
|
||||
if getAssignedBy
|
||||
+viewer
|
||||
= getAssignedBy
|
||||
if $eq field "assignedBy"
|
||||
if currentBoard.allowsAssignedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user-plus
|
||||
| {{_ 'assigned-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
+inlinedForm(classNames="js-card-details-assigner")
|
||||
+editCardAssignerForm
|
||||
else
|
||||
each userId in getAssigners
|
||||
+userAvatar(userId=userId cardId=_id noRemove=true)
|
||||
if getAssignedBy
|
||||
+viewer
|
||||
= getAssignedBy
|
||||
.card-details-person-controls
|
||||
a.member.add-member.card-details-item-add-button.js-select-assigner(title="{{_ 'assigned-by'}}")
|
||||
i.fa.fa-plus
|
||||
a.js-open-inlined-form(
|
||||
title="{{#if getAssignedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}"
|
||||
aria-label="{{#if getAssignedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}")
|
||||
if getAssignedBy
|
||||
i.fa.fa-pencil-square-o(aria-hidden="true")
|
||||
else
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
else
|
||||
each userId in getAssigners
|
||||
+userAvatar(userId=userId noRemove=true)
|
||||
if getAssignedBy
|
||||
+viewer
|
||||
= getAssignedBy
|
||||
|
||||
template(name="cardFieldSectionDependencies")
|
||||
//- #6688: Board Settings / Card can hide the Dependencies section.
|
||||
@@ -548,79 +553,80 @@ template(name="cardFieldSectionDependencies")
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-dependency'}}
|
||||
template(name="cardFieldSectionSort")
|
||||
.card-details-group.card-details-group-sort
|
||||
hr.card-details-section-rule
|
||||
.card-details-group-body
|
||||
if currentBoard.allowsCardSortingByNumber
|
||||
.card-details-item.card-details-sort-order
|
||||
+cardSectionHeader(section="sort" icon="fa-sort-numeric-asc" label="sort" noRule=true)
|
||||
if isSectionOpen "sort"
|
||||
if canModifyCard
|
||||
+inlinedForm(classNames="js-card-details-sort")
|
||||
+editCardSortOrderForm
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
+viewer
|
||||
= sort
|
||||
|
||||
if isSectionOpen "sort"
|
||||
each field in orderedSortFields
|
||||
if $eq field "showLists"
|
||||
if currentBoard.allowsShowLists
|
||||
.card-details-item.card-details-show-lists
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-list
|
||||
| {{_ 'list'}}
|
||||
if canModifyCard
|
||||
details.card-details-list-picker
|
||||
summary
|
||||
each currentSwimlaneListsSorted
|
||||
if isCurrentListId _id
|
||||
+viewer
|
||||
= title
|
||||
div.card-details-list-options(role="listbox" aria-label="{{_ 'list'}}")
|
||||
each currentSwimlaneListsSorted
|
||||
div.js-select-card-details-list-option(role="option" tabindex="0" data-list-id="{{_id}}" aria-selected="{{#if isCurrentListId _id}}true{{else}}false{{/if}}")
|
||||
+viewer
|
||||
= title
|
||||
if hasVisibleSortFields
|
||||
.card-details-group.card-details-group-sort
|
||||
hr.card-details-section-rule
|
||||
.card-details-group-body
|
||||
if currentBoard.allowsCardSortingByNumber
|
||||
.card-details-item.card-details-sort-order
|
||||
+cardSectionHeader(section="sort" icon="fa-sort-numeric-asc" label="sort" noRule=true)
|
||||
if isSectionOpen "sort"
|
||||
if canModifyCard
|
||||
+inlinedForm(classNames="js-card-details-sort")
|
||||
+editCardSortOrderForm
|
||||
else
|
||||
each currentSwimlaneListsSorted
|
||||
if isCurrentListId _id
|
||||
+viewer
|
||||
= title
|
||||
a.js-open-inlined-form
|
||||
+viewer
|
||||
= sort
|
||||
|
||||
//.card-details-items
|
||||
if $eq field "spentTime"
|
||||
if currentBoard.allowsSpentTime
|
||||
if getSpentTime
|
||||
.card-details-item.card-details-item-spent
|
||||
if getIsOvertime
|
||||
h3.card-details-item-title
|
||||
| {{_ 'overtime-hours'}}
|
||||
if isSectionOpen "sort"
|
||||
each field in orderedSortFields
|
||||
if $eq field "showLists"
|
||||
if currentBoard.allowsShowLists
|
||||
.card-details-item.card-details-show-lists
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-list
|
||||
| {{_ 'list'}}
|
||||
if canModifyCard
|
||||
details.card-details-list-picker
|
||||
summary
|
||||
each currentSwimlaneListsSorted
|
||||
if isCurrentListId _id
|
||||
+viewer
|
||||
= title
|
||||
div.card-details-list-options(role="listbox" aria-label="{{_ 'list'}}")
|
||||
each currentSwimlaneListsSorted
|
||||
div.js-select-card-details-list-option(role="option" tabindex="0" data-list-id="{{_id}}" aria-selected="{{#if isCurrentListId _id}}true{{else}}false{{/if}}")
|
||||
+viewer
|
||||
= title
|
||||
else
|
||||
h3.card-details-item-title
|
||||
| {{_ 'spent-time-hours'}}
|
||||
+cardSpentTime
|
||||
each currentSwimlaneListsSorted
|
||||
if isCurrentListId _id
|
||||
+viewer
|
||||
= title
|
||||
|
||||
//- #6688: the two timers each have a Board Settings / Card toggle,
|
||||
like Spent time above; before they rendered on every card.
|
||||
if $eq field "flowtime"
|
||||
if currentBoard.allowsFlowtime
|
||||
.card-details-item.card-details-item-flow
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-bolt
|
||||
| {{_ 'flowtime'}}
|
||||
+cardFlowtime
|
||||
//.card-details-items
|
||||
if $eq field "spentTime"
|
||||
if currentBoard.allowsSpentTime
|
||||
if getSpentTime
|
||||
.card-details-item.card-details-item-spent
|
||||
if getIsOvertime
|
||||
h3.card-details-item-title
|
||||
| {{_ 'overtime-hours'}}
|
||||
else
|
||||
h3.card-details-item-title
|
||||
| {{_ 'spent-time-hours'}}
|
||||
+cardSpentTime
|
||||
|
||||
if $eq field "pomodoro"
|
||||
if currentBoard.allowsPomodoro
|
||||
.card-details-item.card-details-item-pomodoro
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'pomodoro'}}
|
||||
+cardPomodoro
|
||||
//- #6688: the two timers each have a Board Settings / Card toggle,
|
||||
like Spent time above; before they rendered on every card.
|
||||
if $eq field "flowtime"
|
||||
if currentBoard.allowsFlowtime
|
||||
.card-details-item.card-details-item-flow
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-bolt
|
||||
| {{_ 'flowtime'}}
|
||||
+cardFlowtime
|
||||
|
||||
//.card-details-items
|
||||
if $eq field "pomodoro"
|
||||
if currentBoard.allowsPomodoro
|
||||
.card-details-item.card-details-item-pomodoro
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'pomodoro'}}
|
||||
+cardPomodoro
|
||||
|
||||
//.card-details-items
|
||||
|
||||
template(name="cardFieldSectionCustomFields")
|
||||
//- The heading is here for EVERY card the reader may write to, not only
|
||||
@@ -1248,23 +1254,23 @@ template(name="exportCardPopup")
|
||||
which is the one holding the dialog. client/components/cards/cardDetails.js
|
||||
template(name="cardDestinationPicker")
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
label(for="{{pickerFieldId 'boards'}}") {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus id="{{pickerFieldId 'boards'}}")
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isSelectedBoardId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
label(for="{{pickerFieldId 'swimlanes'}}") {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes(id="{{pickerFieldId 'swimlanes'}}")
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isSelectedSwimlaneId _id}}selected{{/if}}") {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
label(for="{{pickerFieldId 'lists'}}") {{_ 'lists'}}:
|
||||
select.js-select-lists(id="{{pickerFieldId 'lists'}}")
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isSelectedListId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
label(for="{{pickerFieldId 'cards'}}") {{_ 'cards'}}:
|
||||
select.js-select-cards(id="{{pickerFieldId 'cards'}}")
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
@@ -1275,8 +1281,11 @@ template(name="cardDestinationPicker")
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
|
||||
if destinationError
|
||||
.warning.js-destination-error(role="alert") {{destinationError}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
button.primary.confirm.js-done(type="button" disabled=destinationDisabled) {{_ 'done'}}
|
||||
|
||||
template(name="moveCardPopup")
|
||||
label(for='move-card-title') {{_ 'title'}}:
|
||||
@@ -1466,7 +1475,7 @@ template(name="setCardColorPopup")
|
||||
label(for="cardColorWheel") {{_ 'custom-color'}}
|
||||
input.js-card-color-wheel#cardColorWheel(type="color" value=currentColorHex title="{{_ 'custom-color'}}")
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'remove-btn'}}
|
||||
|
||||
template(name="cardDeletePopup")
|
||||
p {{_ "card-delete-pop"}}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { relativeCardSort } from '/client/lib/relativeCardPosition';
|
||||
import { Random } from 'meteor/random';
|
||||
import { dateDisplayPreferences, isDateFormatForced } from '/client/lib/dateDisplay';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { ReactiveDict } from 'meteor/reactive-dict';
|
||||
@@ -770,6 +773,17 @@ Template.registerHelper('isCurrentListId', function isCurrentListId(listId) {
|
||||
Template.registerHelper('orderedVoteAndPokerFields', sectionHelper('voteAndPoker'));
|
||||
}
|
||||
|
||||
Template.cardFieldSectionSort.helpers({
|
||||
hasVisibleSortFields() {
|
||||
const board = this.board();
|
||||
return !!(board && (
|
||||
board.allowsCardSortingByNumber || board.allowsShowLists ||
|
||||
board.allowsFlowtime || board.allowsPomodoro ||
|
||||
(board.allowsSpentTime && this.getSpentTime())
|
||||
));
|
||||
},
|
||||
});
|
||||
|
||||
Template.cardDetails.helpers({
|
||||
// #4448: the order the reorderable card-detail sections (Labels, Dates,
|
||||
// Members, Dependencies, Sort, Custom Fields, Vote/Poker, Description)
|
||||
@@ -835,6 +849,10 @@ Template.cardDetails.helpers({
|
||||
return user && user.isVerticalScrollbars();
|
||||
},
|
||||
|
||||
checklistSectionOpen() {
|
||||
return ReactiveCache.getBoard(this.boardId)?.allowsChecklistTitle === false || isCardSectionOpen('checklists');
|
||||
},
|
||||
|
||||
isLoaded() {
|
||||
return Template.instance().isLoaded;
|
||||
},
|
||||
@@ -1095,6 +1113,7 @@ Template.cardDetails.events({
|
||||
}
|
||||
},
|
||||
'change .js-date-format-selector'(event) {
|
||||
if (isDateFormatForced()) return;
|
||||
const dateFormat = event.target.value;
|
||||
if (Meteor.userId()) {
|
||||
Meteor.call('changeDateFormat', dateFormat);
|
||||
@@ -1524,13 +1543,11 @@ Template.cardDetails.events({
|
||||
// function: isDateFormat" the instant that section rendered and broke
|
||||
// opening the card popup entirely. Registered globally, like isSectionOpen
|
||||
// just below, so every template can see it.
|
||||
Template.registerHelper('isDateFormatForced', isDateFormatForced);
|
||||
Template.registerHelper('dateSectionLabel', () => isDateFormatForced() ? 'date' : 'date-format');
|
||||
|
||||
Template.registerHelper('isDateFormat', function isDateFormat(format) {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (!currentUser) {
|
||||
const stored = window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD';
|
||||
return format === stored;
|
||||
}
|
||||
return currentUser.getDateFormat() === format;
|
||||
return dateDisplayPreferences().dateFormat === format;
|
||||
});
|
||||
|
||||
Template.cardDetails.helpers({
|
||||
@@ -2118,6 +2135,7 @@ Template.editCardAssignerForm.events({
|
||||
// `each boards` the data context is a board, so a helper reaching into the
|
||||
// context for it would find nothing there.
|
||||
Template.cardDestinationPicker.onCreated(function () {
|
||||
this.pickerId = Random.id();
|
||||
this.autorun(() => {
|
||||
const data = Template.currentData();
|
||||
this.dialog = data && data.dialog;
|
||||
@@ -2125,6 +2143,12 @@ Template.cardDestinationPicker.onCreated(function () {
|
||||
});
|
||||
|
||||
Template.cardDestinationPicker.helpers({
|
||||
pickerFieldId(field) { return `${Template.instance().pickerId}-${field}`; },
|
||||
destinationError() { return Template.instance().dialog.error.get(); },
|
||||
destinationDisabled() {
|
||||
const dialog = Template.instance().dialog;
|
||||
return dialog.loading.get() || dialog.saving.get() || !dialog.selectedSwimlaneId.get() || !dialog.selectedListId.get();
|
||||
},
|
||||
boards() {
|
||||
return Template.instance().dialog.boards();
|
||||
},
|
||||
@@ -2186,7 +2210,9 @@ function registerCardDialogTemplate(templateName) {
|
||||
|
||||
Template[templateName].events({
|
||||
async 'click .js-done'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const dialog = tpl.dialog;
|
||||
if (dialog.loading.get() || dialog.saving.get()) return;
|
||||
// Read the target board/swimlane/list from the dialog's live reactive
|
||||
// selection rather than the DOM <select> elements. A reactive re-render
|
||||
// (e.g. the boards() helper transiently returning [] while a 'board'
|
||||
@@ -2203,11 +2229,19 @@ function registerCardDialogTemplate(templateName) {
|
||||
? cardSelect.options[cardSelect.selectedIndex].value
|
||||
: null;
|
||||
|
||||
if (!boardId || !swimlaneId || !listId) return;
|
||||
if (cardId && !dialog.cards().some(card => card._id === cardId)) return;
|
||||
dialog.error.set('');
|
||||
dialog.saving.set(true);
|
||||
const options = { boardId, swimlaneId, listId, cardId };
|
||||
try {
|
||||
await dialog.setDone(cardId, options);
|
||||
} catch (e) {
|
||||
console.error('Error in card dialog operation:', e);
|
||||
dialog.error.set(e.reason || e.message || TAPi18n.__('server-error'));
|
||||
return;
|
||||
} finally {
|
||||
dialog.saving.set(false);
|
||||
}
|
||||
Popup.back(2);
|
||||
},
|
||||
@@ -2217,6 +2251,7 @@ function registerCardDialogTemplate(templateName) {
|
||||
'change .js-select-swimlanes'(event, tpl) {
|
||||
tpl.dialog.selectedSwimlaneId.set($(event.currentTarget).val());
|
||||
tpl.dialog.setFirstListId();
|
||||
tpl.dialog.selectedCardId?.set('');
|
||||
},
|
||||
'change .js-select-lists'(event, tpl) {
|
||||
tpl.dialog.selectedListId.set($(event.currentTarget).val());
|
||||
@@ -2259,12 +2294,7 @@ Template.moveCardPopup.onCreated(function () {
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
const targetSort = targetCard.sort || 0;
|
||||
if (position === 'above') {
|
||||
sortIndex = targetSort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetSort + 0.5;
|
||||
}
|
||||
sortIndex = await relativeCardSort(targetCard, position, card._id);
|
||||
}
|
||||
} else {
|
||||
const maxSort = await card.getMaxSort(options.listId, options.swimlaneId);
|
||||
@@ -2313,12 +2343,7 @@ Template.copyCardPopup.onCreated(function () {
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
const targetSort = targetCard.sort || 0;
|
||||
if (position === 'above') {
|
||||
sortIndex = targetSort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetSort + 0.5;
|
||||
}
|
||||
sortIndex = await relativeCardSort(targetCard, position, newCard._id);
|
||||
}
|
||||
} else {
|
||||
const maxSort = await newCard.getMaxSort(options.listId, options.swimlaneId);
|
||||
@@ -2367,8 +2392,7 @@ Template.linkCardToBoardPopup.onCreated(function () {
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
const targetSort = targetCard.sort || 0;
|
||||
sortIndex = position === 'above' ? targetSort - 0.5 : targetSort + 0.5;
|
||||
sortIndex = await relativeCardSort(targetCard, position, newCard._id);
|
||||
}
|
||||
} else {
|
||||
const maxSort = await newCard.getMaxSort(options.listId, options.swimlaneId);
|
||||
@@ -2414,12 +2438,7 @@ Template.convertChecklistItemToCardPopup.onCreated(function () {
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
const targetSort = targetCard.sort || 0;
|
||||
if (position === 'above') {
|
||||
sortIndex = targetSort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetSort + 0.5;
|
||||
}
|
||||
sortIndex = await relativeCardSort(targetCard, position, newCard._id);
|
||||
}
|
||||
} else {
|
||||
const maxSort = await newCard.getMaxSort(options.listId, options.swimlaneId);
|
||||
@@ -2462,12 +2481,7 @@ Template.copyManyCardsPopup.onCreated(function () {
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
const targetSort = targetCard.sort || 0;
|
||||
if (position === 'above') {
|
||||
sortIndex = targetSort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetSort + 0.5;
|
||||
}
|
||||
sortIndex = await relativeCardSort(targetCard, position, newCard._id);
|
||||
}
|
||||
} else {
|
||||
const maxSort = await newCard.getMaxSort(options.listId, options.swimlaneId);
|
||||
@@ -2475,6 +2489,7 @@ Template.copyManyCardsPopup.onCreated(function () {
|
||||
}
|
||||
|
||||
await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
||||
if (position === 'below') cardId = newCardId;
|
||||
}
|
||||
|
||||
// In case the filter is active we need to add the newly inserted card in
|
||||
|
||||
@@ -9,6 +9,7 @@ function getCardId() {
|
||||
Template.editCardSpentTimePopup.onCreated(function () {
|
||||
this.error = new ReactiveVar('');
|
||||
this.card = Cards.findOne(getCardId());
|
||||
this.overtime = new ReactiveVar(this.card?.getIsOvertime() || false);
|
||||
});
|
||||
|
||||
Template.editCardSpentTimePopup.helpers({
|
||||
@@ -19,8 +20,7 @@ Template.editCardSpentTimePopup.helpers({
|
||||
return Cards.findOne(getCardId());
|
||||
},
|
||||
getIsOvertime() {
|
||||
const card = Cards.findOne(getCardId());
|
||||
return card?.getIsOvertime ? card.getIsOvertime() : false;
|
||||
return Template.instance().overtime.get();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -32,10 +32,7 @@ Template.editCardSpentTimePopup.events({
|
||||
if (!card) return;
|
||||
|
||||
const spentTime = parseFloat(evt.target.time.value);
|
||||
let isOvertime = false;
|
||||
if ($('#overtime').attr('class').indexOf('is-checked') >= 0) {
|
||||
isOvertime = true;
|
||||
}
|
||||
const isOvertime = tpl.overtime.get();
|
||||
if (spentTime >= 0) {
|
||||
card.setSpentTime(spentTime);
|
||||
card.setIsOvertime(isOvertime);
|
||||
@@ -53,12 +50,9 @@ Template.editCardSpentTimePopup.events({
|
||||
card.setIsOvertime(false);
|
||||
Popup.back();
|
||||
},
|
||||
'click a.js-toggle-overtime'(evt) {
|
||||
const card = Cards.findOne(getCardId());
|
||||
if (!card) return;
|
||||
card.setIsOvertime(!card.getIsOvertime());
|
||||
$('#overtime .materialCheckBox').toggleClass('is-checked');
|
||||
$('#overtime').toggleClass('is-checked');
|
||||
'click a.js-toggle-overtime'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.overtime.set(!tpl.overtime.get());
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -43,12 +43,13 @@ textarea.js-edit-checklist-item {
|
||||
.checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
|
||||
color: #fff;
|
||||
background-color: #666;
|
||||
padding: 0.01em 16px;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
border-radius: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
.checklist-title {
|
||||
padding: 10px;
|
||||
padding: calc(10px * min(1, var(--wekan-ui-font-scale, 1)));
|
||||
}
|
||||
.checklist-title .checkbox {
|
||||
float: inline-start;
|
||||
@@ -134,6 +135,8 @@ textarea.js-edit-checklist-item {
|
||||
}
|
||||
.checklist-item .item-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.checklist-item .item-title.is-checked {
|
||||
color: #8c8c8c;
|
||||
@@ -143,7 +146,7 @@ textarea.js-edit-checklist-item {
|
||||
.checklist-item .item-title .viewer p {
|
||||
margin-bottom: 2px;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
max-width: 420px;
|
||||
}
|
||||
.checklist-item span.fa.checklistitem-handle {
|
||||
@@ -234,3 +237,14 @@ textarea.js-edit-checklist-item {
|
||||
.checklist-title .checklist-collapse:focus-visible {
|
||||
color: #2980b9;
|
||||
}
|
||||
|
||||
.checklist-title .checklist-due-date-add {
|
||||
float: inline-end;
|
||||
width: auto;
|
||||
min-width: 28px;
|
||||
padding: 4px 6px;
|
||||
margin: 0 4px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
}
|
||||
@@ -15,10 +15,10 @@ template(name="checklists")
|
||||
.material-toggle-switch(title="{{_ 'hide-finished-checklist'}}")
|
||||
//span.toggle-switch-title
|
||||
if card.hideFinishedChecklistIfItemsAreHidden
|
||||
input.toggle-switch(type="checkbox" id="toggleHideFinishedChecklist" checked="checked")
|
||||
input.toggle-switch.js-toggle-hide-finished-checklist(type="checkbox" aria-label="{{_ 'hide-finished-checklist'}}" id="{{visibilitySwitchId}}" checked="checked")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleHideFinishedChecklist")
|
||||
label.toggle-label(for="toggleHideFinishedChecklist")
|
||||
input.toggle-switch.js-toggle-hide-finished-checklist(type="checkbox" aria-label="{{_ 'hide-finished-checklist'}}" id="{{visibilitySwitchId}}")
|
||||
label.toggle-label(for="{{visibilitySwitchId}}")
|
||||
|
||||
.card-checklist-items
|
||||
each checklist in checklists
|
||||
@@ -53,13 +53,17 @@ template(name="checklistDetail")
|
||||
title="{{#if checklistCollapsed}}{{_ 'expand-checklist'}}{{else}}{{_ 'collapse-checklist'}}{{/if}}"
|
||||
role="button" tabindex="0"
|
||||
aria-expanded="{{#if checklistCollapsed}}false{{else}}true{{/if}}")
|
||||
unless $eq currentBoard.allowsChecklistDueDate false
|
||||
unless checklist.getDue
|
||||
if canModifyCard
|
||||
+checklistDueDateAdd(checklist=checklist)
|
||||
if canModifyCard
|
||||
a.fa.fa-navicon.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
|
||||
|
||||
if canModifyCard
|
||||
h4.title.js-open-inlined-form.is-editable
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
span.fa.checklist-handle(class="fa-arrows" title="{{_ 'dragChecklist'}}")
|
||||
span.fa.checklist-handle(class="fa-arrows" title="{{_ 'moveChecklist'}}")
|
||||
+viewer
|
||||
= checklist.title
|
||||
else
|
||||
@@ -67,10 +71,9 @@ template(name="checklistDetail")
|
||||
+viewer
|
||||
= checklist.title
|
||||
|
||||
if checklist.getDue
|
||||
+checklistDueDate(checklist=checklist canModifyCard=canModifyCard)
|
||||
else if canModifyCard
|
||||
+checklistDueDateAdd(checklist=checklist)
|
||||
unless $eq currentBoard.allowsChecklistDueDate false
|
||||
if checklist.getDue
|
||||
+checklistDueDate(checklist=checklist canModifyCard=canModifyCard)
|
||||
|
||||
//- The progress bar stays visible when folded: it is the summary of what was
|
||||
folded away, so hiding it too would leave a title that says nothing.
|
||||
@@ -99,12 +102,12 @@ template(name="addChecklistItemForm")
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
|
||||
if showNewlineBecomesNewChecklistItem
|
||||
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItem'}}")
|
||||
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItem")
|
||||
input.toggle-switch(type="checkbox" aria-label="{{_ 'newlineBecomesNewChecklistItem'}}" id="toggleNewlineBecomesNewChecklistItem")
|
||||
label.toggle-label(for="toggleNewlineBecomesNewChecklistItem")
|
||||
| {{_ 'newLineNewItem'}}
|
||||
if $eq position 'top'
|
||||
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItemOriginOrder'}}")
|
||||
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItemOriginOrder")
|
||||
input.toggle-switch(type="checkbox" aria-label="{{_ 'newlineBecomesNewChecklistItemOriginOrder'}}" id="toggleNewlineBecomesNewChecklistItemOriginOrder")
|
||||
label.toggle-label(for="toggleNewlineBecomesNewChecklistItemOriginOrder")
|
||||
| {{_ 'originOrder'}}
|
||||
|
||||
@@ -165,7 +168,7 @@ template(name="checklistItems")
|
||||
|
||||
template(name='checklistItemDetail')
|
||||
.js-checklist-item.checklist-item(class="{{#if item.isFinished }}is-checked{{#if checklist.isItemHidden item.isFinished}} invisible{{/if}}{{/if}}{{#if checklist.hideAllChecklistItems}} is-checked invisible{{/if}}"
|
||||
role="checkbox" aria-checked="{{#if item.isFinished }}true{{else}}false{{/if}}" tabindex="0")
|
||||
role="checkbox" aria-checked="{{#if item.isFinished }}true{{else}}false{{/if}}" aria-readonly="{{#if canCheckChecklistItem}}false{{else}}true{{/if}}" tabindex="{{#if canCheckChecklistItem}}0{{else}}-1{{/if}}")
|
||||
//- #3307: checking off an item is a lower bar than editing/deleting one - a
|
||||
Worker may do the former but not the latter, so the checkbox itself is
|
||||
gated on canCheckChecklistItem while everything else here still needs
|
||||
@@ -177,7 +180,7 @@ template(name='checklistItemDetail')
|
||||
.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
if canModifyCard
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
|
||||
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'move-scope'}}")
|
||||
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
@@ -239,18 +242,18 @@ template(name="checklistActionsPopup")
|
||||
| {{_ "hideCheckedChecklistItems"}} ...
|
||||
.material-toggle-switch(title="{{_ 'hide-checked-items'}}")
|
||||
if checklist.hideCheckedState
|
||||
input.toggle-switch(type="checkbox" id="toggleHideCheckedChecklistItems_{{checklist._id}}" checked="checked")
|
||||
input.toggle-switch(type="checkbox" aria-label="{{_ 'hide-checked-items'}}" id="toggleHideCheckedChecklistItems_{{checklist._id}}" checked="checked")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleHideCheckedChecklistItems_{{checklist._id}}")
|
||||
input.toggle-switch(type="checkbox" aria-label="{{_ 'hide-checked-items'}}" id="toggleHideCheckedChecklistItems_{{checklist._id}}")
|
||||
label.toggle-label(for="toggleHideCheckedChecklistItems_{{checklist._id}}")
|
||||
a.js-hide-all-checklist-items
|
||||
i.fa.fa-ban
|
||||
| {{_ "hideAllChecklistItems"}} ...
|
||||
.material-toggle-switch(title="{{_ 'hideAllChecklistItems'}}")
|
||||
if checklist.hideAllChecklistItems
|
||||
input.toggle-switch(type="checkbox" id="toggleHideAllChecklistItems_{{checklist._id}}" checked="checked")
|
||||
input.toggle-switch(type="checkbox" aria-label="{{_ 'hideAllChecklistItems'}}" id="toggleHideAllChecklistItems_{{checklist._id}}" checked="checked")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleHideAllChecklistItems_{{checklist._id}}")
|
||||
input.toggle-switch(type="checkbox" aria-label="{{_ 'hideAllChecklistItems'}}" id="toggleHideAllChecklistItems_{{checklist._id}}")
|
||||
label.toggle-label(for="toggleHideAllChecklistItems_{{checklist._id}}")
|
||||
a.js-show-checklist-at-minicard
|
||||
i.fa.fa-th-list
|
||||
@@ -261,9 +264,9 @@ template(name="checklistActionsPopup")
|
||||
because the board default (allowsChecklistsOnMinicard, on out of the
|
||||
box) put it there - reported by email.
|
||||
if shownAtMinicard
|
||||
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}" checked="checked")
|
||||
input.toggle-switch(type="checkbox" aria-label="{{_ 'show-on-minicard'}}" id="toggleShowChecklistAtMinicard_{{checklist._id}}" checked="checked")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}")
|
||||
input.toggle-switch(type="checkbox" aria-label="{{_ 'show-on-minicard'}}" id="toggleShowChecklistAtMinicard_{{checklist._id}}")
|
||||
label.toggle-label(for="toggleShowChecklistAtMinicard_{{checklist._id}}")
|
||||
|
||||
//- #3818 / #4729: pick this checklist's automatic-reset interval (or 'none' to
|
||||
@@ -340,9 +343,8 @@ template(name="checklistDueDate")
|
||||
+dateBadgeBody(canModifyCard=canModifyCard baseClass="card-date due-date checklist-due-date" showTitle=showTitle classes=classes showISODate=showISODate showDate=showDate showWeekOfYear=showWeekOfYear showWeek=showWeek)
|
||||
|
||||
template(name="checklistDueDateAdd")
|
||||
button.js-checklist-due-date(type="button" title="{{_ 'card-due'}}" role="button" tabindex="0")
|
||||
button.js-checklist-due-date.checklist-due-date-add(type="button" title="{{_ 'card-due'}}" aria-label="{{_ 'card-due'}}")
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'card-due'}}
|
||||
|
||||
template(name="editChecklistDueDatePopup")
|
||||
+editDateForm(showDate=showDate showTime=showTime error=error datePicker=datePicker)
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Random } from 'meteor/random';
|
||||
import { formatDateForDisplay } from '/client/lib/dateDisplay';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
@@ -205,7 +206,12 @@ Template.checklistDetail.events({
|
||||
},
|
||||
});
|
||||
|
||||
Template.checklists.onCreated(function () {
|
||||
this.visibilitySwitchId = `toggleHideFinishedChecklist_${Random.id()}`;
|
||||
});
|
||||
|
||||
Template.checklists.helpers({
|
||||
visibilitySwitchId() { return Template.instance().visibilitySwitchId; },
|
||||
checklists() {
|
||||
const card = ReactiveCache.getCard(this.cardId);
|
||||
if (!card || typeof card.checklists !== 'function') {
|
||||
@@ -221,9 +227,11 @@ Template.checklists.events({
|
||||
// #4017: apply/append a template card's checklists onto this already
|
||||
// existing card, alongside whatever checklists it already has.
|
||||
'click .js-copy-checklist-from-template': Popup.open('copyChecklistFromTemplate'),
|
||||
'submit .js-add-checklist'(event, tpl) {
|
||||
async 'submit .js-add-checklist'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = tpl.find('textarea.js-add-checklist-item');
|
||||
const form = event.currentTarget;
|
||||
if (form.dataset.submitting === 'true') return;
|
||||
const textarea = form.querySelector('textarea.js-add-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
let cardId = Template.currentData().cardId;
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
@@ -242,11 +250,22 @@ Template.checklists.events({
|
||||
}
|
||||
|
||||
if (title) {
|
||||
Checklists.insert({
|
||||
cardId,
|
||||
title,
|
||||
sort: sortIndex,
|
||||
});
|
||||
form.dataset.submitting = 'true';
|
||||
try {
|
||||
// Keep the client insert path used by this form; wait for the server
|
||||
// acknowledgement before accepting another submission.
|
||||
await new Promise((resolve, reject) => {
|
||||
Checklists.insert({ cardId, title, sort: sortIndex }, (error, id) => {
|
||||
if (error) reject(error);
|
||||
else resolve(id);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
delete form.dataset.submitting;
|
||||
alert(error.reason || error.message || TAPi18n.__('server-error'));
|
||||
return;
|
||||
}
|
||||
textarea.value = '';
|
||||
tpl.$('.js-close-inlined-form').click();
|
||||
setTimeout(() => {
|
||||
tpl.$('.add-checklist-item')
|
||||
@@ -389,7 +408,7 @@ Template.checklists.events({
|
||||
'click .js-open-inlined-form'(event, tpl) {
|
||||
tpl.$('.js-close-inlined-form').click();
|
||||
},
|
||||
'click #toggleHideFinishedChecklist'(event) {
|
||||
'click .js-toggle-hide-finished-checklist'(event) {
|
||||
event.preventDefault();
|
||||
Template.currentData().card.toggleHideFinishedChecklist();
|
||||
},
|
||||
@@ -596,6 +615,13 @@ Template.checklistItemDetail.helpers({
|
||||
});
|
||||
|
||||
Template.checklistItemDetail.events({
|
||||
'keydown .js-checklist-item'(event) {
|
||||
if (event.target !== event.currentTarget || event.repeat || event.originalEvent?.repeat || ![' ', 'Enter'].includes(event.key)) return;
|
||||
if (event.currentTarget.getAttribute('aria-readonly') === 'true') return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.currentTarget.querySelector('.check-box-container')?.click();
|
||||
},
|
||||
// #2422: open the checklist item's linked subtask card. Reuses the same
|
||||
// navigation guard subtasks.js uses for its own "View it" button, so a
|
||||
// subtask on another (not-yet-loaded) board resolves the same way.
|
||||
|
||||
@@ -36,7 +36,7 @@ template(name="cardLabelsPopup")
|
||||
li.js-card-label-item
|
||||
a.card-label-edit-button.fa.fa-pencil.js-edit-label
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
span.fa.label-handle(class="fa-arrows" title="{{_ 'dragLabel'}}")
|
||||
span.fa.label-handle(class="fa-arrows" title="{{_ 'move-scope'}}")
|
||||
span.card-label.card-label-selectable.js-select-label.card-label-wrapper(class="{{labelColorClass color}}"
|
||||
class="{{# if isLabelSelected ../_id }}active{{/if}}" style="{{labelColorStyle color}}")
|
||||
+viewer
|
||||
|
||||
@@ -309,9 +309,17 @@
|
||||
/* #4990: the title text is the edit target. It is a block so that the whole
|
||||
line, not only the words, is that target: a click on the empty space after a
|
||||
short title is still a click on the title. */
|
||||
.minicard .minicard-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
.minicard .minicard-title .minicard-title-text {
|
||||
display: block;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Direct title editing is disabled; the former editor styles are retained as
|
||||
@@ -1070,11 +1078,8 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
/* Nudge down so the checkmark sits at the vertical middle of the title text
|
||||
(the checked materialCheckBox is shifted up 4px by its own positioning). */
|
||||
position: relative;
|
||||
top: 4px;
|
||||
margin-inline-end: 6px;
|
||||
flex-shrink: 0;
|
||||
margin-inline-end: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,40 +38,44 @@ template(name="minicard")
|
||||
fold on purpose - they must always be reachable, collapsed or not - and
|
||||
the title right after the caret is likewise unconditional: collapsed, the
|
||||
minicard shows only the caret and the title.
|
||||
a.fa.minicard-collapse.js-collapse-minicard(
|
||||
class="{{#if minicardCollapsed}}fa-caret-right{{else}}fa-caret-down{{/if}}"
|
||||
title="{{#if minicardCollapsed}}{{_ 'uncollapse'}}{{else}}{{_ 'collapse'}}{{/if}}"
|
||||
aria-label="{{#if minicardCollapsed}}{{_ 'uncollapse'}}{{else}}{{_ 'collapse'}}{{/if}}"
|
||||
role="button" tabindex="0"
|
||||
aria-expanded="{{#if minicardCollapsed}}false{{else}}true{{/if}}")
|
||||
if showCollapseControl
|
||||
a.fa.minicard-collapse.js-collapse-minicard(
|
||||
class="{{#if minicardCollapsed}}fa-caret-right{{else}}fa-caret-down{{/if}}"
|
||||
title="{{#if minicardCollapsed}}{{_ 'uncollapse'}}{{else}}{{_ 'collapse'}}{{/if}}"
|
||||
aria-label="{{#if minicardCollapsed}}{{_ 'uncollapse'}}{{else}}{{_ 'collapse'}}{{/if}}"
|
||||
role="button" tabindex="0"
|
||||
aria-expanded="{{#if minicardCollapsed}}false{{else}}true{{/if}}")
|
||||
if showLabelsAboveTitle
|
||||
+minicardLabels(show=showLabels labels=labels hideText=hiddenMinicardLabelText)
|
||||
.minicard-title
|
||||
if showDueComplete
|
||||
if canModifyCard
|
||||
a.minicard-complete-toggle.js-toggle-card-complete(title="{{#if getDueComplete}}{{_ 'card-mark-incomplete'}}{{else}}{{_ 'card-mark-complete'}}{{/if}}")
|
||||
.materialCheckBox(class="{{#if getDueComplete}}is-checked{{/if}}")
|
||||
if $eq 'prefix-with-full-path' currentBoard.presentParentTask
|
||||
.parent-prefix
|
||||
| {{ parentString ' > ' }}
|
||||
if $eq 'prefix-with-parent' currentBoard.presentParentTask
|
||||
.parent-prefix
|
||||
| {{ parentCardName }}
|
||||
if isLinkedBoard
|
||||
a.js-linked-link
|
||||
span.linked-icon
|
||||
i.fa.fa-folder
|
||||
else if isLinkedCard
|
||||
a.js-linked-link
|
||||
span.linked-icon
|
||||
i.fa.fa-id-card
|
||||
if getArchived
|
||||
span.linked-icon.linked-archived
|
||||
i.fa.fa-archive
|
||||
span.minicard-title-text
|
||||
if showCardNumber
|
||||
span.card-number
|
||||
| ##{getCardNumber}
|
||||
+viewer
|
||||
= getTitle
|
||||
.minicard-title-row
|
||||
if showDueComplete
|
||||
if canModifyCard
|
||||
a.minicard-complete-toggle.js-toggle-card-complete(title="{{#if getDueComplete}}{{_ 'card-mark-incomplete'}}{{else}}{{_ 'card-mark-complete'}}{{/if}}")
|
||||
.materialCheckBox(class="{{#if getDueComplete}}is-checked{{/if}}")
|
||||
if isLinkedBoard
|
||||
a.js-linked-link
|
||||
span.linked-icon
|
||||
i.fa.fa-folder
|
||||
else if isLinkedCard
|
||||
a.js-linked-link
|
||||
span.linked-icon
|
||||
i.fa.fa-id-card
|
||||
if getArchived
|
||||
span.linked-icon.linked-archived
|
||||
i.fa.fa-archive
|
||||
span.minicard-title-text
|
||||
if showCardNumber
|
||||
span.card-number
|
||||
| ##{getCardNumber}
|
||||
+viewer
|
||||
= getTitle
|
||||
//- Everything under the title renders in the board's minicard order
|
||||
(Board Settings / Card, "Show on Minicard"; board.minicardFieldOrder,
|
||||
models/lib/cardFieldOrder.js): the sections top to bottom, and inside
|
||||
@@ -143,16 +147,8 @@ template(name="minicard")
|
||||
.minicard-cover(style="background-image: url('{{cover.link 'original'}}?dummyReloadAfterSessionEstablished={{sess}}');")
|
||||
|
||||
if $eq section "labels"
|
||||
if showLabels
|
||||
if labels
|
||||
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
|
||||
each labels
|
||||
unless hiddenMinicardLabelText
|
||||
span.js-card-label.card-label(class="{{labelColorClass color}}" style="{{labelColorStyle color}}" title=name)
|
||||
+viewer
|
||||
= name
|
||||
if hiddenMinicardLabelText
|
||||
.minicard-label(class="{{labelColorClass color}}" style="{{labelColorStyle color}}" title="{{name}}")
|
||||
unless labelsAboveTitle
|
||||
+minicardLabels(show=showLabels labels=labels hideText=hiddenMinicardLabelText)
|
||||
|
||||
if $eq section "customFields"
|
||||
if showCustomFieldsOnMinicard
|
||||
@@ -402,3 +398,15 @@ template(name="minicardChecklist")
|
||||
else
|
||||
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
template(name="minicardLabels")
|
||||
if show
|
||||
if labels
|
||||
.minicard-labels(class="{{#if hideText}}minicard-labels-no-text{{/if}}")
|
||||
each label in labels
|
||||
unless hideText
|
||||
span.js-card-label.card-label(class="{{labelColorClass label.color}}" style="{{labelColorStyle label.color}}" title=label.name)
|
||||
+viewer
|
||||
= label.name
|
||||
if hideText
|
||||
.minicard-label(class="{{labelColorClass label.color}}" style="{{labelColorStyle label.color}}" title="{{label.name}}")
|
||||
@@ -1,3 +1,4 @@
|
||||
import { relativeCardSort } from '/client/lib/relativeCardPosition';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { CustomFieldStringTemplate } from '/client/lib/customFields';
|
||||
@@ -40,8 +41,14 @@ function getMinicardFlag(board, onMinicardField, legacyField, defaultValue) {
|
||||
|
||||
Template.minicard.helpers({
|
||||
// #1591: the whole-minicard fold, same shape as a list's collapsed() helper.
|
||||
showCollapseControl() { return ReactiveCache.getBoard(this.boardId)?.allowsMinicardCollapse !== false; },
|
||||
showLabelsAboveTitle() {
|
||||
const board = ReactiveCache.getBoard(this.boardId);
|
||||
return board?.labelsAboveTitleOnMinicard === true && (board.allowsMinicardCollapse === false || !Utils.getCardCollapseState(this));
|
||||
},
|
||||
labelsAboveTitle() { return ReactiveCache.getBoard(this.boardId)?.labelsAboveTitleOnMinicard === true; },
|
||||
minicardCollapsed() {
|
||||
return Utils.getCardCollapseState(this);
|
||||
return ReactiveCache.getBoard(this.boardId)?.allowsMinicardCollapse !== false && Utils.getCardCollapseState(this);
|
||||
},
|
||||
// The minicard's field order from Board Settings / Card's "Show on
|
||||
// Minicard" column (board.minicardFieldOrder): the sections top to bottom,
|
||||
@@ -390,24 +397,21 @@ Template.minicard.helpers({
|
||||
|
||||
// #459: accessible reordering — keyboard/screen-reader users can move a card up
|
||||
// or down within its list via sr-only buttons (no drag-and-drop required). The
|
||||
// move swaps the card's sort value with its neighbour in the same list+swimlane.
|
||||
function moveCardBy(card, delta) {
|
||||
// move inserts beside its neighbor in the same list+swimlane, repairing ties.
|
||||
async function moveCardBy(card, delta) {
|
||||
const siblings = ReactiveCache.getCards(
|
||||
{ listId: card.listId, swimlaneId: card.swimlaneId, archived: false },
|
||||
{ sort: { sort: 1 } },
|
||||
{ boardId: card.boardId, listId: card.listId, swimlaneId: card.swimlaneId, archived: false, deletedAt: null },
|
||||
{ sort: { sort: 1, _id: 1 } },
|
||||
);
|
||||
const idx = siblings.findIndex(c => c._id === card._id);
|
||||
const target = siblings[idx + delta];
|
||||
if (idx < 0 || !target) return;
|
||||
// Capture both sort values before either update; the docs are reactive and
|
||||
// card.sort would otherwise change after the first move.
|
||||
const cardSort = card.sort;
|
||||
const targetSort = target.sort;
|
||||
// Persist through the card model's move() mutation — the canonical client
|
||||
// path (e.g. editCardSortOrderPopup). A raw Cards.update of `sort` is the
|
||||
// wrong path here and would be reverted.
|
||||
card.move(card.boardId, card.swimlaneId, card.listId, targetSort);
|
||||
target.move(target.boardId, target.swimlaneId, target.listId, cardSort);
|
||||
try {
|
||||
const sort = await relativeCardSort(target, delta < 0 ? 'above' : 'below', card._id);
|
||||
await card.move(card.boardId, card.swimlaneId, card.listId, sort);
|
||||
} catch (error) {
|
||||
window.alert(error.reason || error.message || TAPi18n.__('server-error'));
|
||||
}
|
||||
}
|
||||
|
||||
Template.minicard.events({
|
||||
|
||||
@@ -93,7 +93,7 @@ template(name="subtasksItems")
|
||||
else
|
||||
a.add-subtask-item.js-open-inlined-form
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-subtask-item'}}...
|
||||
| {{_ 'add-subtask'}}...
|
||||
|
||||
template(name='subtaskItemDetail')
|
||||
.js-subtasks-item.subtasks-item
|
||||
|
||||
@@ -807,8 +807,21 @@ button.loud-text-button:hover {
|
||||
.toggle-switch:checked:disabled ~ .toggle-label:after {
|
||||
background-color: #bcbdbc;
|
||||
}
|
||||
.toggle-switch {
|
||||
display: none;
|
||||
.material-toggle-switch input.toggle-switch[type="checkbox"] {
|
||||
display: block;
|
||||
visibility: visible;
|
||||
position: absolute;
|
||||
inset-inline-start: auto;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
clip-path: inset(50%);
|
||||
overflow: hidden;
|
||||
}
|
||||
.material-toggle-switch .toggle-switch:focus-visible ~ .toggle-label {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
.toggle-switch-title {
|
||||
margin: 0 0.5em;
|
||||
|
||||
@@ -3,6 +3,21 @@
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.gantt-view .gantt-container {
|
||||
font-size: calc(12px * var(--wekan-ui-font-scale, 1));
|
||||
line-height: calc(14.5px * var(--wekan-ui-font-scale, 1));
|
||||
}
|
||||
.gantt-view .gantt-container .lower-text {
|
||||
font-size: calc(12px * var(--wekan-ui-font-scale, 1));
|
||||
}
|
||||
.gantt-view .gantt-container .upper-text,
|
||||
.gantt-view .gantt-container .side-header * {
|
||||
font-size: calc(14px * var(--wekan-ui-font-scale, 1));
|
||||
}
|
||||
.gantt-view .gantt .bar-label {
|
||||
font-size: calc(13px * var(--wekan-ui-font-scale, 1));
|
||||
}
|
||||
|
||||
.frappe-gantt-empty {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { fontScaleValue } from '/models/lib/uiFonts';
|
||||
import { titleViewerHtml, titleViewerSvgText } from '/client/lib/titleViewer';
|
||||
import { formatDateForDisplay, dateDisplayPreferences } from '/client/lib/dateDisplay';
|
||||
import { Template } from 'meteor/templating';
|
||||
@@ -204,7 +206,13 @@ Template.frappeGanttView.onRendered(function() {
|
||||
// observer below re-translates them whenever they reappear.
|
||||
const viewModes = translatedViewModes(GanttLib);
|
||||
translateFrappeChrome(container, templateInstance);
|
||||
const fontScale = Number(fontScaleValue(Meteor.user()?.profile?.uiFontSize) || 1);
|
||||
new GanttLib(container, tasks, {
|
||||
// Scale geometry with SVG text, so the 150% preset still fits its bars
|
||||
// and date headers rather than merely increasing the glyph sizes.
|
||||
bar_height: 30 * fontScale,
|
||||
upper_header_height: 45 * fontScale,
|
||||
lower_header_height: 30 * fontScale,
|
||||
view_modes: viewModes,
|
||||
view_mode: viewModes.find(mode => mode._key === 'WEEK').name,
|
||||
// Month names in the header come from Intl.DateTimeFormat(language);
|
||||
|
||||
@@ -211,7 +211,7 @@ template(name="importMapMembersAddPopup")
|
||||
p
|
||||
| {{_ 'import-user-select'}}
|
||||
.js-map-member
|
||||
input.js-search-member-input(type="text" placeholder="{{_ 'search-users'}}")
|
||||
input.js-search-member-input(type="text" placeholder="{{_ 'search'}}")
|
||||
ul.pop-over-list
|
||||
each searchResults
|
||||
li.item.js-member-item
|
||||
|
||||
@@ -281,6 +281,13 @@ Template.list.onCreated(function () {
|
||||
// callback, we basically solve all issues related to reactive updates. A
|
||||
// comment below provides further details.
|
||||
Template.list.onRendered(function () {
|
||||
this.initializeListResize();
|
||||
});
|
||||
|
||||
// #6705: collapse removes listBody but keeps the parent list alive. Bind card
|
||||
// widgets and their reactive work to the body so expansion initializes the new
|
||||
// .js-minicards element, including lists that were collapsed on page load.
|
||||
Template.listBody.onRendered(function () {
|
||||
const boardBodyEl =
|
||||
this.firstNode?.parentElement?.closest?.('.board-body') ||
|
||||
document.querySelector('.board-body');
|
||||
@@ -288,11 +295,10 @@ Template.list.onRendered(function () {
|
||||
boardBodyEl && Blaze.getView(boardBodyEl, 'Template.boardBody');
|
||||
const boardComponent = boardView?.templateInstance?.();
|
||||
|
||||
// Initialize list resize functionality immediately
|
||||
this.initializeListResize();
|
||||
|
||||
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
|
||||
const $cards = this.$('.js-minicards');
|
||||
this.cardDragTargets = $cards;
|
||||
this.cardDragDestroyed = false;
|
||||
|
||||
// Destroy any existing sortable before re-initializing. Without this, a
|
||||
// re-render of the list (e.g. when a Card Details panel opens) binds a SECOND
|
||||
@@ -668,6 +674,7 @@ Template.list.onRendered(function () {
|
||||
// .droppable() on initialized elements is an idempotent option refresh.
|
||||
ReactiveCache.getCards({ boardId: currentBoardId });
|
||||
Tracker.afterFlush(() => {
|
||||
if (this.cardDragDestroyed) return;
|
||||
$cards.find(itemsSelector).droppable({
|
||||
hoverClass: 'draggable-hover-card',
|
||||
accept: '.js-member,.js-label',
|
||||
@@ -688,6 +695,15 @@ Template.list.onRendered(function () {
|
||||
});
|
||||
});
|
||||
|
||||
Template.listBody.onDestroyed(function () {
|
||||
this.cardDragDestroyed = true;
|
||||
const $cards = this.cardDragTargets;
|
||||
if ($cards && ($cards.data('uiSortable') || $cards.data('sortable'))) {
|
||||
$cards.sortable('destroy');
|
||||
}
|
||||
this.cardDragTargets = null;
|
||||
});
|
||||
|
||||
// A board-wide list (no swimlaneId of its own) renders once per swimlane in
|
||||
// Swimlanes view - the SAME list document, one row per swimlane - so its
|
||||
// collapse state must be resolved per-swimlane, or collapsing it in one
|
||||
|
||||
@@ -131,7 +131,7 @@ template(name="listHeader")
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
a.list-header-handle-desktop.handle.nodragscroll.js-list-handle(title="{{_ 'drag-list'}}")
|
||||
a.list-header-handle-desktop.handle.nodragscroll.js-list-handle(title="{{_ 'moveListPopup-title'}}")
|
||||
i.fa.fa-arrows
|
||||
unless collapsed
|
||||
div.list-header-menu
|
||||
@@ -142,7 +142,7 @@ template(name="listHeader")
|
||||
//
|
||||
a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
a.list-header-handle-desktop.handle.nodragscroll.js-list-handle(title="{{_ 'drag-list'}}")
|
||||
a.list-header-handle-desktop.handle.nodragscroll.js-list-handle(title="{{_ 'moveListPopup-title'}}")
|
||||
i.fa.fa-arrows
|
||||
if canSeeAddCard
|
||||
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
|
||||
@@ -384,7 +384,7 @@ template(name="setListWidthPopup")
|
||||
unless isAutoWidth
|
||||
label {{_ 'set-list-width-value'}}
|
||||
p
|
||||
input.list-width-value(type="number" value="{{ listWidthValue }}" min="270")
|
||||
input.list-width-value(type="number" value="{{ listWidthValue }}" min="{{ listWidthMinimum }}" step="1")
|
||||
input.list-width-apply(type="submit" value="{{_ 'apply'}}")
|
||||
input.list-width-error
|
||||
if canChangeAutoWidth
|
||||
@@ -398,7 +398,7 @@ template(name="setListWidthPopup")
|
||||
|
||||
template(name="listWidthErrorPopup")
|
||||
.list-width-invalid
|
||||
p {{_ 'list-width-error-message'}} '>=270'
|
||||
p {{_ 'list-width-error-message'}}
|
||||
button.full.js-back-view(type="submit") {{_ 'cancel'}}
|
||||
|
||||
template(name="setListColorPopup")
|
||||
@@ -413,7 +413,7 @@ template(name="setListColorPopup")
|
||||
label(for="listColorWheel") {{_ 'custom-color'}}
|
||||
input.js-list-color-wheel#listColorWheel(type="color" value=currentColorHex title="{{_ 'custom-color'}}")
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'remove-btn'}}
|
||||
|
||||
//- List sync settings (docs/Features/ImportExport/Sync.md). UI wiring only -
|
||||
calls the EXISTING setListSyncSource/hasListSyncCredential/syncListNow
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { relativePosition } from '/models/lib/relativePosition';
|
||||
import { formatDateForDisplay } from '/client/lib/dateDisplay';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import Lists from '../../../models/lists';
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
import {
|
||||
DEFAULT_LIST_WIDTH,
|
||||
MIN_LIST_WIDTH,
|
||||
parseListWidthInput,
|
||||
normalizeListWidth,
|
||||
} from '/models/lib/listWidth';
|
||||
// List sync (docs/Features/ImportExport/Sync.md): UI wiring only, calls the
|
||||
@@ -341,24 +343,26 @@ Template.listHeader.onCreated(function () {
|
||||
});
|
||||
|
||||
// #459: accessible reordering — move a list left/right via sr-only buttons by
|
||||
// swapping its sort value with the adjacent list (no drag-and-drop required).
|
||||
function moveListBy(list, delta) {
|
||||
const siblings = ReactiveCache.getLists(
|
||||
{ boardId: list.boardId, archived: false },
|
||||
{ sort: { sort: 1 } },
|
||||
);
|
||||
// inserting beside an adjacent visible list (no drag-and-drop required).
|
||||
async function moveListBy(list, delta) {
|
||||
const swimlaneId = resolveContainerSwimlaneId(list);
|
||||
const selector = { boardId: list.boardId, archived: false, deletedAt: null };
|
||||
if (Utils.boardView() === 'board-view-swimlanes' && swimlaneId) {
|
||||
selector.swimlaneId = { $in: [swimlaneId, null, ''] };
|
||||
}
|
||||
const siblings = ReactiveCache.getLists(selector, { sort: { sort: 1 } });
|
||||
const idx = siblings.findIndex(l => l._id === list._id);
|
||||
const target = siblings[idx + delta];
|
||||
if (idx < 0 || !target) return;
|
||||
// Capture both sort values before either update; the docs are reactive and
|
||||
// list.sort would otherwise change after the first update.
|
||||
const listSort = list.sort;
|
||||
const targetSort = target.sort;
|
||||
// Lists are a server-restricted collection; persist the swap through the
|
||||
// updateListSort method — the same path the drag-and-drop reorder uses. A raw
|
||||
// client Lists.update of `sort` is reverted by the server.
|
||||
Meteor.call('updateListSort', list._id, list.boardId, { sort: targetSort });
|
||||
Meteor.call('updateListSort', target._id, target.boardId, { sort: listSort });
|
||||
try {
|
||||
const plan = relativePosition(siblings, target._id, delta < 0 ? 'above' : 'below', list._id);
|
||||
for (const update of plan.updates) {
|
||||
await Meteor.callAsync('updateListSort', update.id, list.boardId, { sort: update.sort });
|
||||
}
|
||||
await Meteor.callAsync('updateListSort', list._id, list.boardId, { sort: plan.sort });
|
||||
} catch (error) {
|
||||
window.alert(error.reason || error.message || TAPi18n.__('server-error'));
|
||||
}
|
||||
}
|
||||
|
||||
Template.listHeader.events({
|
||||
@@ -1094,6 +1098,9 @@ function setAnonFixedListWidth(boardId, width) {
|
||||
}
|
||||
|
||||
Template.setListWidthPopup.helpers({
|
||||
listWidthMinimum() {
|
||||
return MIN_LIST_WIDTH;
|
||||
},
|
||||
listWidthValue() {
|
||||
const list = Template.currentData();
|
||||
// #5729 In fixed width mode the input edits the single per-board value.
|
||||
@@ -1188,9 +1195,9 @@ Template.setListWidthPopup.events({
|
||||
'click .list-width-apply'(event, tpl) {
|
||||
const list = Template.currentData();
|
||||
const boardId = list.boardId;
|
||||
const width = parseInt(tpl.$('.list-width-value').val(), 10);
|
||||
const width = parseListWidthInput(tpl.$('.list-width-value').val());
|
||||
|
||||
if (!width || width < MIN_LIST_WIDTH) {
|
||||
if (width === null) {
|
||||
tpl.$('.list-width-error').click();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ template(name="minilist")
|
||||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
span.drag-handle(title="{{_ 'dragList'}}")
|
||||
span.drag-handle(title="{{_ 'moveListPopup-title'}}")
|
||||
i.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
||||
@@ -1,18 +1,18 @@
|
||||
template(name="bookmarks")
|
||||
.panel
|
||||
h2 {{_ 'bookmarks'}}
|
||||
h2 {{_ 'starred-boards'}}
|
||||
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'}}")
|
||||
a.js-toggle-star(title="{{_ 'unstar-item'}}")
|
||||
i.fa.fa-star
|
||||
else
|
||||
p {{_ 'no-starred-boards'}}
|
||||
p {{_ 'no-starred-items'}}
|
||||
else
|
||||
p {{_ 'please-sign-in'}}
|
||||
p {{_ 'log-in'}}
|
||||
|
||||
// Desktop popup
|
||||
template(name="bookmarksPopup")
|
||||
@@ -23,7 +23,7 @@ template(name="bookmarksPopup")
|
||||
a(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
i.fa.fa-star
|
||||
| #{title}
|
||||
a.js-toggle-star.right(title="{{_ 'star-board-short-unstar'}}")
|
||||
a.js-toggle-star.right(title="{{_ 'unstar-item'}}")
|
||||
i.fa.fa-star
|
||||
else
|
||||
li {{_ 'no-starred-boards'}}
|
||||
li {{_ 'no-starred-items'}}
|
||||
@@ -1,12 +1,25 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { findWhere } from '/imports/lib/collectionHelpers';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import Attachments from '/models/attachments';
|
||||
import { Markdown } from 'meteor/wekan-markdown';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import { memberMatchesTerm } from '/models/lib/memberAutocomplete';
|
||||
import autosize from 'autosize';
|
||||
var converter = require('@wekanteam/html-to-markdown');
|
||||
|
||||
function syncMarkdownExternalLinkPattern(setting) {
|
||||
const pattern = {
|
||||
prefix: (setting && setting.externalLinkPatternPrefix) || '',
|
||||
urlTemplate: (setting && setting.externalLinkPatternUrl) || '',
|
||||
};
|
||||
const current = Markdown.externalLinkPattern.get();
|
||||
// ReactiveVar compares objects by identity. Setting a new object on every
|
||||
// viewer render would invalidate that viewer again indefinitely.
|
||||
if (current?.prefix !== pattern.prefix || current?.urlTemplate !== pattern.urlTemplate) {
|
||||
Markdown.externalLinkPattern.set(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
const specialHandles = [
|
||||
{userId: 'board_members', username: 'board_members'},
|
||||
{userId: 'card_members', username: 'card_members'},
|
||||
@@ -66,232 +79,9 @@ Template.editor.onRendered(function () {
|
||||
},
|
||||
];
|
||||
|
||||
const enableTextarea = function() {
|
||||
const $textarea = tpl.$(textareaSelector);
|
||||
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
|
||||
? [
|
||||
['view', ['fullscreen']],
|
||||
['table', ['table']],
|
||||
['font', ['bold', 'underline']],
|
||||
//['fontsize', ['fontsize']],
|
||||
['color', ['color']],
|
||||
]
|
||||
: [
|
||||
['style', ['style']],
|
||||
['font', ['bold', 'underline', 'clear']],
|
||||
['fontsize', ['fontsize']],
|
||||
['fontname', ['fontname']],
|
||||
['color', ['color']],
|
||||
['para', ['ul', 'ol', 'paragraph']],
|
||||
['table', ['table']],
|
||||
//['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
|
||||
['insert', ['link']], //, 'picture']], // modal popup has issue somehow :(
|
||||
['view', ['fullscreen', 'codeview', 'help']],
|
||||
];
|
||||
const cleanPastedHTML = function(input) {
|
||||
const badTags = [
|
||||
'style',
|
||||
'script',
|
||||
'applet',
|
||||
'embed',
|
||||
'noframes',
|
||||
'noscript',
|
||||
'meta',
|
||||
'link',
|
||||
'button',
|
||||
'form',
|
||||
].join('|');
|
||||
const badPatterns = new RegExp(
|
||||
`(?:${[
|
||||
`<(${badTags})\\s*[^>][\\s\\S]*?<\\/\\1>`,
|
||||
`<(${badTags})[^>]*?\\/>`,
|
||||
].join('|')})`,
|
||||
'gi',
|
||||
);
|
||||
let output = input;
|
||||
// remove bad Tags
|
||||
output = output.replace(badPatterns, '');
|
||||
// remove attributes ' style="..."'
|
||||
const badAttributes = new RegExp(
|
||||
`(?:${[
|
||||
'on\\S+=([\'\"]?).*?\\1',
|
||||
'href=([\'\"]?)javascript:.*?\\2',
|
||||
'style=([\'\"]?).*?\\3',
|
||||
'target=\\S+',
|
||||
].join('|')})`,
|
||||
'gi',
|
||||
);
|
||||
output = output.replace(badAttributes, '');
|
||||
output = output.replace(/(<a )/gi, '$1target=_ '); // always to new target
|
||||
return output;
|
||||
};
|
||||
const editor = '.editor';
|
||||
const selectors = [
|
||||
`.js-new-description-form ${editor}`,
|
||||
`.js-new-comment-form ${editor}`,
|
||||
`.js-edit-comment ${editor}`,
|
||||
].join(','); // only new comment and edit comment
|
||||
const inputs = $(selectors);
|
||||
if (inputs.length === 0) {
|
||||
// only enable richereditor to new comment or edit comment no others
|
||||
enableTextarea();
|
||||
} else {
|
||||
const placeholder = inputs.attr('placeholder') || '';
|
||||
const mSummernotes = [];
|
||||
const getSummernote = function(input) {
|
||||
const idx = inputs.index(input);
|
||||
if (idx > -1) {
|
||||
return mSummernotes[idx];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
inputs.each(function(idx, input) {
|
||||
mSummernotes[idx] = $(input).summernote({
|
||||
placeholder,
|
||||
callbacks: {
|
||||
onInit(object) {
|
||||
const originalInput = this;
|
||||
$(originalInput).on('submitted', function() {
|
||||
// when comment is submitted, the original textarea will be set to '', so shall we
|
||||
if (!this.value) {
|
||||
const sn = getSummernote(this);
|
||||
sn && sn.summernote('code', '');
|
||||
}
|
||||
});
|
||||
const jEditor = object && object.editable;
|
||||
const toolbar = object && object.toolbar;
|
||||
if (jEditor !== undefined) {
|
||||
jEditor.escapeableTextComplete(mentions);
|
||||
}
|
||||
if (toolbar !== undefined) {
|
||||
const fBtn = toolbar.find('.btn-fullscreen');
|
||||
fBtn.on('click', function() {
|
||||
const $this = $(this),
|
||||
isActive = $this.hasClass('active');
|
||||
$('.minicards,#header-quick-access').toggle(!isActive); // mini card is still showing when editor is in fullscreen mode, we hide here manually
|
||||
});
|
||||
}
|
||||
},
|
||||
onImageUpload(files) {
|
||||
const $summernote = getSummernote(this);
|
||||
if (files && files.length > 0) {
|
||||
const image = files[0];
|
||||
const currentCard = Utils.getCurrentCard();
|
||||
const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
|
||||
const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
|
||||
const processUpload = async function(file) {
|
||||
const uploader = await Attachments.insertAsync(
|
||||
{
|
||||
file,
|
||||
meta: Utils.getCommonAttachmentMetaFrom(currentCard),
|
||||
chunkSize: 'dynamic',
|
||||
},
|
||||
false,
|
||||
);
|
||||
uploader.on('uploaded', (error, fileRef) => {
|
||||
if (!error) {
|
||||
if (fileRef.isImage) {
|
||||
const img = document.createElement('img');
|
||||
img.src = fileRef.link();
|
||||
img.setAttribute('width', '100%');
|
||||
$summernote.summernote('insertNode', img);
|
||||
}
|
||||
}
|
||||
});
|
||||
uploader.start();
|
||||
};
|
||||
if (MAX_IMAGE_PIXEL) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const dataurl = e && e.target && e.target.result;
|
||||
if (dataurl !== undefined) {
|
||||
// need to shrink image
|
||||
Utils.shrinkImage({
|
||||
dataurl,
|
||||
maxSize: MAX_IMAGE_PIXEL,
|
||||
ratio: COMPRESS_RATIO,
|
||||
toBlob: true,
|
||||
callback(blob) {
|
||||
if (blob !== false) {
|
||||
blob.name = image.name;
|
||||
processUpload(blob);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(image);
|
||||
} else {
|
||||
processUpload(image);
|
||||
}
|
||||
}
|
||||
},
|
||||
onPaste(e) {
|
||||
var clipboardData = e.clipboardData;
|
||||
var pastedData = clipboardData.getData('Text');
|
||||
|
||||
//if pasted data is an image, exit
|
||||
if (!pastedData.length) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// clear up unwanted tag info when user pasted in text
|
||||
const thisNote = this;
|
||||
const updatePastedText = function(object) {
|
||||
const someNote = getSummernote(object);
|
||||
// Fix Pasting text into a card is adding a line before and after
|
||||
// (and multiplies by pasting more) by changing paste "p" to "br".
|
||||
// Fixes https://github.com/wekan/wekan/2890 .
|
||||
// == Fix Start ==
|
||||
someNote.execCommand('defaultParagraphSeparator', false, 'br');
|
||||
// == Fix End ==
|
||||
const original = someNote.summernote('code');
|
||||
const cleaned = cleanPastedHTML(original); //this is where to call whatever clean function you want. I have mine in a different file, called CleanPastedHTML.
|
||||
someNote.summernote('code', ''); //clear original
|
||||
someNote.summernote('pasteHTML', cleaned); //this sets the displayed content editor to the cleaned pasted code.
|
||||
};
|
||||
setTimeout(function() {
|
||||
//this kinda sucks, but if you don't do a setTimeout,
|
||||
//the function is called before the text is really pasted.
|
||||
updatePastedText(thisNote);
|
||||
}, 10);
|
||||
},
|
||||
},
|
||||
dialogsInBody: true,
|
||||
spellCheck: true,
|
||||
disableGrammar: false,
|
||||
disableDragAndDrop: false,
|
||||
toolbar,
|
||||
popover: {
|
||||
image: [
|
||||
['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
|
||||
['float', ['floatLeft', 'floatRight', 'floatNone']],
|
||||
['remove', ['removeMedia']],
|
||||
],
|
||||
link: [['link', ['linkDialogShow', 'unlink']]],
|
||||
table: [
|
||||
['add', ['addRowDown', 'addRowUp', 'addColLeft', 'addColRight']],
|
||||
['delete', ['deleteRow', 'deleteCol', 'deleteTable']],
|
||||
],
|
||||
air: [
|
||||
['color', ['color']],
|
||||
['font', ['bold', 'underline', 'clear']],
|
||||
],
|
||||
},
|
||||
height: 200,
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
enableTextarea();
|
||||
}
|
||||
enableTextarea();
|
||||
const $textarea = tpl.$(textareaSelector);
|
||||
autosize($textarea);
|
||||
$textarea.escapeableTextComplete(mentions);
|
||||
});
|
||||
|
||||
Template.editor.events({
|
||||
@@ -373,10 +163,7 @@ Blaze.Template.registerHelper(
|
||||
// wekan/wekan#3069: same same-render-pass reasoning as alwaysShowCodeAsText
|
||||
// above, for the external issue-tracker autolink pattern.
|
||||
if (typeof Markdown !== 'undefined' && Markdown.externalLinkPattern) {
|
||||
Markdown.externalLinkPattern.set({
|
||||
prefix: (setting && setting.externalLinkPatternPrefix) || '',
|
||||
urlTemplate: (setting && setting.externalLinkPatternUrl) || '',
|
||||
});
|
||||
syncMarkdownExternalLinkPattern(setting);
|
||||
}
|
||||
let content = Blaze.toHTML(view.templateContentBlock);
|
||||
// Admin Panel / Features: when "render links as plain text" is enabled, every
|
||||
@@ -534,10 +321,7 @@ Meteor.startup(() => {
|
||||
Markdown.alwaysShowCodeAsText.set(!!(setting && setting.alwaysShowCodeAsText));
|
||||
}
|
||||
if (typeof Markdown !== 'undefined' && Markdown.externalLinkPattern) {
|
||||
Markdown.externalLinkPattern.set({
|
||||
prefix: (setting && setting.externalLinkPatternPrefix) || '',
|
||||
urlTemplate: (setting && setting.externalLinkPatternUrl) || '',
|
||||
});
|
||||
syncMarkdownExternalLinkPattern(setting);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1541,10 +1541,6 @@ body.mobile-mode #header-quick-access .allBoards {
|
||||
margin: 7px;
|
||||
padding: 0;
|
||||
}
|
||||
#headerIsSettingDatabaseCallDone {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── Small screens: home ICON only, never the "All Boards" label ─────────────
|
||||
This block is last in the file on purpose.
|
||||
|
||||
|
||||
@@ -52,19 +52,20 @@ template(name="header")
|
||||
= headerTitleText
|
||||
|
||||
// Logo - visible; on mobile constrained by CSS
|
||||
unless currentSetting.hideLogo
|
||||
if currentSetting.customTopLeftCornerLogoImageUrl
|
||||
if currentSetting.customTopLeftCornerLogoLinkUrl
|
||||
a.header-logo(href="{{currentSetting.customTopLeftCornerLogoLinkUrl}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
|
||||
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" alt="{{currentSetting.productName}}" height="{{#if currentSetting.customTopLeftCornerLogoHeight}}#{currentSetting.customTopLeftCornerLogoHeight}{{else}}27{{/if}}" width="auto" margin="0" padding="0")
|
||||
unless currentSetting.customTopLeftCornerLogoLinkUrl
|
||||
img.header-logo(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
|
||||
//- `header-logo` on all three branches - a custom logo that links, one
|
||||
that does not, and the stock one - so the bar can space the logo the
|
||||
same way whichever is drawn. docs/Features/Page/Header.md
|
||||
div#headerIsSettingDatabaseCallDone.header-logo
|
||||
img(src="{{pathFor '/logo-header.png'}}" alt="{{headerLogoAlt}}" title="{{headerLogoAlt}}")
|
||||
if logoSettingsReady
|
||||
unless currentSetting.hideLogo
|
||||
if currentSetting.customTopLeftCornerLogoImageUrl
|
||||
if currentSetting.customTopLeftCornerLogoLinkUrl
|
||||
a.header-logo(href="{{currentSetting.customTopLeftCornerLogoLinkUrl}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
|
||||
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" alt="{{currentSetting.productName}}" height="{{#if currentSetting.customTopLeftCornerLogoHeight}}#{currentSetting.customTopLeftCornerLogoHeight}{{else}}27{{/if}}" width="auto" margin="0" padding="0")
|
||||
unless currentSetting.customTopLeftCornerLogoLinkUrl
|
||||
img.header-logo(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
|
||||
//- `header-logo` on all three branches - a custom logo that links, one
|
||||
that does not, and the stock one - so the bar can space the logo the
|
||||
same way whichever is drawn. docs/Features/Page/Header.md
|
||||
div.header-logo
|
||||
img(src="{{pathFor '/logo-header.png'}}" alt="{{headerLogoAlt}}" title="{{headerLogoAlt}}")
|
||||
|
||||
// Mobile/desktop mode toggle (swapped with zoom)
|
||||
// #6680 follow-up: .js-header-collapsible-icon marks every icon the new
|
||||
|
||||
@@ -66,6 +66,7 @@ Template.header.onCreated(function () {
|
||||
const templateInstance = this;
|
||||
templateInstance.currentSetting = new ReactiveVar();
|
||||
templateInstance.isLoading = new ReactiveVar(false);
|
||||
templateInstance.subscribe('setting');
|
||||
|
||||
// Publish the page's path so the browser tab can carry it too. Here rather
|
||||
// than in `Utils`, because this is the one place that already works it out
|
||||
@@ -74,30 +75,11 @@ Template.header.onCreated(function () {
|
||||
headerPathVar.set(headerFullPath());
|
||||
});
|
||||
|
||||
Meteor.subscribe('setting', {
|
||||
onReady() {
|
||||
templateInstance.currentSetting.set(ReactiveCache.getCurrentSetting());
|
||||
let currSetting = templateInstance.currentSetting.curValue;
|
||||
if (
|
||||
currSetting &&
|
||||
currSetting !== undefined &&
|
||||
currSetting.customLoginLogoImageUrl !== undefined &&
|
||||
document.getElementById('headerIsSettingDatabaseCallDone') != null
|
||||
)
|
||||
document.getElementById(
|
||||
'headerIsSettingDatabaseCallDone',
|
||||
).style.display = 'none';
|
||||
else if (
|
||||
document.getElementById('headerIsSettingDatabaseCallDone') != null
|
||||
)
|
||||
document.getElementById(
|
||||
'headerIsSettingDatabaseCallDone',
|
||||
).style.display = 'block';
|
||||
return this.stop();
|
||||
},
|
||||
});
|
||||
});
|
||||
Template.header.helpers({
|
||||
logoSettingsReady() {
|
||||
return Template.instance().subscriptionsReady();
|
||||
},
|
||||
// The page's title, beside the house icon. A board's own title wherever there
|
||||
// is a board; otherwise the page's, by route name. Two helpers rather than
|
||||
// one string, because a translated title has to go through {{_ }} and a board
|
||||
|
||||
@@ -19,10 +19,6 @@ table, tbody, td, tfoot, th, thread, tr, tt, ul, var {
|
||||
font-size: 100%;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
.note-popover .popover-content .note-color-palette div .note-color-btn,
|
||||
.panel-heading.note-toolbar .note-color-palette div .note-color-btn {
|
||||
background: none;
|
||||
}
|
||||
a:focus {
|
||||
outline: unset;
|
||||
outline-offset: unset;
|
||||
@@ -577,7 +573,24 @@ kbd {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.textcomplete-dropdown {
|
||||
z-index: 2000 !important;
|
||||
/* The menu is attached to body, above both card windows (2001) and
|
||||
card-detail popups (100000). #6704 */
|
||||
z-index: 100001 !important;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.textcomplete-dropdown .textcomplete-item > span {
|
||||
display: block;
|
||||
padding: 4px 12px 4px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.textcomplete-dropdown .textcomplete-item.active > span {
|
||||
color: #fff;
|
||||
}
|
||||
.fa.fa-times-thin:before {
|
||||
content: '\00d7';
|
||||
|
||||
@@ -25,23 +25,24 @@ template(name="main")
|
||||
template(name="userFormsLayout")
|
||||
+recoveryMaintenance
|
||||
section.auth-layout
|
||||
if currentSetting.hideLogo
|
||||
h1.at-form-landing-logo
|
||||
br
|
||||
br
|
||||
unless currentSetting.hideLogo
|
||||
h1.at-form-landing-logo
|
||||
if currentSetting.customLoginLogoImageUrl
|
||||
if currentSetting.customLoginLogoLinkUrl
|
||||
a(href="{{currentSetting.customLoginLogoLinkUrl}}")
|
||||
if logoSettingsReady
|
||||
if currentSetting.hideLogo
|
||||
h1.at-form-landing-logo
|
||||
br
|
||||
br
|
||||
unless currentSetting.hideLogo
|
||||
h1.at-form-landing-logo
|
||||
if currentSetting.customLoginLogoImageUrl
|
||||
if currentSetting.customLoginLogoLinkUrl
|
||||
a(href="{{currentSetting.customLoginLogoLinkUrl}}")
|
||||
img(src="{{currentSetting.customLoginLogoImageUrl}}" alt="{{currentSetting.productName}}" width="300" height="auto")
|
||||
br
|
||||
unless currentSetting.customLoginLogoLinkUrl
|
||||
img(src="{{currentSetting.customLoginLogoImageUrl}}" alt="{{currentSetting.productName}}" width="300" height="auto")
|
||||
br
|
||||
else
|
||||
img(src="{{pathFor '/wekan-logo.svg'}}" alt="" width="300" height="auto")
|
||||
br
|
||||
unless currentSetting.customLoginLogoLinkUrl
|
||||
img(src="{{currentSetting.customLoginLogoImageUrl}}" alt="{{currentSetting.productName}}" width="300" height="auto")
|
||||
br
|
||||
else
|
||||
img(src="{{pathFor '/wekan-logo.svg'}}" alt="" width="300" height="auto")
|
||||
br
|
||||
if currentSetting.textBelowCustomLoginLogo
|
||||
hr
|
||||
section.textBelowCustomLoginLogo
|
||||
@@ -129,6 +130,7 @@ template(name="userFormsLayout")
|
||||
.auth-bottom-space
|
||||
|
||||
template(name="defaultLayout")
|
||||
+attachmentViewer
|
||||
+recoveryMaintenance
|
||||
//- Shared migration / board-repair progress dashboard. Mounted app-wide so it
|
||||
//- shows for both a board-open repair AND an Admin Panel database migration.
|
||||
|
||||
@@ -29,6 +29,11 @@ Template.userFormsLayout.onCreated(function () {
|
||||
const templateInstance = this;
|
||||
templateInstance.currentSetting = new ReactiveVar();
|
||||
templateInstance.isLoading = new ReactiveVar(false);
|
||||
templateInstance.subscribe('setting', {
|
||||
onReady() {
|
||||
templateInstance.currentSetting.set(ReactiveCache.getCurrentSetting());
|
||||
},
|
||||
});
|
||||
// The Meteor accounts-* OAuth providers the server reports enabled (keys
|
||||
// only) and whether e-mailed one-time-code login is on; both are filled by
|
||||
// the getAuthenticationsEnabled call in onRendered.
|
||||
@@ -64,12 +69,6 @@ Template.userFormsLayout.onCreated(function () {
|
||||
}
|
||||
});
|
||||
|
||||
Meteor.subscribe('setting', {
|
||||
onReady() {
|
||||
templateInstance.currentSetting.set(ReactiveCache.getCurrentSetting());
|
||||
return this.stop();
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -272,6 +271,9 @@ Template.userFormsLayout.onDestroyed(() => {
|
||||
});
|
||||
|
||||
Template.userFormsLayout.helpers({
|
||||
logoSettingsReady() {
|
||||
return Template.instance().subscriptionsReady();
|
||||
},
|
||||
// One entry per enabled Meteor accounts-* provider: key, icon class and the
|
||||
// translated provider name for {{_ 'sign-in-with' label}}. No credential
|
||||
// ever reaches this list - the server sends keys only.
|
||||
|
||||
@@ -22,7 +22,7 @@ template(name="myAttachments")
|
||||
= list.title
|
||||
each card in list.myAttachmentCards
|
||||
.my-attachments-card-wrapper
|
||||
a.my-attachments-card-title.js-my-attachment-card(href=card.originRelativeUrl)
|
||||
a.my-attachments-card-title.js-my-attachment-card(href=card.originRelativeUrl data-card-id=card._id data-board-id=card.boardId)
|
||||
i.fa.fa-external-link
|
||||
= card.title
|
||||
.attachment-gallery.my-attachments-gallery
|
||||
|
||||
@@ -205,11 +205,12 @@ Template.myAttachments.events({
|
||||
// would navigate away from this page and lose the reader's place in a
|
||||
// list that spans many boards.
|
||||
'click .js-my-attachment-card'(evt) {
|
||||
if (evt.button !== 0 || evt.ctrlKey || evt.metaKey || evt.shiftKey || evt.altKey) return;
|
||||
// `each card in ...` binds a lexical variable, not a Blaze data context.
|
||||
// Read the identifiers from the link even when its child was clicked (#6702).
|
||||
const { cardId, boardId } = evt.currentTarget.dataset;
|
||||
if (!cardId || !boardId) return;
|
||||
evt.preventDefault();
|
||||
const card = Blaze.getData(evt.currentTarget);
|
||||
if (!card || !card._id) return;
|
||||
const cardId = card._id;
|
||||
const boardId = card.boardId;
|
||||
Meteor.subscribe('popupCardData', cardId, {
|
||||
onReady() {
|
||||
Session.set('popupCardId', cardId);
|
||||
|
||||
@@ -41,7 +41,7 @@ template(name="myCards")
|
||||
= list.title
|
||||
each card in list.myCards
|
||||
.my-cards-card-wrapper
|
||||
a.minicard-wrapper.js-minicard(href=card.originRelativeUrl)
|
||||
a.minicard-wrapper.js-minicard(href=card.originRelativeUrl data-card-id=card._id data-board-id=card.boardId)
|
||||
+minicard(card)
|
||||
if $eq myCardsView 'table'
|
||||
.wrapper
|
||||
@@ -49,14 +49,14 @@ template(name="myCards")
|
||||
caption.sr-only {{_ 'my-cards'}}
|
||||
thead
|
||||
tr
|
||||
th(scope="col") {{_ "Card"}}
|
||||
th(scope="col") {{_ "List"}}
|
||||
th(scope="col") {{_ "Board"}}
|
||||
th(scope="col") {{_ "Swimlane"}}
|
||||
th(scope="col") {{_ "card"}}
|
||||
th(scope="col") {{_ "list"}}
|
||||
th(scope="col") {{_ "board"}}
|
||||
th(scope="col") {{_ "swimlane"}}
|
||||
unless isMiniScreen
|
||||
th(scope="col") {{_ "Members"}}
|
||||
th(scope="col") {{_ "Labels"}}
|
||||
th(scope="col") {{_ "Due Date"}}
|
||||
th(scope="col") {{_ "members"}}
|
||||
th(scope="col") {{_ "labels"}}
|
||||
th(scope="col") {{_ "due-date"}}
|
||||
tbody
|
||||
each board in myCardsList
|
||||
each swimlane in board.mySwimlanes
|
||||
|
||||
@@ -184,11 +184,12 @@ Template.myCards.events({
|
||||
// tableView.js (Board Table view) already use: subscribe the popup-only
|
||||
// publication, then open the shared `cardDetails` popup template.
|
||||
'click .js-minicard'(evt) {
|
||||
if (evt.button !== 0 || evt.ctrlKey || evt.metaKey || evt.shiftKey || evt.altKey) return;
|
||||
// `each card in ...` binds a lexical variable, not a Blaze data context.
|
||||
// Read the identifiers from the link even when its child was clicked (#6702).
|
||||
const { cardId, boardId } = evt.currentTarget.dataset;
|
||||
if (!cardId || !boardId) return;
|
||||
evt.preventDefault();
|
||||
const card = Blaze.getData(evt.currentTarget);
|
||||
if (!card || !card._id) return;
|
||||
const cardId = card._id;
|
||||
const boardId = card.boardId;
|
||||
Meteor.subscribe('popupCardData', cardId, {
|
||||
onReady() {
|
||||
Session.set('popupCardId', cardId);
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
first row of a popup, and it must match the space beside it: `.content` has 18px of
|
||||
side padding, so 6 + 12 = 18 makes the top gap and the left gutter the same. */
|
||||
.pop-over .header {
|
||||
height: 41px;
|
||||
height: calc(41px * var(--wekan-ui-font-scale, 1));
|
||||
position: relative;
|
||||
margin-bottom: 6px;
|
||||
background: #f7f7f7;
|
||||
@@ -141,7 +141,7 @@
|
||||
.pop-over[data-popup='memberMenuPopup'] > .content-wrapper {
|
||||
/* The header is out of the flow here, so its height is padding instead: 41px of
|
||||
header + its 1px bottom border + the same 6px gap every other popup has. */
|
||||
padding-top: calc(41px + 1px + 6px);
|
||||
padding-top: calc(41px * var(--wekan-ui-font-scale, 1) + 1px + 6px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -883,7 +883,8 @@ html[dir='rtl'] .pop-over:has(.edit-date) > .js-date-popup-resize {
|
||||
width: 50px;
|
||||
}
|
||||
.pop-over.miniprofile .miniprofile-header .info {
|
||||
margin: 0 0 0 64px;
|
||||
margin: 0;
|
||||
margin-inline-start: 64px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.pop-over.miniprofile .miniprofile-header .info h3 a {
|
||||
@@ -926,7 +927,7 @@ html[dir='rtl'] .pop-over:has(.edit-date) > .js-date-popup-resize {
|
||||
force. The fallback is the stock WeKan blue, so an unthemed instance is
|
||||
unchanged. */
|
||||
background: var(--theme-accent, #2980b9);
|
||||
height: 48px;
|
||||
height: calc(48px * var(--wekan-ui-font-scale, 1));
|
||||
padding: 0px 0px;
|
||||
border: 0px;
|
||||
margin: 0px 0px;
|
||||
@@ -955,10 +956,10 @@ html[dir='rtl'] .pop-over:has(.edit-date) > .js-date-popup-resize {
|
||||
}
|
||||
.pop-over .content-wrapper {
|
||||
width: 100%;
|
||||
height: calc(100% - 48px);
|
||||
height: calc(100% - 48px * var(--wekan-ui-font-scale, 1));
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
margin: 48px 0px 0px 0px;
|
||||
margin: calc(48px * var(--wekan-ui-font-scale, 1)) 0 0;
|
||||
}
|
||||
.pop-over .content-container {
|
||||
width: 100%;
|
||||
|
||||
@@ -9,8 +9,8 @@ template(name="boardActions")
|
||||
option(value="bottom") {{_'r-bottom-of'}}
|
||||
div.trigger-text
|
||||
| {{_'r-its-list'}}
|
||||
div.trigger-button.js-add-gen-move-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-gen-move-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -38,8 +38,8 @@ template(name="boardActions")
|
||||
| {{_'r-in-swimlane'}}
|
||||
div.trigger-dropdown
|
||||
input(id="swimlaneName",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-button.js-add-spec-move-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-spec-move-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -49,8 +49,8 @@ template(name="boardActions")
|
||||
option(value="unarchive") {{_'r-unarchive'}}
|
||||
div.trigger-text
|
||||
| {{_'r-card'}}
|
||||
div.trigger-button.js-add-arch-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-arch-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -58,8 +58,8 @@ template(name="boardActions")
|
||||
| {{_'r-add-swimlane'}}
|
||||
div.trigger-dropdown
|
||||
input(id="swimlane-name",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-button.js-add-swimlane-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-swimlane-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -75,8 +75,8 @@ template(name="boardActions")
|
||||
| {{_'r-in-swimlane'}}
|
||||
div.trigger-dropdown
|
||||
input(id="swimlane-name2",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-button.js-create-card-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-create-card-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -100,8 +100,8 @@ template(name="boardActions")
|
||||
| {{_'r-in-swimlane'}}
|
||||
div.trigger-dropdown
|
||||
input(id="swimlaneName-link",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-button.js-link-card-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-link-card-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -117,8 +117,8 @@ template(name="boardActions")
|
||||
option(value="name") {{_'r-sort-name'}}
|
||||
option(value="created") {{_'createdAt'}}
|
||||
option(value="modified") {{_'modifiedAt'}}
|
||||
div.trigger-button.js-sort-list-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-sort-list-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -130,8 +130,8 @@ template(name="boardActions")
|
||||
| {{_'r-in-list'}}
|
||||
div.trigger-dropdown
|
||||
input(id="moveall-to-list",type=text,placeholder="{{_'r-list-name'}}")
|
||||
div.trigger-button.js-move-all-cards-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-move-all-cards-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ Template.boardActions.helpers({
|
||||
|
||||
loadingBoardsLabel() {
|
||||
try {
|
||||
const txt = TAPi18n.__('loading-boards');
|
||||
const txt = TAPi18n.__('loading');
|
||||
if (txt && !txt.startsWith("key '")) return txt;
|
||||
} catch (e) {
|
||||
// ignore translation lookup errors
|
||||
|
||||
@@ -15,8 +15,8 @@ template(name="cardActions")
|
||||
option(value="receivedAt") {{_'r-df-received-at'}}
|
||||
div.trigger-text
|
||||
| {{_'r-to-current-datetime'}}
|
||||
div.trigger-button.js-set-date-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-set-date-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -29,8 +29,8 @@ template(name="cardActions")
|
||||
option(value="dueAt") {{_'r-df-due-at'}}
|
||||
option(value="endAt") {{_'r-df-end-at'}}
|
||||
option(value="receivedAt") {{_'r-df-received-at'}}
|
||||
div.trigger-button.js-remove-datevalue-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-remove-datevalue-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -45,8 +45,8 @@ template(name="cardActions")
|
||||
each labels
|
||||
option(value="#{_id}")
|
||||
= name
|
||||
div.trigger-button.js-add-label-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-label-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -58,29 +58,29 @@ template(name="cardActions")
|
||||
| {{_'r-member'}}
|
||||
div.trigger-dropdown
|
||||
input(id="member-name",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-button.js-add-member-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-member-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{_'r-add-actinguser-member'}}
|
||||
div.trigger-button.js-add-actinguser-member-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-actinguser-member-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{_'r-remove-all'}}
|
||||
div.trigger-button.js-add-removeall-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-removeall-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{_'r-remove-all-labels'}}
|
||||
div.trigger-button.js-add-removealllabels-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-removealllabels-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -90,8 +90,8 @@ template(name="cardActions")
|
||||
id="color-action"
|
||||
class="card-details-{{cardColorButton}}")
|
||||
| {{_ cardColorButtonText }}
|
||||
div.trigger-button.js-set-color-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-set-color-action.js-goto-rules(type="button" aria-label="{{_ 'select-color'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -99,8 +99,8 @@ template(name="cardActions")
|
||||
select(id="complete-action")
|
||||
option(value="markCardComplete") {{_'r-mark-complete'}}
|
||||
option(value="markCardIncomplete") {{_'r-mark-incomplete'}}
|
||||
div.trigger-button.js-set-complete-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-set-complete-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -123,8 +123,8 @@ template(name="cardActions")
|
||||
option(value="months") {{_'r-unit-months'}}
|
||||
div.trigger-text
|
||||
| {{_'r-later'}}
|
||||
div.trigger-button.js-set-reldate-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-set-reldate-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
template(name="setCardActionsColorPopup")
|
||||
form.edit-label
|
||||
|
||||
@@ -9,8 +9,8 @@ template(name="checklistActions")
|
||||
| {{_'r-checklist'}}
|
||||
div.trigger-dropdown
|
||||
input(id="checklist-name",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-button.js-add-checklist-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-checklist-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -22,8 +22,8 @@ template(name="checklistActions")
|
||||
| {{_'r-items-check'}}
|
||||
div.trigger-dropdown
|
||||
input(id="checklist-name2",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-button.js-add-checkall-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-checkall-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
|
||||
div.trigger-item
|
||||
@@ -40,8 +40,8 @@ template(name="checklistActions")
|
||||
| {{_'r-of-checklist'}}
|
||||
div.trigger-dropdown
|
||||
input(id="checklist-name3",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-button.js-add-check-item-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-check-item-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -53,8 +53,8 @@ template(name="checklistActions")
|
||||
| {{_'r-with-items'}}
|
||||
div.trigger-dropdown
|
||||
input(id="checklist-items",type=text,placeholder="{{_'r-items-list'}}")
|
||||
div.trigger-button.js-add-checklist-items-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-checklist-items-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
|
||||
@@ -76,8 +76,8 @@ Template.checklistActions.events({
|
||||
const data = Template.currentData();
|
||||
const ruleName = data.ruleName.get();
|
||||
const trigger = data.triggerVar.get();
|
||||
const checkItemName = tpl.find('#checkitem-name');
|
||||
const checklistName = tpl.find('#checklist-name3');
|
||||
const checkItemName = tpl.find('#checkitem-name').value;
|
||||
const checklistName = tpl.find('#checklist-name3').value;
|
||||
const actionSelected = tpl.find('#check-item-action').value;
|
||||
const boardId = Session.get('currentBoard');
|
||||
const desc = Utils.getTriggerActionDesc(event, tpl);
|
||||
|
||||
@@ -9,5 +9,5 @@ template(name="mailActions")
|
||||
textarea(id="email-msg")
|
||||
div.trigger-hint.trigger-hint-email-vars
|
||||
| {{_'r-email-vars-hint'}}
|
||||
div.trigger-button.trigger-button-email.js-mail-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.trigger-button-email.js-mail-action.js-goto-rules(type="button" aria-label="{{_ 'r-add-action'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
@@ -17,15 +17,15 @@ template(name="boardTriggers")
|
||||
| {{_'r-in-swimlane'}}
|
||||
div.trigger-dropdown
|
||||
input(id="create-swimlane-name",type=text,placeholder="{{_'r-swimlane-name'}}")
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-create-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-create-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item#trigger-three
|
||||
div.trigger-content
|
||||
@@ -35,15 +35,15 @@ template(name="boardTriggers")
|
||||
i.fa.fa-search
|
||||
div.trigger-text
|
||||
| {{_'r-is-moved'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-gen-moved-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-gen-moved-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item#trigger-four
|
||||
div.trigger-content
|
||||
@@ -65,15 +65,15 @@ template(name="boardTriggers")
|
||||
| {{_'r-in-swimlane'}}
|
||||
div.trigger-dropdown
|
||||
input(id="create-swimlane-name-2",type=text,placeholder="{{_'r-swimlane-name'}}")
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-moved-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-moved-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item#trigger-five
|
||||
div.trigger-content
|
||||
@@ -87,15 +87,15 @@ template(name="boardTriggers")
|
||||
select(id="arch-action")
|
||||
option(value="archived") {{_'r-archived'}}
|
||||
option(value="unarchived") {{_'r-unarchived'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-arch-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-arch-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
|
||||
@@ -9,5 +9,5 @@ template(name="buttonTriggers")
|
||||
select#button-type.js-button-type
|
||||
option(value="card" selected="selected") {{_'r-card-button'}}
|
||||
option(value="board") {{_'r-board-button'}}
|
||||
div.trigger-button.js-add-button-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-button-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
@@ -9,15 +9,15 @@ template(name="cardTriggers")
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-gen-label-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-gen-label-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -40,15 +40,15 @@ template(name="cardTriggers")
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-spec-label-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-spec-label-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -60,15 +60,15 @@ template(name="cardTriggers")
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-gen-member-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-gen-member-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
|
||||
div.trigger-item
|
||||
@@ -89,15 +89,15 @@ template(name="cardTriggers")
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-spec-member-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-spec-member-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -109,15 +109,15 @@ template(name="cardTriggers")
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-gen-assignee-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-gen-assignee-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
|
||||
div.trigger-item
|
||||
@@ -138,15 +138,15 @@ template(name="cardTriggers")
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-spec-assignee-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-spec-assignee-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -160,15 +160,15 @@ template(name="cardTriggers")
|
||||
option(value="removed") {{_'r-attachment-removed-from'}}
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-attachment-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-attachment-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item(title="{{_'advanced-filter-description'}}")
|
||||
div.trigger-content
|
||||
@@ -176,8 +176,8 @@ template(name="cardTriggers")
|
||||
| {{_'r-when-a-card-matches-advanced-filter'}}
|
||||
div.trigger-dropdown
|
||||
input(class="advanced-filter-trigger-value",type=text,placeholder="{{_'advanced-filter-label'}}")
|
||||
div.trigger-button.js-add-advanced-filter-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-advanced-filter-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item(title="{{_'text-contains-trigger-description'}}")
|
||||
div.trigger-content
|
||||
@@ -185,61 +185,61 @@ template(name="cardTriggers")
|
||||
| {{_'r-when-a-card-title-or-description-contains'}}
|
||||
div.trigger-dropdown
|
||||
input(class="text-contains-trigger-value",type=text,placeholder="{{_'text-contains-trigger-label'}}")
|
||||
div.trigger-button.js-add-text-contains-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-text-contains-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-due-date-changed'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-due-date-changed-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-due-date-changed-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-start-date-changed'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-start-date-changed-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-start-date-changed-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-end-date-changed'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-end-date-changed-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-end-date-changed-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-received-date-changed'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-received-date-changed-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-received-date-changed-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
@@ -9,15 +9,15 @@ template(name="checklistTriggers")
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-gen-check-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-gen-check-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
|
||||
div.trigger-item
|
||||
@@ -38,15 +38,15 @@ template(name="checklistTriggers")
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-spec-check-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-spec-check-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -56,15 +56,15 @@ template(name="checklistTriggers")
|
||||
select(id="gen-comp-check-action")
|
||||
option(value="completed") {{_'r-completed'}}
|
||||
option(value="uncompleted") {{_'r-made-incomplete'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-gen-comp-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-gen-comp-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -82,15 +82,15 @@ template(name="checklistTriggers")
|
||||
select(id="spec-comp-check-action")
|
||||
option(value="completed") {{_'r-completed'}}
|
||||
option(value="uncompleted") {{_'r-made-incomplete'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-spec-comp-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-spec-comp-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -100,15 +100,15 @@ template(name="checklistTriggers")
|
||||
select(id="check-item-gen-action")
|
||||
option(value="checked") {{_'r-checked'}}
|
||||
option(value="unchecked") {{_'r-unchecked'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-gen-check-item-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-gen-check-item-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
@@ -126,12 +126,12 @@ template(name="checklistTriggers")
|
||||
select(id="check-item-spec-action")
|
||||
option(value="checked") {{_'r-checked'}}
|
||||
option(value="unchecked") {{_'r-unchecked'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
button.trigger-button.trigger-button-person.js-show-user-field(type="button" aria-label="{{_ 'username'}}")
|
||||
i.fa.fa-user(aria-hidden="true")
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-spec-check-item-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-spec-check-item-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
@@ -39,8 +39,8 @@ template(name="scheduledTriggers")
|
||||
| {{_'r-of-cards-in-list'}}
|
||||
div.trigger-dropdown
|
||||
input#schedule-list-name(type=text placeholder="{{_'r-list-name'}}")
|
||||
div.trigger-button.js-add-scheduled-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-scheduled-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item#trigger-due
|
||||
div.trigger-content
|
||||
@@ -59,8 +59,8 @@ template(name="scheduledTriggers")
|
||||
| {{_'r-schedule-at-time'}}
|
||||
div.trigger-dropdown
|
||||
+calendarTimeInput(inputId="due-time" value="09:00")
|
||||
div.trigger-button.js-add-due-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-due-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
|
||||
div.trigger-item#trigger-aging
|
||||
div.trigger-content
|
||||
@@ -76,5 +76,5 @@ template(name="scheduledTriggers")
|
||||
| {{_'r-schedule-at-time'}}
|
||||
div.trigger-dropdown
|
||||
+calendarTimeInput(inputId="aging-time" value="09:00")
|
||||
div.trigger-button.js-add-aging-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
button.trigger-button.js-add-aging-trigger.js-goto-action(type="button" aria-label="{{_ 'r-add-trigger'}}")
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
@@ -1,5 +1,4 @@
|
||||
template(name="adminProblems")
|
||||
+attachmentViewer
|
||||
.setting-content.admin-reports-content
|
||||
unless currentUser.isAdmin
|
||||
| {{_ 'error-notAuthorized'}}
|
||||
|
||||
@@ -19,19 +19,19 @@ template(name="notificationSettingsPopup")
|
||||
| {{_ labelKey}}
|
||||
.notification-settings-popup-options
|
||||
if isAdminScope
|
||||
a.flex.js-notify-option(data-service="{{service}}" data-value="true" class="{{#if onSelected}}is-checked{{/if}}")
|
||||
.materialCheckBox
|
||||
a.flex.js-notify-option(data-service="{{service}}" data-value="true")
|
||||
.materialCheckBox(class="{{#if onSelected}}is-checked{{/if}}")
|
||||
span {{_ 'yes'}}
|
||||
a.flex.js-notify-option(data-service="{{service}}" data-value="false" class="{{#if offSelected}}is-checked{{/if}}")
|
||||
.materialCheckBox
|
||||
a.flex.js-notify-option(data-service="{{service}}" data-value="false")
|
||||
.materialCheckBox(class="{{#if offSelected}}is-checked{{/if}}")
|
||||
span {{_ 'no'}}
|
||||
else
|
||||
a.flex.js-notify-option(data-service="{{service}}" data-value="" class="{{#if inheritSelected}}is-checked{{/if}}")
|
||||
.materialCheckBox
|
||||
a.flex.js-notify-option(data-service="{{service}}" data-value="")
|
||||
.materialCheckBox(class="{{#if inheritSelected}}is-checked{{/if}}")
|
||||
span {{_ 'default'}}
|
||||
a.flex.js-notify-option(data-service="{{service}}" data-value="true" class="{{#if onSelected}}is-checked{{/if}}")
|
||||
.materialCheckBox
|
||||
a.flex.js-notify-option(data-service="{{service}}" data-value="true")
|
||||
.materialCheckBox(class="{{#if onSelected}}is-checked{{/if}}")
|
||||
span {{_ 'yes'}}
|
||||
a.flex.js-notify-option(data-service="{{service}}" data-value="false" class="{{#if offSelected}}is-checked{{/if}}")
|
||||
.materialCheckBox
|
||||
a.flex.js-notify-option(data-service="{{service}}" data-value="false")
|
||||
.materialCheckBox(class="{{#if offSelected}}is-checked{{/if}}")
|
||||
span {{_ 'no'}}
|
||||
@@ -35,10 +35,10 @@ function currentValue(scope, service) {
|
||||
|
||||
Template.notificationSettingsPopup.helpers({
|
||||
isAdminScope() {
|
||||
return Template.currentData().scope === 'admin';
|
||||
return Template.instance().data.scope === 'admin';
|
||||
},
|
||||
notifyServiceRows() {
|
||||
const scope = Template.currentData().scope;
|
||||
const scope = Template.instance().data.scope;
|
||||
return Object.keys(NOTIFICATION_SERVICES).map(service => {
|
||||
const value = currentValue(scope, service);
|
||||
return {
|
||||
@@ -60,7 +60,7 @@ Template.notificationSettingsPopup.events({
|
||||
const service = target.dataset.service;
|
||||
const rawValue = target.dataset.value;
|
||||
const value = rawValue === 'true' ? true : rawValue === 'false' ? false : null;
|
||||
const scope = Template.currentData().scope;
|
||||
const scope = instance.data.scope;
|
||||
|
||||
if (scope === 'admin') {
|
||||
// Inherit is not offered at admin scope - it IS the base default - so
|
||||
|
||||
@@ -687,7 +687,7 @@ template(name="settingsOrgPopup")
|
||||
//
|
||||
i.fa.fa-user
|
||||
//
|
||||
| {{_ 'impersonate-org'}}
|
||||
| {{_ 'impersonate-user'}}
|
||||
//
|
||||
//
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { accountOperationErrorKey } from '/client/lib/accountOperationError';
|
||||
import { formatDateForDisplay } from '/client/lib/dateDisplay';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
@@ -2587,16 +2588,7 @@ Template.settingsUserPopup.events({
|
||||
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);
|
||||
}
|
||||
alert(TAPi18n.__(accountOperationErrorKey(error)));
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('User deleted successfully:', result);
|
||||
@@ -2619,13 +2611,7 @@ Template.settingsUserPopup.events({
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.error('Error anonymizing user:', error);
|
||||
}
|
||||
if (error.error === 'not-authorized') {
|
||||
alert('You are not authorized to anonymize this user.');
|
||||
} else if (error.error === 'user-not-found') {
|
||||
alert('User not found.');
|
||||
} else {
|
||||
alert('Error anonymizing user: ' + error.reason);
|
||||
}
|
||||
alert(TAPi18n.__(accountOperationErrorKey(error)));
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('User anonymized successfully:', result);
|
||||
|
||||
@@ -360,6 +360,17 @@ template(name='tableVisibilityModeSettings')
|
||||
a.flex.js-toggle-all-boards-hide
|
||||
.materialCheckBox#hide-board-member-list(class="{{#if currentSetting.hideBoardMemberList}}is-checked{{/if}}")
|
||||
span {{_ 'board-member-list'}}
|
||||
li.tableVisibilityMode-form
|
||||
a.flex.js-toggle-all-boards-hide
|
||||
.materialCheckBox#hide-date-format(class="{{#if currentSetting.hideDateFormat}}is-checked{{/if}}")
|
||||
span {{_ 'date-format-for-everyone'}}:
|
||||
select.wekan-form-control#global-date-format(aria-label="{{_ 'date-format-for-everyone'}}")
|
||||
option(value="YYYY-MM-DD" selected="{{#if isGlobalDateFormat 'YYYY-MM-DD'}}selected{{/if}}") {{_ 'date-format-yyyy-mm-dd'}} · {{_ 'time'}}
|
||||
option(value="DD-MM-YYYY" selected="{{#if isGlobalDateFormat 'DD-MM-YYYY'}}selected{{/if}}") {{_ 'date-format-dd-mm-yyyy'}} · {{_ 'time'}}
|
||||
option(value="MM-DD-YYYY" selected="{{#if isGlobalDateFormat 'MM-DD-YYYY'}}selected{{/if}}") {{_ 'date-format-mm-dd-yyyy'}} · {{_ 'time'}}
|
||||
option(value="YYYY-MM-DD-date-only" selected="{{#if isGlobalDateFormat 'YYYY-MM-DD-date-only'}}selected{{/if}}") {{_ 'date'}}: {{_ 'date-format-yyyy-mm-dd'}}
|
||||
option(value="DD-MM-YYYY-date-only" selected="{{#if isGlobalDateFormat 'DD-MM-YYYY-date-only'}}selected{{/if}}") {{_ 'date'}}: {{_ 'date-format-dd-mm-yyyy'}}
|
||||
option(value="MM-DD-YYYY-date-only" selected="{{#if isGlobalDateFormat 'MM-DD-YYYY-date-only'}}selected{{/if}}") {{_ 'date'}}: {{_ 'date-format-mm-dd-yyyy'}}
|
||||
li.tableVisibilityMode-form
|
||||
.title {{_ 'wait-spinner'}}
|
||||
+selectSpinnerName(spinnerName=currentSetting.spinnerName)
|
||||
@@ -487,6 +498,7 @@ template(name='tableVisibilityModeSettings')
|
||||
.title {{_ 'custom-login-logo-image-url'}}
|
||||
.form-group
|
||||
input.wekan-form-control#custom-login-logo-image-url(type="text", placeholder="" value="{{currentSetting.customLoginLogoImageUrl}}")
|
||||
input.js-site-logo-upload(type="file" data-kind="login" accept="image/png,image/jpeg,image/gif,image/webp" aria-label="{{_ 'upload'}} {{_ 'custom-login-logo-image-url'}}")
|
||||
li.tableVisibilityMode-form
|
||||
.title {{_ 'custom-login-logo-link-url'}}
|
||||
.form-group
|
||||
@@ -499,6 +511,9 @@ template(name='tableVisibilityModeSettings')
|
||||
.title {{_ 'custom-top-left-corner-logo-image-url'}}
|
||||
.form-group
|
||||
input.wekan-form-control#custom-top-left-corner-logo-image-url(type="text", placeholder="" value="{{currentSetting.customTopLeftCornerLogoImageUrl}}")
|
||||
input.js-site-logo-upload(type="file" data-kind="header" accept="image/png,image/jpeg,image/gif,image/webp" aria-label="{{_ 'upload'}} {{_ 'custom-top-left-corner-logo-image-url'}}")
|
||||
li.tableVisibilityMode-form
|
||||
span.js-site-logo-upload-status(role="status" aria-live="polite")
|
||||
li.tableVisibilityMode-form
|
||||
.title {{_ 'custom-top-left-corner-logo-link-url'}}
|
||||
.form-group
|
||||
|
||||
@@ -901,6 +901,9 @@ Template.setting.events({
|
||||
});
|
||||
|
||||
Template.tableVisibilityModeSettings.helpers({
|
||||
isGlobalDateFormat(format) {
|
||||
return (ReactiveCache.getCurrentSetting()?.globalDateFormat || 'YYYY-MM-DD') === format;
|
||||
},
|
||||
// Multitenancy option D: every group of this pane except "Change color" writes
|
||||
// the INSTANCE settings, so only the site admin is shown them.
|
||||
isSiteAdmin() {
|
||||
@@ -947,6 +950,33 @@ function saveVisibilitySettings($set) {
|
||||
}
|
||||
|
||||
Template.tableVisibilityModeSettings.events({
|
||||
async 'change input.js-site-logo-upload'(event, tpl) {
|
||||
const input = event.currentTarget;
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
const status = tpl.find('.js-site-logo-upload-status');
|
||||
const kind = input.dataset.kind;
|
||||
input.value = '';
|
||||
if (file.size > 2 * 1024 * 1024 || !['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(file.type)) {
|
||||
if (status) status.textContent = 'Upload a PNG, JPEG, GIF or WebP image smaller than 2 MB';
|
||||
return;
|
||||
}
|
||||
if (status) status.textContent = TAPi18n.__('upload') + '…';
|
||||
try {
|
||||
const base64 = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.onload = () => resolve(String(reader.result).split(',')[1]);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
const url = await Meteor.callAsync('uploadSiteLogo', kind, base64);
|
||||
const field = tpl.find(kind === 'login' ? '#custom-login-logo-image-url' : '#custom-top-left-corner-logo-image-url');
|
||||
if (field) field.value = url;
|
||||
if (status) status.textContent = TAPi18n.__('save');
|
||||
} catch (error) {
|
||||
if (status) status.textContent = error.reason || error.message || 'Upload failed';
|
||||
}
|
||||
},
|
||||
// ── All Boards ────────────────────────────────────────────────────────────
|
||||
// Boards visibility, board activities, the two All Boards lists and the spinner.
|
||||
//
|
||||
@@ -982,11 +1012,15 @@ Template.tableVisibilityModeSettings.events({
|
||||
['#hide-board-activities', 'hideBoardActivitiesOnAllBoards'],
|
||||
['#hide-card-counter-list', 'hideCardCounterList'],
|
||||
['#hide-board-member-list', 'hideBoardMemberList'],
|
||||
['#hide-date-format', 'hideDateFormat'],
|
||||
]) {
|
||||
if ($(selector).length) {
|
||||
$set[key] = $(selector).hasClass('is-checked');
|
||||
}
|
||||
}
|
||||
if ($('#global-date-format').length) {
|
||||
$set.globalDateFormat = visibilityText('#global-date-format');
|
||||
}
|
||||
if ($('#spinnerName').length) {
|
||||
$set.spinnerName = visibilityText('#spinnerName');
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ template(name="newTranslationPopup")
|
||||
label
|
||||
| {{_ 'text'}}
|
||||
span.error.hide.text-taken
|
||||
| {{_ 'error-text-taken'}}
|
||||
| {{_ 'editTranslationPopup-title'}}
|
||||
input.js-translation-text(type="text" value="" required)
|
||||
label
|
||||
| {{_ 'translation-text'}}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { buildHeader, pageInfo, TABLE_PAGE_ROWS_PER_PAGE } from '/models/lib/tablePage';
|
||||
|
||||
@@ -52,7 +53,7 @@ Template.translationSettings.onCreated(function () {
|
||||
if (value === '') {
|
||||
this.findTranslationsOptions.set({});
|
||||
} else {
|
||||
const regex = new RegExp(value, 'i');
|
||||
const regex = new RegExp(value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
||||
this.findTranslationsOptions.set({
|
||||
$or: [
|
||||
{ language: regex },
|
||||
@@ -210,7 +211,9 @@ Template.newTranslationPopup.events({
|
||||
if (error) {
|
||||
const errorElement = error.error;
|
||||
if (errorElement === 'text-already-taken') {
|
||||
textMessageElement.show();
|
||||
textMessageElement.text(`${TAPi18n.__('editTranslationPopup-title')}: ${text} (${language})`).show();
|
||||
} else {
|
||||
textMessageElement.text(TAPi18n.__('server-error')).show();
|
||||
}
|
||||
} else {
|
||||
textMessageElement.hide();
|
||||
@@ -218,7 +221,6 @@ Template.newTranslationPopup.events({
|
||||
}
|
||||
},
|
||||
);
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
|
||||
.card-field-order-columns {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
column-gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
@@ -179,7 +179,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 0;
|
||||
padding: calc(4px * min(1, var(--wekan-ui-font-scale, 1))) 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
|
||||
.card-field-order-move.is-disabled {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.card-field-order-label {
|
||||
@@ -765,4 +765,30 @@ body.sidebar-resizing-active {
|
||||
.pop-over-list-subheading + .pop-over-list {
|
||||
margin-top: 0;
|
||||
}
|
||||
/* #6699: this editor uses real, labelled checkboxes. Undo all three parts
|
||||
of forms.css's material-checkbox hiding rule in this popup only. */
|
||||
.wip-limit-groups input[type="checkbox"] {
|
||||
display: inline-block;
|
||||
visibility: visible;
|
||||
position: static;
|
||||
inset-inline-start: auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 16px;
|
||||
margin-inline-end: 6px;
|
||||
appearance: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wip-limit-groups label.flex {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
/* Stack the two settings columns without changing single-side desktop headings. */
|
||||
@media screen and (max-width: 800px) {
|
||||
.card-field-order-columns { grid-template-columns: minmax(0, 1fr); }
|
||||
}
|
||||
.card-field-order-column { min-width: 0; }
|
||||
.card-field-order-label { overflow-wrap: anywhere; }
|
||||
@@ -217,7 +217,7 @@ template(name="boardChangeBackgroundImagePopup")
|
||||
Save it belongs beside.
|
||||
.buttonsContainer.board-background-image-buttons
|
||||
input.primary(type="submit" value="{{_ 'save'}}")
|
||||
button.js-remove-background-image.negate.card-details-red(type="button") {{_ 'unset-color'}}
|
||||
button.js-remove-background-image.negate.card-details-red(type="button") {{_ 'remove-background-image'}}
|
||||
hr
|
||||
//- ...and the same question answered from this machine instead of from a
|
||||
URL, with what has been uploaded to this board under it - so an upload is
|
||||
@@ -300,9 +300,9 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.card-field-order-toggle(class=row.toggle class="{{#if row.checked}}is-checked{{/if}}" title="{{row.title}}")
|
||||
i.fa(class="{{#if row.checked}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
if canModifyBoard
|
||||
a.flex.card-field-order-move.js-card-field-order-up(href="#" role="button" class="{{#unless row.canMoveUp}}is-disabled{{/unless}}" title="{{_ 'card-field-order-move-up'}}" aria-label="{{_ 'card-field-order-move-up'}}")
|
||||
a.flex.card-field-order-move.js-card-field-order-up(href="#" role="button" class="{{#unless row.canMoveUp}}is-disabled{{/unless}}" aria-disabled="{{#if row.canMoveUp}}false{{else}}true{{/if}}" title="{{#if row.canMoveUp}}{{_ 'card-field-order-move-up'}}{{else}}{{_ 'r-rule-disabled'}}{{/if}}" aria-label="{{_ 'card-field-order-move-up'}}")
|
||||
i.fa.fa-arrow-up
|
||||
a.flex.card-field-order-move.js-card-field-order-down(href="#" role="button" class="{{#unless row.canMoveDown}}is-disabled{{/unless}}" title="{{_ 'card-field-order-move-down'}}" aria-label="{{_ 'card-field-order-move-down'}}")
|
||||
a.flex.card-field-order-move.js-card-field-order-down(href="#" role="button" class="{{#unless row.canMoveDown}}is-disabled{{/unless}}" aria-disabled="{{#if row.canMoveDown}}false{{else}}true{{/if}}" title="{{#if row.canMoveDown}}{{_ 'card-field-order-move-down'}}{{else}}{{_ 'r-rule-disabled'}}{{/if}}" aria-label="{{_ 'card-field-order-move-down'}}")
|
||||
i.fa.fa-arrow-down
|
||||
span.card-field-order-label
|
||||
each icon in row.icons
|
||||
@@ -326,9 +326,9 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.card-field-order-toggle(class=row.toggle class="{{#if row.checked}}is-checked{{/if}}" title="{{row.title}}")
|
||||
i.fa(class="{{#if row.checked}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
if canModifyBoard
|
||||
a.flex.card-field-order-move.js-card-field-order-up(href="#" role="button" class="{{#unless row.canMoveUp}}is-disabled{{/unless}}" title="{{_ 'card-field-order-move-up'}}" aria-label="{{_ 'card-field-order-move-up'}}")
|
||||
a.flex.card-field-order-move.js-card-field-order-up(href="#" role="button" class="{{#unless row.canMoveUp}}is-disabled{{/unless}}" aria-disabled="{{#if row.canMoveUp}}false{{else}}true{{/if}}" title="{{#if row.canMoveUp}}{{_ 'card-field-order-move-up'}}{{else}}{{_ 'r-rule-disabled'}}{{/if}}" aria-label="{{_ 'card-field-order-move-up'}}")
|
||||
i.fa.fa-arrow-up
|
||||
a.flex.card-field-order-move.js-card-field-order-down(href="#" role="button" class="{{#unless row.canMoveDown}}is-disabled{{/unless}}" title="{{_ 'card-field-order-move-down'}}" aria-label="{{_ 'card-field-order-move-down'}}")
|
||||
a.flex.card-field-order-move.js-card-field-order-down(href="#" role="button" class="{{#unless row.canMoveDown}}is-disabled{{/unless}}" aria-disabled="{{#if row.canMoveDown}}false{{else}}true{{/if}}" title="{{#if row.canMoveDown}}{{_ 'card-field-order-move-down'}}{{else}}{{_ 'r-rule-disabled'}}{{/if}}" aria-label="{{_ 'card-field-order-move-down'}}")
|
||||
i.fa.fa-arrow-down
|
||||
span.card-field-order-label
|
||||
each icon in row.icons
|
||||
@@ -702,7 +702,7 @@ template(name="wipLimitGroupsPopup")
|
||||
.wip-limit-group-name
|
||||
input.js-wip-limit-group-name(type="text" value="{{name}}" placeholder="{{_ 'wip-limit-group-name-placeholder'}}")
|
||||
.wip-limit-group-lists
|
||||
each ../boardLists
|
||||
each boardLists
|
||||
label.flex
|
||||
input.js-wip-limit-group-list-toggle(type="checkbox" data-list-id="{{_id}}" checked="{{isListInGroup _id ../listIds}}")
|
||||
| {{title}}
|
||||
|
||||
@@ -529,7 +529,7 @@ Template.boardMenuPopup.events({
|
||||
'click .js-change-background-image': Popup.open('boardChangeBackgroundImage'),
|
||||
'click .js-open-notification-settings': Popup.open('notificationSettings', {
|
||||
titleKey: 'notifications',
|
||||
scope: 'board',
|
||||
dataContext: { scope: 'board' },
|
||||
}),
|
||||
'click .js-board-info-on-my-boards': Popup.open('boardInfoOnMyBoards'),
|
||||
'click .js-change-language': Popup.open('changeLanguage'),
|
||||
@@ -1836,7 +1836,7 @@ function buildCardSettingsRows(side, data) {
|
||||
toggle: spec.toggle,
|
||||
checked: typeof helper === 'function' ? Boolean(helper.call(data)) : false,
|
||||
icons: row.icons,
|
||||
title: row.label.map(k => TAPi18n.__(k)).join(' '),
|
||||
title: row.label.map(k => TAPi18n.__(k)).join(row.labelSeparator || ' '),
|
||||
personal: Boolean(spec.personal),
|
||||
labelTextOverride: Boolean(spec.labelTextOverride),
|
||||
canMoveUp: positioned && canMove(stored, layout, row.key, 'up'),
|
||||
@@ -1852,6 +1852,10 @@ function buildCardSettingsRows(side, data) {
|
||||
// allowsLabels}}` read, defaults and fallbacks included, without a second
|
||||
// copy of that logic. Registered at the end of the object.
|
||||
const boardCardSettingsHelpers = {
|
||||
allowsMinicardCollapse() { return ReactiveCache.getBoard(Session.get('currentBoard'))?.allowsMinicardCollapse !== false; },
|
||||
labelsAboveTitleOnMinicard() { return ReactiveCache.getBoard(Session.get('currentBoard'))?.labelsAboveTitleOnMinicard === true; },
|
||||
allowsChecklistDueDate() { return ReactiveCache.getBoard(Session.get('currentBoard'))?.allowsChecklistDueDate !== false; },
|
||||
allowsChecklistTitle() { return ReactiveCache.getBoard(Session.get('currentBoard'))?.allowsChecklistTitle !== false; },
|
||||
// Board Settings / Card Settings shows both columns - "Show on Card" and
|
||||
// "Show on Minicard" beside each other. The card's own menu and the
|
||||
// minicard's menu open the SAME popup asking for one of them, and the other
|
||||
@@ -2311,6 +2315,7 @@ Template.boardCardSettingsPopup.helpers(boardCardSettingsHelpers);
|
||||
// board's setter, which normalises it. models/lib/cardFieldOrder.js
|
||||
function moveCardSettingsRow(evt, direction) {
|
||||
evt.preventDefault();
|
||||
if (evt.currentTarget.classList.contains('is-disabled')) return;
|
||||
const rowEl = evt.currentTarget.closest('.js-card-field-order-row');
|
||||
if (!rowEl) return;
|
||||
const { key, side } = rowEl.dataset;
|
||||
@@ -2544,6 +2549,26 @@ Template.boardCardSettingsPopup.events({
|
||||
const newValue = !tpl.currentBoard.allowsDescriptionText;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsDescriptionText: newValue } });
|
||||
},
|
||||
'click .js-field-minicard-collapse'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const value = tpl.currentBoard.allowsMinicardCollapse !== false;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsMinicardCollapse: !value } });
|
||||
},
|
||||
'click .js-field-labels-above-title'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const value = tpl.currentBoard.labelsAboveTitleOnMinicard === true;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { labelsAboveTitleOnMinicard: !value } });
|
||||
},
|
||||
'click .js-field-checklist-due-date'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const value = tpl.currentBoard.allowsChecklistDueDate !== false;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsChecklistDueDate: !value } });
|
||||
},
|
||||
'click .js-field-checklist-title'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const value = tpl.currentBoard.allowsChecklistTitle !== false;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsChecklistTitle: !value } });
|
||||
},
|
||||
'click .js-field-has-checklists'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsChecklists;
|
||||
|
||||
@@ -110,7 +110,7 @@ template(name="archivesSidebar")
|
||||
+spinner
|
||||
|
||||
template(name="restoreArchivedCardToListPopup")
|
||||
p {{_ 'no-list-found'}}, {{_ 'select-list'}}:
|
||||
p {{_ 'error-list-doesNotExist'}}, {{_ 'list'}}:
|
||||
select.js-select-restore-target-list(autofocus)
|
||||
each availableLists
|
||||
option(value="{{_id}}" selected="{{#if isSelectedList _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
@@ -119,7 +119,7 @@ template(name="restoreArchivedCardToListPopup")
|
||||
button.primary.confirm.js-restore-card-to-list {{_ 'restore'}}
|
||||
|
||||
template(name="restoreArchivedListToSwimlanePopup")
|
||||
p {{_ 'no-swimlane-found'}}, {{_ 'select-swimlane'}}:
|
||||
p {{_ 'no-results'}}, {{_ 'wip-limit-group-select-swimlane'}}:
|
||||
select.js-select-restore-target-swimlane(autofocus)
|
||||
each availableSwimlanes
|
||||
option(value="{{_id}}" selected="{{#if isSelectedSwimlane _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
@@ -313,4 +313,4 @@ template(name="setSelectionColorPopup")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'remove-btn'}}
|
||||
@@ -164,7 +164,7 @@ template(name="setSwimlaneColorPopup")
|
||||
// Buttons aligned left too
|
||||
.flush-left
|
||||
button.primary.confirm.js-submit(style="margin-inline-start:0") {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right(style="margin-inline-start:8px") {{_ 'unset-color'}}
|
||||
button.js-remove-color.negate.wide.right(style="margin-inline-start:8px") {{_ 'remove-btn'}}
|
||||
|
||||
template(name="setSwimlaneHeightPopup")
|
||||
.flush-left.swimlane-height-popup
|
||||
|
||||
@@ -3,7 +3,7 @@ template(name='passwordInput')
|
||||
label(for='at-field-{{_id}}') {{displayName}}
|
||||
.password-input-container
|
||||
input.password-field(type="{{type}}" placeholder="{{displayName}}" autocomplete="{{autocomplete}}" required="{{required}}")
|
||||
button.password-toggle-btn.primary(type="button" tabindex="-1" aria-label="{{_ 'password'}}: {{_ 'visibility'}}" title="{{_ 'password'}}: {{_ 'visibility'}}")
|
||||
button.password-toggle-btn.primary(type="button" aria-label="{{_ 'password'}}: {{_ 'visibility'}}" title="{{_ 'password'}}: {{_ 'visibility'}}")
|
||||
i.fa.fa-eye.eye-icon(aria-hidden="true")
|
||||
i.fa.fa-eye-slash.eye-slash-icon(aria-hidden="true")
|
||||
if errs
|
||||
|
||||
@@ -329,7 +329,7 @@ Template.changeAvatarPopup.events({
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (this && typeof this.link === 'function') {
|
||||
const avatarUrl = this.link();
|
||||
const avatarUrl = `/cdn/storage/avatars/${this._id}`;
|
||||
changeAvatarSetAvatar(tpl, avatarUrl);
|
||||
}
|
||||
},
|
||||
@@ -495,7 +495,7 @@ Template.adminChangeAvatarPopup.events({
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (this && typeof this.link === 'function') {
|
||||
const avatarUrl = this.link();
|
||||
const avatarUrl = `/cdn/storage/avatars/${this._id}`;
|
||||
adminChangeAvatarSetAvatar(tpl, avatarUrl);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -295,7 +295,7 @@ template(name="changeFontPopup")
|
||||
label {{_ 'text-color'}}
|
||||
.color-wheel-wrap
|
||||
input.js-ui-text-color(type="color" value=textColorHex title="{{_ 'text-color'}}")
|
||||
a.js-reset-text-color(class="{{#unless hasTextColor}}disabled{{/unless}}") {{_ 'unset-color'}}
|
||||
a.js-reset-text-color(class="{{#unless hasTextColor}}disabled{{/unless}}") {{_ 'remove-btn'}}
|
||||
|
||||
template(name="changeSettingsPopup")
|
||||
ul.pop-over-list
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { accountOperationErrorKey } from '/client/lib/accountOperationError';
|
||||
import { languageFlags, languageLabelParts } from '/imports/i18n/languageFlags';
|
||||
const { availableCalendarSystems } = require('/imports/lib/calendarSystems');
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
@@ -93,7 +94,7 @@ Template.memberMenuPopup.events({
|
||||
'click .js-change-color': Popup.open('changeColor'),
|
||||
'click .js-open-notification-settings': Popup.open('notificationSettings', {
|
||||
titleKey: 'notifications',
|
||||
scope: 'member',
|
||||
dataContext: { scope: 'member' },
|
||||
}),
|
||||
'click .js-change-font': Popup.open('changeFont'),
|
||||
'click .js-change-avatar': Popup.open('changeAvatar'),
|
||||
@@ -258,7 +259,7 @@ Template.editProfilePopup.events({
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.error('Error removing user:', error);
|
||||
}
|
||||
alert('Error deleting account: ' + error.reason);
|
||||
alert(TAPi18n.__(accountOperationErrorKey(error)));
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('User deleted successfully:', result);
|
||||
@@ -277,7 +278,7 @@ Template.editProfilePopup.events({
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.error('Error anonymizing user:', error);
|
||||
}
|
||||
alert('Error anonymizing account: ' + error.reason);
|
||||
alert(TAPi18n.__(accountOperationErrorKey(error)));
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('User anonymized successfully:', result);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Keep server diagnostics in the console; show existing localized UI messages.
|
||||
// The last-admin case must precede the general authorization failure.
|
||||
export function accountOperationErrorKey(error) {
|
||||
if (error?.error === 'not-authorized' && error.reason === 'Cannot delete the last administrator') return 'last-admin-desc';
|
||||
if (error?.error === 'not-authorized') return 'error-notAllowed';
|
||||
if (error?.error === 'user-not-found') return 'error-user-doesNotExist';
|
||||
return 'server-error';
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
const { resolveDateFormat } = require('/models/lib/dateFormatPolicy');
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
|
||||
import { formatJalaliDate, gregorianToJalali } from '/imports/lib/jalaliDate';
|
||||
@@ -5,21 +6,26 @@ import { TAPi18n } from '/imports/i18n';
|
||||
const { CALENDAR_SYSTEMS, formatNativeCalendarDate, nativeCalendarParts } = require('/imports/lib/calendarSystems');
|
||||
const { calendarMonthRange, shiftedDay } = require('/imports/lib/calendarMonth');
|
||||
|
||||
export function isDateFormatForced() {
|
||||
return !!ReactiveCache.getCurrentSetting()?.hideDateFormat;
|
||||
}
|
||||
|
||||
export function dateDisplayPreferences() {
|
||||
const setting = ReactiveCache.getCurrentSetting();
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user) {
|
||||
return {
|
||||
calendarSystem: user.getCalendarSystem ? user.getCalendarSystem() : 'gregorian',
|
||||
dateFormat: user.getDateFormat ? user.getDateFormat() : 'YYYY-MM-DD',
|
||||
dateFormat: resolveDateFormat(user.getDateFormat ? user.getDateFormat() : null, setting),
|
||||
};
|
||||
}
|
||||
try {
|
||||
return {
|
||||
calendarSystem: window.localStorage.getItem('calendarSystem') || 'gregorian',
|
||||
dateFormat: window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD',
|
||||
dateFormat: resolveDateFormat(window.localStorage.getItem('dateFormat'), setting),
|
||||
};
|
||||
} catch (error) {
|
||||
return { calendarSystem: 'gregorian', dateFormat: 'YYYY-MM-DD' };
|
||||
return { calendarSystem: 'gregorian', dateFormat: resolveDateFormat(null, setting) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +52,7 @@ export function formatDateForDisplay(date, includeTime = true, gregorianFormatte
|
||||
);
|
||||
if (system) return text || '';
|
||||
}
|
||||
return gregorianFormatter
|
||||
return gregorianFormatter && !isDateFormatForced() && !dateFormat.endsWith('-date-only')
|
||||
? gregorianFormatter(value)
|
||||
: formatDateByUserPreference(value, dateFormat, includeTime);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,15 @@ export class BoardSwimlaneListDialog {
|
||||
this.selectedBoardId = new ReactiveVar(this.currentBoardId);
|
||||
this.selectedSwimlaneId = new ReactiveVar('');
|
||||
this.selectedListId = new ReactiveVar('');
|
||||
this.loading = new ReactiveVar(true);
|
||||
this.error = new ReactiveVar('');
|
||||
this.saving = new ReactiveVar(false);
|
||||
this._request = 0;
|
||||
this.tpl.view?.onViewDestroyed(() => {
|
||||
this._disposed = true;
|
||||
this._request++;
|
||||
this._boardSubscription?.stop();
|
||||
});
|
||||
this.setOption(this.currentBoardId);
|
||||
}
|
||||
|
||||
@@ -99,7 +108,7 @@ export class BoardSwimlaneListDialog {
|
||||
setFirstSwimlaneId() {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
const swimlaneId = board.swimlanes()[0]._id;
|
||||
const swimlaneId = board?.swimlanes()[0]?._id || '';
|
||||
this.selectedSwimlaneId.set(swimlaneId);
|
||||
} catch (e) {}
|
||||
}
|
||||
@@ -124,6 +133,7 @@ export class BoardSwimlaneListDialog {
|
||||
const selector = {
|
||||
boardId,
|
||||
archived: false,
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
if (swimlaneId) {
|
||||
@@ -197,7 +207,7 @@ export class BoardSwimlaneListDialog {
|
||||
/** returns all available swimlanes of the current board */
|
||||
swimlanes() {
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
return board.swimlanes();
|
||||
return board?.swimlanes() || [];
|
||||
}
|
||||
|
||||
/** returns all available lists of the current board */
|
||||
@@ -233,16 +243,36 @@ export class BoardSwimlaneListDialog {
|
||||
|
||||
/** get the board data from the server */
|
||||
getBoardData(boardId) {
|
||||
const self = this;
|
||||
Meteor.subscribe('board', boardId, false, {
|
||||
onReady() {
|
||||
const sameBoardId = self.selectedBoardId.get() == boardId;
|
||||
self.selectedBoardId.set(boardId);
|
||||
|
||||
if (!sameBoardId) {
|
||||
self.setFirstSwimlaneId();
|
||||
self.setFirstListId();
|
||||
const changed = this.selectedBoardId.get() !== boardId;
|
||||
const request = ++this._request;
|
||||
this._boardSubscription?.stop();
|
||||
this.selectedBoardId.set(boardId);
|
||||
this.loading.set(true);
|
||||
this.error.set('');
|
||||
if (changed) {
|
||||
this.selectedSwimlaneId.set('');
|
||||
this.selectedListId.set('');
|
||||
this.selectedCardId?.set('');
|
||||
}
|
||||
if (!boardId) return;
|
||||
this._boardSubscription = this.tpl.subscribe('board', boardId, false, {
|
||||
onReady: () => {
|
||||
if (this._disposed || request !== this._request) return;
|
||||
const lanes = this.swimlanes();
|
||||
if (!lanes.some(lane => lane._id === this.selectedSwimlaneId.get())) this.setFirstSwimlaneId();
|
||||
if (!this.lists().some(list => list._id === this.selectedListId.get())) this.setFirstListId();
|
||||
if (this.selectedCardId && !this.cards().some(card => card._id === this.selectedCardId.get())) {
|
||||
this.selectedCardId.set('');
|
||||
}
|
||||
this.loading.set(false);
|
||||
},
|
||||
onStop: error => {
|
||||
if (this._disposed || request !== this._request || !error) return;
|
||||
this.selectedSwimlaneId.set('');
|
||||
this.selectedListId.set('');
|
||||
this.selectedCardId?.set('');
|
||||
this.error.set(TAPi18n.__('server-error'));
|
||||
this.loading.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { BoardSwimlaneListDialog } from '/client/lib/dialogWithBoardSwimlaneList';
|
||||
@@ -10,7 +9,7 @@ import { BoardSwimlaneListDialog } from '/client/lib/dialogWithBoardSwimlaneList
|
||||
export class BoardSwimlaneListCardDialog extends BoardSwimlaneListDialog {
|
||||
constructor(tpl, callbacks = {}) {
|
||||
super(tpl, callbacks);
|
||||
this.selectedCardId = new ReactiveVar('');
|
||||
this.selectedCardId = new ReactiveVar(this.cardOption.cardId || '');
|
||||
}
|
||||
|
||||
getDefaultOption() {
|
||||
@@ -46,26 +45,7 @@ export class BoardSwimlaneListCardDialog extends BoardSwimlaneListDialog {
|
||||
|
||||
/** returns if the card id was the last confirmed one */
|
||||
isDialogOptionCardId(cardId) {
|
||||
return this.cardOption.cardId == cardId;
|
||||
}
|
||||
|
||||
/** Override to also reset card id on board change */
|
||||
getBoardData(boardId) {
|
||||
const self = this;
|
||||
Meteor.subscribe('board', boardId, false, {
|
||||
onReady() {
|
||||
const sameBoardId = self.selectedBoardId.get() == boardId;
|
||||
self.selectedBoardId.set(boardId);
|
||||
|
||||
if (!sameBoardId) {
|
||||
self.setFirstSwimlaneId();
|
||||
self.setFirstListId();
|
||||
if (self.selectedCardId) {
|
||||
self.selectedCardId.set('');
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return this.selectedCardId.get() === cardId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,7 +31,7 @@ const MOVE_CANCEL_PX = 10; // finger moving this far before the hold = a scroll
|
||||
// and jQuery-UI sortables (lists / swimlanes / cards) already have touch via
|
||||
// touch-punch, so leave them to it.
|
||||
const EXCLUDE =
|
||||
'input, select, textarea, .note-editable, .ui-sortable, .ui-sortable-handle';
|
||||
'input, select, textarea, .ui-sortable, .ui-sortable-handle';
|
||||
|
||||
function draggableAncestor(el) {
|
||||
while (el && el !== document.body) {
|
||||
|
||||
@@ -33,7 +33,7 @@ function isExcluded(target) {
|
||||
// .js-list-handle) must be excluded too: those start a jQuery UI
|
||||
// sortable reorder via touch-punch, and hijacking the touch to scroll
|
||||
// here would block dragging the handle to reorder swimlanes/lists.
|
||||
'input, select, textarea, .note-editable, .nodragscroll, .handle, .ui-sortable-handle',
|
||||
'input, select, textarea, .nodragscroll, .handle, .ui-sortable-handle',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { resolveDateFormat } = require('/models/lib/dateFormatPolicy');
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
|
||||
@@ -44,12 +45,13 @@ export function browserTimezone() {
|
||||
// back to, which the server cannot see at all. An export of a card should print
|
||||
// its dates the way the card prints them.
|
||||
export function cardDateFormat() {
|
||||
const setting = ReactiveCache.getCurrentSetting();
|
||||
try {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (currentUser) return currentUser.getDateFormat();
|
||||
return window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD';
|
||||
if (currentUser) return resolveDateFormat(currentUser.getDateFormat(), setting);
|
||||
return resolveDateFormat(window.localStorage.getItem('dateFormat'), setting);
|
||||
} catch (error) {
|
||||
return 'YYYY-MM-DD';
|
||||
return resolveDateFormat(null, setting);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveDateFormat } from '/models/lib/dateFormatPolicy';
|
||||
import { Blaze } from 'meteor/blaze';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
@@ -410,7 +411,7 @@ class AdvancedFilter {
|
||||
user && typeof user.getDateFormat === 'function'
|
||||
? user.getDateFormat()
|
||||
: 'YYYY-MM-DD';
|
||||
dayFirst = /^D/.test(dateFormat);
|
||||
dayFirst = /^D/.test(resolveDateFormat(dateFormat, ReactiveCache.getCurrentSetting()));
|
||||
} catch (error) {
|
||||
dayFirst = false;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ function isExcluded(target) {
|
||||
target.closest &&
|
||||
// Form fields keep their own behaviour (the zoom-level input lives in the
|
||||
// quick-access bar), and `.nodragscroll` opts an element out explicitly.
|
||||
target.closest('input, select, textarea, .note-editable, .nodragscroll')
|
||||
target.closest('input, select, textarea, .nodragscroll')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,10 @@ Template.inlinedForm.events({
|
||||
// in Member Settings submits on plain Enter (Shift+Enter for a newline).
|
||||
const submitOnEnter = !!ReactiveCache.getCurrentUser()?.hasSubmitOnEnter?.();
|
||||
if (isSubmitKey(evt, { submitOnEnter })) {
|
||||
// #6700: the enclosing checklist has its own Enter handler. A handled
|
||||
// submit key must not bubble to it and create a second document.
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
tpl.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -46,6 +46,9 @@ hotkeys.filter = (event) => {
|
||||
if (currentElement instanceof HTMLInputElement || currentElement instanceof HTMLSelectElement || currentElement instanceof HTMLTextAreaElement)
|
||||
return false;
|
||||
|
||||
if (currentElement.closest('button, a[href], summary, [role="button"], [role="tab"], [role="checkbox"]'))
|
||||
return false;
|
||||
|
||||
// We can trigger events!
|
||||
return true;
|
||||
};
|
||||
@@ -165,6 +168,7 @@ hotkeys(shiftNums, (event, handler) => {
|
||||
}
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
const board = ReactiveCache.getBoard(currentBoardId);
|
||||
if (!board) return;
|
||||
const labels = board.labels;
|
||||
if (MultiSelection.isActive()) {
|
||||
const cardIds = MultiSelection.getSelectedCardIds();
|
||||
@@ -188,6 +192,7 @@ hotkeys(nums, (event, handler) => {
|
||||
return;
|
||||
}
|
||||
const board = ReactiveCache.getBoard(currentBoardId);
|
||||
if (!board) return;
|
||||
const labels = board.labels;
|
||||
if (MultiSelection.isActive() && ReactiveCache.getCurrentUser().isBoardMember()) {
|
||||
const cardIds = MultiSelection.getSelectedCardIds();
|
||||
|
||||
+1
-1
@@ -116,7 +116,7 @@ window.Popup = new (class {
|
||||
title: self._getTitle(popupName, openOptions.titleKey),
|
||||
depth: self._stack.length,
|
||||
offset: self._getOffset(openerElement, popupName),
|
||||
dataContext: (this && this.currentData && this.currentData()) || (options && options.dataContextIfCurrentDataIsUndefined) || this,
|
||||
dataContext: openOptions.dataContext || (this && this.currentData && this.currentData()) || (options && options.dataContextIfCurrentDataIsUndefined) || this,
|
||||
});
|
||||
|
||||
const $contentWrapper = $('.content-wrapper')
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import Cards from '/models/cards';
|
||||
import { relativePosition } from '/models/lib/relativePosition';
|
||||
|
||||
export async function relativeCardSort(targetCard, position, movingId) {
|
||||
if (targetCard._id === movingId) return targetCard.sort;
|
||||
const scope = { boardId: targetCard.boardId, listId: targetCard.listId,
|
||||
swimlaneId: targetCard.swimlaneId, archived: false, deletedAt: null };
|
||||
const siblings = Cards.find(scope, { sort: { sort: 1, _id: 1 } }).fetch();
|
||||
const plan = relativePosition(siblings, targetCard._id, position, movingId);
|
||||
for (const update of plan.updates) {
|
||||
const sibling = Cards.findOne({ ...scope, _id: update.id });
|
||||
if (!sibling) throw new Error(TAPi18n.__('no-cards-found'));
|
||||
await sibling.move(sibling.boardId, sibling.swimlaneId, sibling.listId, update.sort);
|
||||
}
|
||||
return plan.sort;
|
||||
}
|
||||
@@ -26,9 +26,9 @@ export function createEscapeableTextComplete(element, strategies, options = {})
|
||||
// Merge default options
|
||||
const mergedOptions = {
|
||||
dropdown: {
|
||||
className: 'textcomplete-dropdown',
|
||||
className: 'dropdown-menu textcomplete-dropdown',
|
||||
maxCount: 10,
|
||||
placement: 'bottom',
|
||||
placement: 'auto',
|
||||
...options.dropdown,
|
||||
},
|
||||
};
|
||||
@@ -46,7 +46,7 @@ export function createEscapeableTextComplete(element, strategies, options = {})
|
||||
// handler (the form `submit`, the inlinedForm/minicard handlers, …) AND
|
||||
// prevent the default newline so the textcomplete `selected` action is the
|
||||
// only effect. `stopImmediatePropagation` also blocks other listeners bound
|
||||
// to this same element (e.g. summernote in the richer editor).
|
||||
// to this same textarea.
|
||||
element.addEventListener('keydown', (evt) => {
|
||||
const isTabOrEnter = evt.keyCode === 9 || evt.keyCode === 13;
|
||||
if (isTabOrEnter && !shouldSubmitOnEnter({ autocompleteOpen: dropdownMenuIsOpened })) {
|
||||
|
||||
+3
-6
@@ -764,12 +764,9 @@ export const Utils = {
|
||||
})
|
||||
);
|
||||
},
|
||||
// Shared by every card attachment upload path — the card Attachments popup,
|
||||
// the clipboard upload AND images uploaded inside card comments through the
|
||||
// rich text editor (client/components/main/editor.js). Keeping the meta
|
||||
// identical (same meta.cardId) is what makes comment attachments appear in
|
||||
// the card's Attachments list (#3843); the actual logic lives in
|
||||
// models/lib/attachmentMeta.js so it can be unit tested.
|
||||
// Shared by the card Attachments popup and clipboard upload. The same
|
||||
// meta.cardId keeps existing attachments referenced in comments visible
|
||||
// in the card gallery, including linked cards (#3843).
|
||||
getCommonAttachmentMetaFrom(card) {
|
||||
return buildCardAttachmentMeta(card, id => ReactiveCache.getCard(id));
|
||||
},
|
||||
|
||||
@@ -312,9 +312,6 @@ services:
|
||||
# Defaults below. Uncomment to change. wekan/server/accounts-common.js
|
||||
# - ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90
|
||||
#---------------------------------------------------------------
|
||||
# ==== RICH TEXT EDITOR IN CARD COMMENTS ====
|
||||
# https://github.com/wekan/wekan/pull/2560
|
||||
- RICHER_CARD_COMMENT_EDITOR=false
|
||||
#---------------------------------------------------------------
|
||||
# ==== CARD LOADING: all (default) or lazy ====
|
||||
# 'all' = load every card of a board into the browser (default).
|
||||
|
||||
Loaded 100 of 675 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user