Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e4c0539af
|
||
|
|
b8fcef2fd4
|
||
|
|
4e2e6adc73 | ||
|
|
091541a960 | ||
|
|
967b8482d3 | ||
|
|
cb43837c32 | ||
|
|
899617bd4d | ||
|
|
2757bfb365 | ||
|
|
1ed942bb8a | ||
|
|
179157ccd2 | ||
|
|
3e36b931a6 | ||
|
|
c9e60847d0 | ||
|
|
4d83dc0c05 | ||
|
|
995965d157 | ||
|
|
15fbb314b0 | ||
|
|
68e0032c6d | ||
|
|
cffca535b5 | ||
|
|
b2d2632e3b | ||
|
|
119379d4b4 | ||
|
|
c434f83dfc | ||
|
|
1527f364b1 | ||
|
|
5779d94d78 | ||
|
|
096a3dc46b | ||
|
|
c883f31d35 | ||
|
|
1fa9714d4e | ||
|
|
53b64ee222 | ||
|
|
e4b8396a3b | ||
|
|
8cf7e83493 | ||
|
|
68220f944b | ||
|
|
fc4d84574e | ||
|
|
2989e1d5b1 | ||
|
|
5844e20d55 | ||
|
|
2376237b6e | ||
|
|
bcf55bdc3a | ||
|
|
ac44706461 | ||
|
|
928db8d675 | ||
|
|
5c9407f729 | ||
|
|
93f50a7fb3 | ||
|
|
39d84f8fe8 | ||
|
|
d369a3614a | ||
|
|
078ab4125e | ||
|
|
3f0dc8b7be | ||
|
|
c020e09aff | ||
|
|
b0c007d44f | ||
|
|
b777ee2a12 | ||
|
|
b328cae552 | ||
|
|
5357bb47c3 | ||
|
|
447877733b | ||
|
|
feb491f8d4 | ||
|
|
8f59d76a4e | ||
|
|
3a52565ab4 | ||
|
|
08ae61161c | ||
|
|
1835245943 | ||
|
|
83de473e4e | ||
|
|
a532ee7f14 | ||
|
|
f00b265b34 | ||
|
|
2931cc990f | ||
|
|
25a4672d6d | ||
|
|
969a4016ee | ||
|
|
5c2d7079ce | ||
|
|
d1c71d0526 | ||
|
|
1fa2ab0146 | ||
|
|
26a03f4845 | ||
|
|
58bfad3adf | ||
|
|
f1c2f1f40f | ||
|
|
7d3e8eb89f | ||
|
|
3fe8f602c8 | ||
|
|
d87eed36a0 | ||
|
|
9cc0660410 | ||
|
|
58ff88890d | ||
|
|
77d60b21a1 | ||
|
|
9df2db0314 | ||
|
|
91839cf3ef | ||
|
|
74f2352387 | ||
|
|
f299ca1285 | ||
|
|
f47e67b5b4 | ||
|
|
fe16b94c1e | ||
|
|
b77b62bfa2 | ||
|
|
43d8bf1b92 | ||
|
|
dca9427bfb | ||
|
|
7f17616b75 | ||
|
|
4a9dcdd0ef | ||
|
|
a1a4176ffb | ||
|
|
a86a087915 | ||
|
|
f9ace4cde9 | ||
|
|
4677324425 | ||
|
|
7e5852c62c | ||
|
|
e01f99eb52 | ||
|
|
8ff9a41f2d | ||
|
|
075e86b00e | ||
|
|
f20080a603 | ||
|
|
3594d75b5d | ||
|
|
72767ad9a7 | ||
|
|
7db386fda3 | ||
|
|
39f3c89b0a | ||
|
|
15a598525f | ||
|
|
d30803276d | ||
|
|
be63a357dd | ||
|
|
c71a97cba2 | ||
|
|
357de728c0 | ||
|
|
db3c0c18f9 | ||
|
|
45706292e7 | ||
|
|
29b3f52e50 | ||
|
|
f81a0d1537 | ||
|
|
e613e8763b | ||
|
|
3d17810293 | ||
|
|
1871873da0 | ||
|
|
15b3213a4d | ||
|
|
4265288417 | ||
|
|
a36d6f9326 | ||
|
|
3c50f90b51 | ||
|
|
24a4b8e5ee | ||
|
|
e676f96905 | ||
|
|
a903154ab2 | ||
|
|
beedf2fefa | ||
|
|
dee5711000 | ||
|
|
e93c049e8c | ||
|
|
fc10ce4259 | ||
|
|
2573034149 | ||
|
|
a1fc2a91dc | ||
|
|
e86c37e636 | ||
|
|
8b71386b90 | ||
|
|
2edef5b4ab | ||
|
|
d2a25ae9a6 | ||
|
|
6f021aa387 | ||
|
|
d80afc7834 | ||
|
|
d8975d21be | ||
|
|
b0918686a2 | ||
|
|
71c9e1b3ee | ||
|
|
10759d777b | ||
|
|
c956ab5a4e | ||
|
|
f9b790668d | ||
|
|
0e5fef6f31 | ||
|
|
832265e091 | ||
|
|
8940a10397 | ||
|
|
1e1d9586d7 | ||
|
|
305864f0c7 | ||
|
|
d475214224 | ||
|
|
73204d4e0a | ||
|
|
533187d328 | ||
|
|
70db04a93f | ||
|
|
e8eae6a1fc | ||
|
|
8ea5a6a097 | ||
|
|
e6a2314a1e | ||
|
|
7836ce27e7 | ||
|
|
a5fc4a4893 | ||
|
|
358d893c33 | ||
|
|
ae211b5adf | ||
|
|
d0b2b5201e | ||
|
|
0130858584 | ||
|
|
6b923eb4c2 | ||
|
|
d033dad20f | ||
|
|
f882a14ee0 | ||
|
|
990d06b8e1 | ||
|
|
b6564c81f1 | ||
|
|
8df46d8512 | ||
|
|
5072861687 | ||
|
|
919bd9e29b | ||
|
|
c5e42607af | ||
|
|
e64e2c6d53 | ||
|
|
36274f35f4 | ||
|
|
d5e717e9ce | ||
|
|
13f78f6207 | ||
|
|
e7aedd9f6b | ||
|
|
fc76d0e576 | ||
|
|
a1b7194261 | ||
|
|
66d15f6ab0 | ||
|
|
561e62a05c | ||
|
|
b4b81684e0 | ||
|
|
898f625f1f | ||
|
|
105199991f | ||
|
|
89be5c1f6e | ||
|
|
ae671bc70a | ||
|
|
78e34b5522 | ||
|
|
42d79b3dcf | ||
|
|
2d4178a281 | ||
|
|
ac0e11e548 | ||
|
|
0543cc42de | ||
|
|
06c803388b | ||
|
|
58b00162d9 | ||
|
|
08084d96e8 | ||
|
|
8b63e84cba | ||
|
|
a2e76f2cf0 | ||
|
|
72f22d1235 | ||
|
|
76945ee5b5 | ||
|
|
d8b7d6e5f4 | ||
|
|
1369316401 | ||
|
|
29b0cc1f86 | ||
|
|
74f422b523 | ||
|
|
0fefdeec4c | ||
|
|
b99e7979db | ||
|
|
64b43a42dd | ||
|
|
9d35870863 | ||
|
|
526580cbb6 | ||
|
|
d3366ca7cd | ||
|
|
048a90f876 | ||
|
|
abc24cf381 | ||
|
|
9d21f19ab8 | ||
|
|
07a0072b87 | ||
|
|
dbe5705b47 | ||
|
|
0ba3255382 | ||
|
|
2e34b2d004 | ||
|
|
fc92b342ce | ||
|
|
45c4afd22b | ||
|
|
d9f1ecb834 | ||
|
|
a37190e0fc | ||
|
|
cd37c6c980 | ||
|
|
316567f219 | ||
|
|
e5c61b32e3 | ||
|
|
a41d5ff2c0 | ||
|
|
769a139e70 | ||
|
|
7729d9f33e | ||
|
|
8e13f2bce9 | ||
|
|
4e28ffef9c | ||
|
|
d06a7a2c76 | ||
|
|
886f3bb840 | ||
|
|
b501d7af33 | ||
|
|
c3ea57986f | ||
|
|
048947ea7e | ||
|
|
7834a660a5 | ||
|
|
eee7c49439 | ||
|
|
dbab502be4 | ||
|
|
f4d5279a09 | ||
|
|
c8430a8c14 | ||
|
|
63efa5d0f0 | ||
|
|
8cda80752f | ||
|
|
abeb2d2477 | ||
|
|
a7c74e8d8b | ||
|
|
4b7ed35300 | ||
|
|
6463a1ef87 | ||
|
|
f85dc3fa76 | ||
|
|
fb05b1a847 | ||
|
|
5b751217d5 | ||
|
|
07e6f663db | ||
|
|
23ed41bddf | ||
|
|
3e33c766d6 | ||
|
|
83078736f5 | ||
|
|
7ccb50e4f5 | ||
|
|
1032c9d9cf | ||
|
|
ecbecc9d86 | ||
|
|
797426335a | ||
|
|
e7f2068105 | ||
|
|
afc51165f4 | ||
|
|
87a6354e42 | ||
|
|
edc8c17e35 | ||
|
|
2a700f5690 | ||
|
|
a6ce798862 | ||
|
|
30fd008310 | ||
|
|
3608d4b657 | ||
|
|
7c1ee7f161 | ||
|
|
d978d626ae | ||
|
|
1059164255 | ||
|
|
741668a831 | ||
|
|
5f06ebfaec | ||
|
|
b5e79b37ed | ||
|
|
7076ce5df3 | ||
|
|
3e00b2ccc2 | ||
|
|
5681592572 | ||
|
|
87dcfdbff6 | ||
|
|
b035bed3d8 | ||
|
|
94d032c847 | ||
|
|
347d30c3e0 | ||
|
|
720ef251ae | ||
|
|
24577ad2a9 | ||
|
|
06cdd459db | ||
|
|
fad217db8a | ||
|
|
4aeb3aa3d2 | ||
|
|
8baa9124a6 | ||
|
|
1b1b04d083 | ||
|
|
982f0b1cf5 | ||
|
|
b5f3cbc38e | ||
|
|
28464685a7 | ||
|
|
3a6aa7dccd | ||
|
|
2cc30a85f2 | ||
|
|
c5ac0cf8ce | ||
|
|
95ad27e0df | ||
|
|
b0e2674b11 | ||
|
|
46e67512ac | ||
|
|
994ecf8ec0 | ||
|
|
d486eb2a8f | ||
|
|
8f574799e2 | ||
|
|
7496d994e1 | ||
|
|
ec728dabf6 | ||
|
|
2474ff15d6 | ||
|
|
53c205b4ce | ||
|
|
7b88f4c21a | ||
|
|
20368533a0 | ||
|
|
ac7712f141 | ||
|
|
0b9d33cac4 | ||
|
|
d4aca1c009 | ||
|
|
a5ca6b7a79 | ||
|
|
964caabaaf | ||
|
|
9455646d66 | ||
|
|
c2f67001d6 | ||
|
|
b57362c5e8 | ||
|
|
d5d04728fb | ||
|
|
b99e1ccc05 | ||
|
|
3390324c2a | ||
|
|
5748475e8d | ||
|
|
accb51cc67 | ||
|
|
f81fe11dae | ||
|
|
5309e4863d | ||
|
|
f26a2ac9ad | ||
|
|
c37047d8ee | ||
|
|
d5ca31a38b | ||
|
|
0958182306 | ||
|
|
9f979054de | ||
|
|
86e671ea49 | ||
|
|
47794f799b | ||
|
|
b6200c14fc | ||
|
|
53b0224cce | ||
|
|
f02e02b401 | ||
|
|
52ce4f6128 | ||
|
|
a12e075780 | ||
|
|
597eb6b2bb | ||
|
|
ba953b57e5 | ||
|
|
3adf69bd4c | ||
|
|
cb1c7da78c | ||
|
|
64bc774b1e | ||
|
|
172f3bb31b | ||
|
|
8183a645a1 | ||
|
|
f69e543376 | ||
|
|
fbce6e5e9b | ||
|
|
5efb90640c | ||
|
|
93f90873e8 | ||
|
|
8b8a0c8458 | ||
|
|
fc37c08687 | ||
|
|
933bd97393 | ||
|
|
ed5304cd30 | ||
|
|
e6a3ce6772 | ||
|
|
acc49aeccb | ||
|
|
f19938b6bb | ||
|
|
997931e462 | ||
|
|
e837c0948a | ||
|
|
361099b2cc | ||
|
|
5105cf6489 | ||
|
|
01043096f3 | ||
|
|
af86ea8785 | ||
|
|
b32a14fbd5 | ||
|
|
14de1ac072 | ||
|
|
ddc3335854 | ||
|
|
c01743b53a | ||
|
|
508d7a80ef | ||
|
|
49ee49d2b8 | ||
|
|
89999ab10f | ||
|
|
39effe9a0b | ||
|
|
ad3a38e900 | ||
|
|
b4742b84ab | ||
|
|
9ddad24e2a | ||
|
|
9f3c216576 | ||
|
|
664c285cbb | ||
|
|
7fb2c3de7b | ||
|
|
24f4bfd5ad | ||
|
|
45595d95ab | ||
|
|
568ea35ed7 | ||
|
|
6243550c84 | ||
|
|
6650618682 | ||
|
|
b26d9fa163 | ||
|
|
0c6acbfaa2 | ||
|
|
af7323a0ab | ||
|
|
1950aff865 | ||
|
|
9013ffc5ad | ||
|
|
6d56d69ae3 | ||
|
|
80f4b37337 | ||
|
|
0e287c9117 | ||
|
|
a3ed18294f | ||
|
|
8e4404ab49 | ||
|
|
8b6c459453 | ||
|
|
8d6d409cc2 | ||
|
|
9962f2bd9b | ||
|
|
6c816e573d | ||
|
|
7f1dd16dfc | ||
|
|
09a5737e21 | ||
|
|
7d94e575b7 | ||
|
|
bfd554e65c | ||
|
|
a692f270da | ||
|
|
6999faced9 | ||
|
|
ea70df6805 | ||
|
|
736fa07dca | ||
|
|
83a683435a | ||
|
|
7efba64c39 | ||
|
|
79521baad8 | ||
|
|
c1f0da1217 | ||
|
|
8da4a2ad04 | ||
|
|
6f8a502cf2 | ||
|
|
98fc5d6ffc | ||
|
|
8a8469ac2a | ||
|
|
059fc979dc | ||
|
|
21f2e2e6ef | ||
|
|
82e5ce1d76 | ||
|
|
b92292fe8f | ||
|
|
6b7cb473aa | ||
|
|
ac2bacf42c | ||
|
|
21c62c6b82 | ||
|
|
1a998f9ece | ||
|
|
fd10c4c9ad | ||
|
|
8d73d2f575 | ||
|
|
22b9930168 | ||
|
|
7337854bb7 | ||
|
|
f6303840c5 | ||
|
|
31bcf947cb | ||
|
|
0f8ff1f21c | ||
|
|
61554fa53a | ||
|
|
da4b40115d | ||
|
|
c7f9c95596 | ||
|
|
5c31adb314 | ||
|
|
cea6aee038 | ||
|
|
cb32b11586 | ||
|
|
d0de79cba2 | ||
|
|
c52f2481d3 | ||
|
|
690f49e60c | ||
|
|
fa78267068 | ||
|
|
e0391f7dd4 | ||
|
|
0f6883bdf3 | ||
|
|
b5a406f8eb | ||
|
|
80ffbcd562 | ||
|
|
8715133868 | ||
|
|
75cecab73f | ||
|
|
f778707236 | ||
|
|
b6b9d5b787 | ||
|
|
d19506d32f | ||
|
|
8349167a32 | ||
|
|
cf2bae0bfb | ||
|
|
eb75d60df1 | ||
|
|
57f1f9da5b | ||
|
|
c8c7230331 | ||
|
|
7e193ef603 | ||
|
|
5b1cd3dc2f | ||
|
|
b895a66e24 | ||
|
|
25dd4be3f8 | ||
|
|
6d70826b40 | ||
|
|
2dbe335b30 | ||
|
|
9e94321c90 | ||
|
|
6e07efdfa8 | ||
|
|
5c6aa15564 | ||
|
|
62c01725f9 | ||
|
|
b404ac6068 | ||
|
|
4520a432c4 | ||
|
|
aecdac5aca | ||
|
|
f2d6695fdc | ||
|
|
5435753573 | ||
|
|
90cfc3f696 | ||
|
|
7b7e376ba2 | ||
|
|
f347dc7d4d | ||
|
|
d174a4832e | ||
|
|
bc1cd58491 | ||
|
|
86ade7f1f6 | ||
|
|
f39f66192f | ||
|
|
330e0e3570 | ||
|
|
a54f23e2ec | ||
|
|
f7db51b457 | ||
|
|
ebaa31c1e0 | ||
|
|
123417d931 | ||
|
|
d7b0441376 | ||
|
|
a4c74a5980 | ||
|
|
5e09f6b599 | ||
|
|
f2968f0235 | ||
|
|
fa8036aa3c | ||
|
|
7556b3ed8e | ||
|
|
786c9295a9 | ||
|
|
c1cc1e3e3d | ||
|
|
158f31ad4f | ||
|
|
5e4319473e | ||
|
|
82c78ebd1e | ||
|
|
628a49d84e | ||
|
|
3c8db21d2f | ||
|
|
550f4a19cc | ||
|
|
cd2c69f5f0 | ||
|
|
cadd015d20 | ||
|
|
5aa2219541 | ||
|
|
4e77df9ff5 | ||
|
|
b40e6caa4d | ||
|
|
2a3aa981d2 | ||
|
|
653c61081a | ||
|
|
cc3b5cb8c6 | ||
|
|
df153dcc56 | ||
|
|
5878c80fa6 | ||
|
|
d04beea6f8 | ||
|
|
d160d985c9 | ||
|
|
f0a25358b4 | ||
|
|
36e1753f89 | ||
|
|
9defe9681b | ||
|
|
2bb6999656 | ||
|
|
309b488861 | ||
|
|
67192cf5c4 | ||
|
|
c84473ae43 | ||
|
|
f6328937c9 | ||
|
|
80db2517fd | ||
|
|
918bd0b6b5 | ||
|
|
e22f422615 | ||
|
|
1826f51664 | ||
|
|
e547be9ab8 | ||
|
|
89140f1fee | ||
|
|
1b57d09d0d | ||
|
|
e7d67e3ddb | ||
|
|
a4b5ac6c4e | ||
|
|
9b22f9e157 | ||
|
|
2ab0f59f13 | ||
|
|
611368abc6 | ||
|
|
d3b3be2a52 | ||
|
|
11a2c95ea7 | ||
|
|
3e612c39b7 | ||
|
|
58de6bb737 | ||
|
|
f6d8befb56 | ||
|
|
b49aa050e9 | ||
|
|
2ef8465e8d | ||
|
|
7878596608 | ||
|
|
1f2f9f571e | ||
|
|
855cb86088 | ||
|
|
ed3cf96c6f | ||
|
|
fdb477f351 | ||
|
|
ac6cc1041a | ||
|
|
7d09a649ac | ||
|
|
040ed6e605 | ||
|
|
d7e36f5157 | ||
|
|
1a08003834 | ||
|
|
64bc6ea71c | ||
|
|
ef4161889a | ||
|
|
dd9d6083c0 | ||
|
|
027b6a45a1 | ||
|
|
b717e5d7ad | ||
|
|
74965f5f95 | ||
|
|
fa812faf6d | ||
|
|
b459151798 | ||
|
|
73679919fb | ||
|
|
58b0ae29e1 | ||
|
|
773931a538 | ||
|
|
1ef814fe6b | ||
|
|
b010c687f3 | ||
|
|
05950fb287 | ||
|
|
56a685983b | ||
|
|
339c0a2bb7 | ||
|
|
a790453c9a | ||
|
|
04eb8a8fbb | ||
|
|
4ab1bc9638 | ||
|
|
699b9a9c71 | ||
|
|
3a203d274d | ||
|
|
887511fdda | ||
|
|
64f58455ff | ||
|
|
5163ec0da2 | ||
|
|
c47eb013b4 | ||
|
|
d4cbe12316 | ||
|
|
794222c85e | ||
|
|
675c661114 | ||
|
|
7700efde1f | ||
|
|
16832124ce | ||
|
|
bf5ca3d3eb | ||
|
|
189b6cae84 | ||
|
|
9bf0114148 | ||
|
|
9f3bbb5ed2 | ||
|
|
85b981e1c7 | ||
|
|
eb5ac8b7dd | ||
|
|
5ef32af248 | ||
|
|
9cb5837e3e | ||
|
|
5ae9c2b7ac | ||
|
|
7d16fed34e | ||
|
|
db2701f1d7 | ||
|
|
76818e2c98 | ||
|
|
c4e084ffbb | ||
|
|
a10b28bf6a | ||
|
|
90c0110615 | ||
|
|
73c594bb36 | ||
|
|
903544c680 | ||
|
|
32e1ec9d22 | ||
|
|
3516a21329 | ||
|
|
f6198dee30 | ||
|
|
13f14df880 | ||
|
|
f3ae64cf21 | ||
|
|
1820804b9f | ||
|
|
d3d76b12f7 | ||
|
|
51ad6ed99d | ||
|
|
1c8cea9ada | ||
|
|
a0c190137d | ||
|
|
83e1845290 | ||
|
|
052d8c1584 | ||
|
|
7b0e2213cf | ||
|
|
05e6d15478 | ||
|
|
be0283381b | ||
|
|
379a92846e | ||
|
|
bb6163a70f | ||
|
|
10f009f5b1 | ||
|
|
19c882ed2f | ||
|
|
e7cb6ac6f0 | ||
|
|
38e488ee4a | ||
|
|
589fa30fd1 | ||
|
|
64d351d835 | ||
|
|
4151ecb7e7 | ||
|
|
f4b5e5f07d | ||
|
|
c1639ea589 | ||
|
|
21b0ab064c | ||
|
|
1c0d8e9964 | ||
|
|
8a7949a8ab | ||
|
|
3c36db33e9 | ||
|
|
59d0cfc457 | ||
|
|
72f6d96dbc | ||
|
|
49c81f5426 | ||
|
|
08ed1705b1 | ||
|
|
47088c30c0 | ||
|
|
539dbdc034 | ||
|
|
129af0bad9 | ||
|
|
a9d95dd6b7 | ||
|
|
6d0978d0ed | ||
|
|
13cdf08e22 | ||
|
|
e59bc9cd86 | ||
|
|
6478d27d1c | ||
|
|
76d1800ec7 | ||
|
|
03ba747bc0 | ||
|
|
9eb3889d51 | ||
|
|
5d4bc7a886 | ||
|
|
135af977af | ||
|
|
6dfe81cbcb | ||
|
|
3ec2821058 | ||
|
|
1c5ac8efba | ||
|
|
ce7fa0e314 | ||
|
|
0f464b21a3 | ||
|
|
7169da8dae | ||
|
|
0f56e2dfc3 | ||
|
|
1b086c80a7 | ||
|
|
c27d5d0412 | ||
|
|
7b3043bf46 | ||
|
|
da19160e87 | ||
|
|
790a801f30 | ||
|
|
b08c66a483 | ||
|
|
524aa9124d | ||
|
|
df85b5ac45 | ||
|
|
c009ddc915 | ||
|
|
5407e4533c | ||
|
|
98088c7734 | ||
|
|
0d6fb33043 | ||
|
|
1dbad1b454 | ||
|
|
2c8a6c8ec6 | ||
|
|
06b9946dd8 | ||
|
|
dbcdf15244 | ||
|
|
7e2067d0b1 | ||
|
|
7489f720ea | ||
|
|
140e844238 | ||
|
|
de55d5afbe | ||
|
|
c5fccb52cc | ||
|
|
507ca57a50 | ||
|
|
b2e072e02c | ||
|
|
357f63d01f | ||
|
|
e6422d842a | ||
|
|
99519af83a | ||
|
|
514ce6ad24 | ||
|
|
f57ce52198 | ||
|
|
2a9c921ba6 | ||
|
|
0dd924f148 | ||
|
|
678eb0896d | ||
|
|
233c1c8e81 | ||
|
|
f6a851a3d8 | ||
|
|
4fba9d7802 | ||
|
|
d41ecb054c | ||
|
|
ec0c1bc9af | ||
|
|
05b77d24fa | ||
|
|
4535303807 | ||
|
|
f232fdc30b | ||
|
|
dab72d80dc | ||
|
|
6f83464b24 | ||
|
|
41cd0a8925 | ||
|
|
fc02a15753 | ||
|
|
bd9c5a058a | ||
|
|
c1692cc6b9 | ||
|
|
5259c09a5c | ||
|
|
10f801d31b | ||
|
|
28d19db328 | ||
|
|
0e13bfc50e | ||
|
|
4f4aa22ca1 | ||
|
|
6bec01198d | ||
|
|
5dbceb43b4 | ||
|
|
bc92978fa9 | ||
|
|
893f83b79e | ||
|
|
bde23f6c4d | ||
|
|
e6d4b38c64 | ||
|
|
23264044e6 | ||
|
|
d51a381a79 | ||
|
|
da4459304f | ||
|
|
085699f202 | ||
|
|
2c43aa463a | ||
|
|
41db7b4ad1 | ||
|
|
d0a9fa9a71 | ||
|
|
ed0675aaff | ||
|
|
cad8f58468 | ||
|
|
819d58cf31 | ||
|
|
b951515e3e | ||
|
|
4c3265bca5 | ||
|
|
11672a13f7 | ||
|
|
9a8141a0eb | ||
|
|
5462fb0647 | ||
|
|
6b74b8f770 | ||
|
|
99fff1a1a1 | ||
|
|
645f233b83 | ||
|
|
643e2738bd | ||
|
|
cd356a29ce | ||
|
|
341be982f9 | ||
|
|
2e7565f9eb | ||
|
|
1637a9c938 | ||
|
|
67c2be1858 | ||
|
|
b8d4c01e60 | ||
|
|
e259ab86f7 | ||
|
|
029b3523cb | ||
|
|
f4657ad817 | ||
|
|
5efddff154 | ||
|
|
25efe93507 | ||
|
|
a393e67ef8 | ||
|
|
7f6758cbe9 | ||
|
|
c5644a3321 | ||
|
|
4fec03101f | ||
|
|
10fed0a85c | ||
|
|
fff6316192 | ||
|
|
61623f50b1 | ||
|
|
9f626407cf | ||
|
|
3a51678495 | ||
|
|
c730a0e733 | ||
|
|
80a9971508 | ||
|
|
85788e1d7d | ||
|
|
23680547ea | ||
|
|
9767ead9f3 | ||
|
|
910c9a7f1f | ||
|
|
0bc6c6abb6 | ||
|
|
6361e16dba | ||
|
|
8572cbd761 | ||
|
|
1c0b91c5f4 | ||
|
|
2ebefdd0bf | ||
|
|
29f1384945 | ||
|
|
334e569fda | ||
|
|
2d4170a9bf | ||
|
|
6b2759f97d | ||
|
|
adf4312e5a | ||
|
|
b1707b8691 | ||
|
|
25dff3f2da | ||
|
|
bc88c0dfc2 | ||
|
|
1ca2dfd1b1 | ||
|
|
bdb9274485 | ||
|
|
14f9ac39ad | ||
|
|
d4b52f7de9 | ||
|
|
bdc65ec6f4 | ||
|
|
74966a0bc7 | ||
|
|
7fb31cecd5 | ||
|
|
317c82a9f0 | ||
|
|
9b5c89d235 | ||
|
|
c23e190e8e | ||
|
|
4a73371499 | ||
|
|
6f8af55489 | ||
|
|
2a05f62bf9 | ||
|
|
d6eb1eb76f | ||
|
|
27542f3a51 | ||
|
|
0328e70d5f | ||
|
|
c46932ee30 | ||
|
|
c7408ad8d1 | ||
|
|
05dc8f8f93 | ||
|
|
aea8c285c2 | ||
|
|
a859137971 | ||
|
|
08ebad0fe4 | ||
|
|
d54ebfa0e1 | ||
|
|
304a007d07 | ||
|
|
b6b58c9d95 | ||
|
|
7f0c42cca5 | ||
|
|
9508d46ebd | ||
|
|
bcbf4771ea | ||
|
|
9da5ec25c6 | ||
|
|
028b404377 | ||
|
|
8da0f34ee6 | ||
|
|
cac2628b19 | ||
|
|
9c69530256 | ||
|
|
6e108358d7 | ||
|
|
87f592c67f | ||
|
|
fc95abd587 | ||
|
|
ce3e51b147 | ||
|
|
bb48b23266 | ||
|
|
fdf00e06f5 | ||
|
|
950b246da8 | ||
|
|
6ac0df8448 | ||
|
|
80ae3a18a8 | ||
|
|
972bfb9677 | ||
|
|
b1149e4d6e | ||
|
|
3cd2ba847b | ||
|
|
8bf869654c | ||
|
|
3b98f862bc | ||
|
|
9bc889e274 | ||
|
|
5df924f416 | ||
|
|
5ef94b7079 | ||
|
|
dfcaaf0ae7 | ||
|
|
34e7ff532f | ||
|
|
fbdfcead50 | ||
|
|
b8bbe4431b | ||
|
|
9e31666280 | ||
|
|
5846d9c9b0 | ||
|
|
7707a5b7fc | ||
|
|
18c495940e | ||
|
|
d3155d24c6 | ||
|
|
c0661506c4 | ||
|
|
fa2b34764f | ||
|
|
7a7909b49f | ||
|
|
17d3b3088f | ||
|
|
6a78a178e2 | ||
|
|
749de39e2a | ||
|
|
cd55c0e12e | ||
|
|
d04037739e | ||
|
|
f3a97b9938 | ||
|
|
104fbef37a | ||
|
|
98b3dc71af | ||
|
|
7b28e4d589 | ||
|
|
57291dbe50 | ||
|
|
9f9eefce27 | ||
|
|
af3361b35b | ||
|
|
24d65e1e72 | ||
|
|
29922fca6a | ||
|
|
81319fc48b | ||
|
|
28b7209407 | ||
|
|
0b59f87887 | ||
|
|
b22df9e93d | ||
|
|
1fa72fd34a | ||
|
|
a2f38e7494 | ||
|
|
30a1792c92 | ||
|
|
3d70f24a78 | ||
|
|
b3d14fb5e8 | ||
|
|
59956ca720 | ||
|
|
aaf7690b9a | ||
|
|
9f549dfcd2 | ||
|
|
f191392e3a | ||
|
|
56f26418ea | ||
|
|
118e0e5d5c | ||
|
|
264dcca42b | ||
|
|
73d69f4631 | ||
|
|
485b773338 | ||
|
|
8443e3511d | ||
|
|
b2a349aec5 | ||
|
|
faf8453c6d | ||
|
|
7484c8cce3 | ||
|
|
2b02e7712e | ||
|
|
8a3a7b19aa | ||
|
|
9fa6574c80 | ||
|
|
2bad17010c | ||
|
|
4168385607 | ||
|
|
c0aa1ef375 | ||
|
|
811b79b641 | ||
|
|
dab9755dd4 | ||
|
|
6863479c09 | ||
|
|
e05eebb8b0 | ||
|
|
28ef0e6e89 | ||
|
|
09e65ccac2 | ||
|
|
03e0cfdc15 | ||
|
|
7634cd6c2e | ||
|
|
4594711be4 | ||
|
|
c267933bf7 | ||
|
|
edc6b18b5f | ||
|
|
60657c6e94 | ||
|
|
3c189f9c7f | ||
|
|
bf02c9a453 | ||
|
|
e6b6cd3036 | ||
|
|
6e8589b1f9 | ||
|
|
1354f84a70 | ||
|
|
d07cf92f7c | ||
|
|
e079644ed6 | ||
|
|
6b393178dc | ||
|
|
26e1c9ab69 | ||
|
|
145a17fee5 | ||
|
|
ccc9301ed4 | ||
|
|
f2e46d660f | ||
|
|
8b4676175c | ||
|
|
e9a547c89b | ||
|
|
d07fcceb6a | ||
|
|
6b5002f314 | ||
|
|
5418ffd558 | ||
|
|
ea98afeb68 | ||
|
|
be7d7e26fb | ||
|
|
56377aaf4c | ||
|
|
43601c5e3e | ||
|
|
1db7f3a57d | ||
|
|
e58e99c285 | ||
|
|
b38e25c9f1 | ||
|
|
ce52b8a0ab | ||
|
|
f7800846a5 | ||
|
|
02b75824e5 | ||
|
|
cf52c2f6e4 | ||
|
|
6bc68ba57b | ||
|
|
5ec9cfa537 | ||
|
|
3457cab0b1 | ||
|
|
8349fb6b63 | ||
|
|
33267fa2e4 | ||
|
|
5c4130311a | ||
|
|
ed718f083a | ||
|
|
2aa8ecfc39 | ||
|
|
cdf1d3305b | ||
|
|
fd58bb9e4d | ||
|
|
55ec0b5ff5 | ||
|
|
e5c551f009 | ||
|
|
7666c6c0e1 | ||
|
|
e6ccbfd3b8 | ||
|
|
d8a2e06a95 | ||
|
|
c38fa1c272 | ||
|
|
1a5e23617d | ||
|
|
87916c20bf | ||
|
|
12c223d226 | ||
|
|
9ae2842fa4 | ||
|
|
79853a2fb7 | ||
|
|
ec1e383558 | ||
|
|
df067f01e8 | ||
|
|
4b8cc0683e | ||
|
|
f43b76c323 | ||
|
|
e90b2e384c | ||
|
|
6125822cf7 | ||
|
|
4223c2a79f | ||
|
|
7059cc244a | ||
|
|
5fc608e310 | ||
|
|
29550cbd93 | ||
|
|
35674a205b | ||
|
|
a702596bee | ||
|
|
02fef72e7e | ||
|
|
d5c9e402ce | ||
|
|
b788d398b9 | ||
|
|
5c3b73e3b0 | ||
|
|
82f1de078f | ||
|
|
ddc353023c | ||
|
|
ef75b9ecac | ||
|
|
cf2b3e4fc9 | ||
|
|
cf21991a3f | ||
|
|
9e4f72e2a4 | ||
|
|
f7da243c9e | ||
|
|
a71efd2668 | ||
|
|
6a23290584 | ||
|
|
0e59033ea6 | ||
|
|
186f7a6b82 | ||
|
|
5cf3bba68f | ||
|
|
4647ddc2b7 | ||
|
|
1ac708b0d9 | ||
|
|
07eb062474 | ||
|
|
f660817627 | ||
|
|
0fdf45e847 | ||
|
|
0c3cebd30a | ||
|
|
c589918989 | ||
|
|
256aa0a053 | ||
|
|
67c56e41b0 | ||
|
|
ade9e0f589 | ||
|
|
e14dfb9e7e | ||
|
|
c26cc313fc | ||
|
|
55660e0e69 | ||
|
|
3701544f6a | ||
|
|
acfb081b90 | ||
|
|
a30d7cabe3 | ||
|
|
517d148387 | ||
|
|
f8aec4de73 | ||
|
|
e1b6cf6124 | ||
|
|
1eac00e7fa | ||
|
|
278ed23440 | ||
|
|
b42689a54d | ||
|
|
10bfa30458 | ||
|
|
a0a1cda0a7 | ||
|
|
001af3fabd | ||
|
|
c0316e9446 | ||
|
|
d80a6e7e38 | ||
|
|
8d8d8f9cee | ||
|
|
0630d62ad0 | ||
|
|
c07b282dc9 | ||
|
|
5aa39463b0 | ||
|
|
f1e71247c7 | ||
|
|
419fb2535b | ||
|
|
23697d2a33 | ||
|
|
55edc7ef63 | ||
|
|
c1e3ce64a7 | ||
|
|
d34163402c | ||
|
|
5c5ed102ca | ||
|
|
0bbadfe9a6 | ||
|
|
21ab640686 | ||
|
|
a3f6d200ac | ||
|
|
701c10d618 | ||
|
|
7f8de1f988 | ||
|
|
95ff8ef1f2 | ||
|
|
4eb885f88a | ||
|
|
9a7a0b7271 | ||
|
|
4a8293f881 | ||
|
|
56ed341510 | ||
|
|
7584afda6d | ||
|
|
bbb8f8e51d | ||
|
|
ddd873c7a0 | ||
|
|
63ce45c538 | ||
|
|
8ffc0cb5ff | ||
|
|
ce95907778 | ||
|
|
4ed3250352 | ||
|
|
2dce7a84cd | ||
|
|
b9631659f7 | ||
|
|
405cf3efa3 | ||
|
|
462b3801ee | ||
|
|
7ec42880f0 | ||
|
|
3d6797a305 | ||
|
|
065fe35b6a | ||
|
|
4a4e4135ab | ||
|
|
e388d18aad | ||
|
|
60d9698343 | ||
|
|
a7c9f7aa17 | ||
|
|
7de1b04ebc | ||
|
|
024ec23b7f | ||
|
|
7b2128b356 | ||
|
|
6d54b826e8 | ||
|
|
30c4010512 | ||
|
|
c4c8446fc2 | ||
|
|
c374c87c08 | ||
|
|
8486bb1e87 | ||
|
|
655f44919a | ||
|
|
17ea7978f0 | ||
|
|
b518946c41 | ||
|
|
ca8a4af354 | ||
|
|
1682f8834d | ||
|
|
eac472c54b | ||
|
|
3a873559f8 | ||
|
|
aec6779601 | ||
|
|
973d9eccae | ||
|
|
7468b30001 | ||
|
|
e68cb5f32f | ||
|
|
a157ae5d1c | ||
|
|
620e5ce9dc | ||
|
|
2e3fbf25db | ||
|
|
e8fee653c3 | ||
|
|
0d3f3474d1 | ||
|
|
6ecb6bdaa7 | ||
|
|
738655d380 | ||
|
|
3f0eb227ec | ||
|
|
409a624e33 | ||
|
|
1b02184482 | ||
|
|
f88e6671a2 | ||
|
|
073c297490 | ||
|
|
e92578d41d | ||
|
|
0644aeb70e | ||
|
|
456bda1169 | ||
|
|
4b83a3f861 | ||
|
|
7e18526ed1 | ||
|
|
ffef908c8d | ||
|
|
be5bdbe314 | ||
|
|
6d0f869309 | ||
|
|
bfd0f9d0e4 | ||
|
|
ee9b5a0d25 | ||
|
|
445746fd7f | ||
|
|
ac33d8012b | ||
|
|
8513b2e456 | ||
|
|
26d393f8ee | ||
|
|
bee322cb2e | ||
|
|
bf209bfdd7 | ||
|
|
0e410b6d7a | ||
|
|
2e4e3b1a06 | ||
|
|
7ed26f437d | ||
|
|
e50b9f211d | ||
|
|
e705df6634 | ||
|
|
e669a9fe64 | ||
|
|
6249af041c | ||
|
|
a4a4fc1cf2 | ||
|
|
6acc7c95ff | ||
|
|
3d75e905d1 | ||
|
|
55bc0ed411 | ||
|
|
37142f3bb9 | ||
|
|
c11aa2a194 | ||
|
|
9da7adad95 | ||
|
|
69277d4b24 | ||
|
|
2e157501c3 | ||
|
|
610849ab0b | ||
|
|
b966607070 | ||
|
|
d0c44bded8 | ||
|
|
c79f5be07a | ||
|
|
baab46a3fb | ||
|
|
92beaa3137 | ||
|
|
f03d0ddf30 | ||
|
|
4dea702ff2 | ||
|
|
7e55b83a7b | ||
|
|
259f3776d2 | ||
|
|
2f07eb7097 | ||
|
|
6d6247d449 | ||
|
|
c3f66cce55 | ||
|
|
8ee178dd89 | ||
|
|
d01d7fbcc9 | ||
|
|
252b95f661 | ||
|
|
89f262feae | ||
|
|
7167f53270 | ||
|
|
c989c21d4f | ||
|
|
5d83d4c239 | ||
|
|
94089f9505 | ||
|
|
872dd578c8 | ||
|
|
fba00535fa | ||
|
|
ccd23f6cde | ||
|
|
7a1db4e040 | ||
|
|
7d0a422714 | ||
|
|
e411a4fd8e | ||
|
|
77b57d8233 | ||
|
|
d14005a183 | ||
|
|
0c29fa3cbe | ||
|
|
60bc1d55f4 | ||
|
|
bcadc517c3 | ||
|
|
eb4cb341c6 | ||
|
|
331bc2d1b9 | ||
|
|
b5782c7b86 | ||
|
|
01bb5a930d | ||
|
|
1bb657b073 | ||
|
|
740f39e17c | ||
|
|
e34e4b8404 | ||
|
|
e6fbfa87f0 | ||
|
|
a31de9e1a0 | ||
|
|
a3ac219503 | ||
|
|
5af4a5395b | ||
|
|
d2168aca3d | ||
|
|
6148d96b9a | ||
|
|
b6a6180dc3 | ||
|
|
6edd3c77ec | ||
|
|
36741d7ae5 | ||
|
|
507b196101 | ||
|
|
6fc1cb9127 | ||
|
|
b3ae3871f2 | ||
|
|
1f6b5ed0e3 | ||
|
|
ab5fe6411a | ||
|
|
5dd3209ee8 | ||
|
|
fccefcaa6c | ||
|
|
98249dda8e | ||
|
|
26732d4f6e | ||
|
|
9a6319772e | ||
|
|
2547967113 | ||
|
|
f436525f20 | ||
|
|
7b3f7b8e6b | ||
|
|
a3dece58fb | ||
|
|
a159856628 | ||
|
|
7fb6e2a5d9 | ||
|
|
0cdae9def1 | ||
|
|
63df6cb197 | ||
|
|
55d2d0843a | ||
|
|
7e378be1d8 | ||
|
|
89f86caf69 | ||
|
|
9604598a79 | ||
|
|
90d9805abf | ||
|
|
0deb7ab641 | ||
|
|
230f954571 | ||
|
|
e3f739e5d9 | ||
|
|
6b75bfcb0c | ||
|
|
db47ed5430 | ||
|
|
48361b2047 | ||
|
|
6953cc0670 | ||
|
|
477e1c89e5 | ||
|
|
bae23f9ed8 | ||
|
|
d3625db755 | ||
|
|
d9e2e8f97e | ||
|
|
f1625ad1f5 | ||
|
|
012947c076 | ||
|
|
3b6f6fa80a | ||
|
|
2848b1a38a | ||
|
|
53a0beffee | ||
|
|
4cb99c6014 | ||
|
|
e485168169 | ||
|
|
44a083e095 | ||
|
|
1bd3363708 | ||
|
|
ca87e5c3cf | ||
|
|
2cd702f48d | ||
|
|
97b0c392be | ||
|
|
274f1309c3 | ||
|
|
326f9bed84 | ||
|
|
9f79a8b6ed | ||
|
|
98eda7f49f | ||
|
|
5fe0b737c2 | ||
|
|
c29e92de1c | ||
|
|
e9507d37cf | ||
|
|
3454ab0278 | ||
|
|
cfc9abea99 | ||
|
|
8e68bb6343 | ||
|
|
69ab456ed2 | ||
|
|
441b2b7c0d | ||
|
|
81d3a08b9b | ||
|
|
3ebeac6b30 | ||
|
|
fa2f36b76b | ||
|
|
8b1e1dd92a | ||
|
|
31614e58f2 | ||
|
|
9ff15562d9 | ||
|
|
8c4f432e0c | ||
|
|
d34a886cac | ||
|
|
3ee4d1cace | ||
|
|
ff7729fc35 | ||
|
|
7127862bea | ||
|
|
4a8037047a | ||
|
|
9b3ecd795f | ||
|
|
cc06f515ce | ||
|
|
d57c5f736f | ||
|
|
cf8c82b3fb | ||
|
|
f73bcfd134 | ||
|
|
3e400820e5 | ||
|
|
b6f9eac37e | ||
|
|
7b443b7bfc | ||
|
|
8dafc774d8 | ||
|
|
2fe490ec3d | ||
|
|
d25da91e0b | ||
|
|
1b74fd8d4e | ||
|
|
d796c4d9bb | ||
|
|
8787ce84ac | ||
|
|
33e0c08adc | ||
|
|
a34c2f35a6 | ||
|
|
6903e12112 | ||
|
|
62216e36c1 | ||
|
|
73eb98c57a | ||
|
|
1ee9b2e917 | ||
|
|
8c00adc6b8 | ||
|
|
1c8667eae8 | ||
|
|
f0deed448a | ||
|
|
06d418b12b | ||
|
|
f6f16608dd | ||
|
|
8816c886cf | ||
|
|
6192c19fde | ||
|
|
f463198e40 | ||
|
|
ed907f8c61 | ||
|
|
55d2a5d15c | ||
|
|
6168356ad2 | ||
|
|
71b1a0381f | ||
|
|
640629f69c | ||
|
|
cde5af62cf | ||
|
|
d016e66938 | ||
|
|
5be23f61d0 | ||
|
|
a0c918c646 | ||
|
|
1d910b8a89 | ||
|
|
c042eeb07e | ||
|
|
afd40b21f1 | ||
|
|
f40a00ef0e | ||
|
|
e77be37450 | ||
|
|
15f979f08a | ||
|
|
c8beb6018f | ||
|
|
f934aea2a5 | ||
|
|
3d69c33caf | ||
|
|
2b15a8ff09 | ||
|
|
5212f3beb3 | ||
|
|
4f45ebef84 | ||
|
|
8afdcd7a4c | ||
|
|
8ba18a971e | ||
|
|
c47d443dcf | ||
|
|
b5a13f0206 | ||
|
|
dace6b78c0 | ||
|
|
29c5d6fbb3 | ||
|
|
bebea9efea | ||
|
|
5a5eaec340 | ||
|
|
217cd7bcb6 | ||
|
|
6bf60c496b | ||
|
|
68fb0a81e3 | ||
|
|
779f014fc5 | ||
|
|
4c5da0254a | ||
|
|
fed2e9dd4e | ||
|
|
3e8c2e98e7 | ||
|
|
0db7ab2eaa | ||
|
|
66ccba9267 | ||
|
|
0951c37536 | ||
|
|
1870c256b6 | ||
|
|
117a00c996 | ||
|
|
dd798b4c13 | ||
|
|
b6ceef6775 | ||
|
|
89ce2f825d | ||
|
|
eaf2d3fbb3 | ||
|
|
91eb206ed5 | ||
|
|
eab8d7e2fc | ||
|
|
4d65e214c3 | ||
|
|
4d80dab633 | ||
|
|
8e1a800c31 | ||
|
|
af30a00af2 | ||
|
|
b52a337d36 | ||
|
|
0f4ff6e30e | ||
|
|
ffe7bb6ce5 | ||
|
|
fc33f10e9b | ||
|
|
f8aa487e91 | ||
|
|
77041e0d74 | ||
|
|
1fd2468af6 | ||
|
|
1b8b8d2eef | ||
|
|
d152d8fc1b | ||
|
|
473f8640d0 | ||
|
|
6acdee51b6 | ||
|
|
537ea369bf | ||
|
|
dba265aaef | ||
|
|
97dd5d2064 | ||
|
|
dc0b68ee80 | ||
|
|
93ebae78b8 | ||
|
|
9610d8f883 | ||
|
|
631c250f40 | ||
|
|
5836e50e69 | ||
|
|
0a92e896f8 | ||
|
|
d6f9626a10 | ||
|
|
0ae9865fcb | ||
|
|
0987154a7f | ||
|
|
cb6d487cbd | ||
|
|
55710835fe | ||
|
|
c7b46c86c3 | ||
|
|
4456bc1360 | ||
|
|
41c417fda0 | ||
|
|
345f9ec223 | ||
|
|
8bae45add3 | ||
|
|
6c2e2f271d | ||
|
|
90b0c47c41 | ||
|
|
79e9dadc38 | ||
|
|
404dd0735d | ||
|
|
30458c617e | ||
|
|
e8b9a3a163 | ||
|
|
d68ad47de6 | ||
|
|
10cef2f254 | ||
|
|
8c252163c9 | ||
|
|
c31758960f | ||
|
|
ef32a72b4b | ||
|
|
7dbebed6db | ||
|
|
dda22ddb50 | ||
|
|
42d1b1c725 | ||
|
|
e16e3aac2d | ||
|
|
19237ca7ce | ||
|
|
71eb01e233 | ||
|
|
bb12311e2f | ||
|
|
2f6e34c5f5 | ||
|
|
e241e27ca1 | ||
|
|
bbd9fd838b | ||
|
|
35715ef2a3 | ||
|
|
14de981ac3 | ||
|
|
88b35a6415 | ||
|
|
b55e1bbd40 | ||
|
|
520304d560 | ||
|
|
be1ad7d041 | ||
|
|
f73eab23f9 | ||
|
|
9c2f782f84 | ||
|
|
0b891464b9 | ||
|
|
03439d1bcc | ||
|
|
6e9c8a174f | ||
|
|
ef84d798b5 | ||
|
|
a76f40a4d0 | ||
|
|
537e25df93 | ||
|
|
0764b46a02 | ||
|
|
b0f983dfa9 | ||
|
|
a3c7269d9e | ||
|
|
dc94618c26 | ||
|
|
37d0daee59 | ||
|
|
9225b61225 | ||
|
|
a0a8d0186c | ||
|
|
11dab6680d | ||
|
|
b431600d32 | ||
|
|
fb79ccaa55 | ||
|
|
fa3966e1f7 | ||
|
|
81c81ff4aa | ||
|
|
788406c217 | ||
|
|
49a272d6c0 | ||
|
|
0fcdf47545 | ||
|
|
65ce948bb0 | ||
|
|
5d6014d496 | ||
|
|
6e48988118 | ||
|
|
7d56dca80b | ||
|
|
eb0c9ac1e6 | ||
|
|
752bd501bc | ||
|
|
542ec7b78b | ||
|
|
218855eebf | ||
|
|
f4a449b4ab | ||
|
|
8ce77880bd | ||
|
|
ac70fe2848 | ||
|
|
7ff174cf66 | ||
|
|
a419d831a4 | ||
|
|
7ad04f4535 | ||
|
|
440f553de0 | ||
|
|
c29ff995b3 | ||
|
|
dee71b652d | ||
|
|
7a76cac80c | ||
|
|
3e0ecea0c9 | ||
|
|
60846a4495 | ||
|
|
15eba12e91 | ||
|
|
98e5adfba8 | ||
|
|
422514e907 | ||
|
|
b2c7c7f55b | ||
|
|
82097d622b | ||
|
|
e72019fa55 | ||
|
|
38711f0a29 | ||
|
|
be6635c2a3 | ||
|
|
d298ab7486 | ||
|
|
c912542223 | ||
|
|
bf5d50e8a9 | ||
|
|
4a5cfaf929 | ||
|
|
0ede9d6d93 | ||
|
|
1bf4bf1f30 | ||
|
|
710d522e06 | ||
|
|
ba4f4af29b | ||
|
|
2f25f47d0b | ||
|
|
59c6003fe1 | ||
|
|
4b0cd22bf7 | ||
|
|
3aca142770 | ||
|
|
256990b95c | ||
|
|
537ad1eff3 | ||
|
|
2217a27757 | ||
|
|
55abe2ee75 | ||
|
|
70825d3425 | ||
|
|
7f1bb73b75 | ||
|
|
8b362a4165 | ||
|
|
25eedd187e | ||
|
|
ff9f29d12d | ||
|
|
42968d4c15 | ||
|
|
ca2083c858 | ||
|
|
de0ff4002e | ||
|
|
d0dc206ffc | ||
|
|
02d2ba2b6b | ||
|
|
904211dba5 | ||
|
|
1d374db0f3 | ||
|
|
e0249493d0 | ||
|
|
47f067e38e | ||
|
|
3b36d61698 | ||
|
|
60ecddfce4 | ||
|
|
cc8b771eb4 | ||
|
|
f503b7f517 | ||
|
|
639ac9549f | ||
|
|
839098c884 | ||
|
|
d8495ab08f | ||
|
|
53a314af65 | ||
|
|
2d0c4f5bd8 | ||
|
|
94a3575e2c | ||
|
|
aca661583d | ||
|
|
ca79fcb7e9 | ||
|
|
04bfa0e6ba | ||
|
|
3f8059baad | ||
|
|
b803d5336e | ||
|
|
7b72fcaee1 | ||
|
|
c56bdb84d2 | ||
|
|
ccc0d16b52 | ||
|
|
f38e3624cf | ||
|
|
8586fa8ce0 | ||
|
|
a5444e08ab | ||
|
|
f9e584e13c | ||
|
|
c795dfe96b | ||
|
|
a528a411da | ||
|
|
d96b8942e7 | ||
|
|
a31a615da6 | ||
|
|
e177bf54e2 | ||
|
|
ce55f0d8f4 | ||
|
|
320b76aec4 | ||
|
|
5d86bcce6d | ||
|
|
0b9f23bd4a | ||
|
|
526251397e | ||
|
|
997b9a7c76 | ||
|
|
bfebee5949 | ||
|
|
bd899b2bf2 | ||
|
|
ad511bd137 | ||
|
|
603a65ef17 | ||
|
|
2fa490d83d | ||
|
|
a787bcddf3 | ||
|
|
91a936e07d | ||
|
|
4ce181d172 | ||
|
|
0f5a9c3877 | ||
|
|
545566f566 | ||
|
|
8c0b4f79d8 | ||
|
|
c413a7e860 | ||
|
|
053bf1dfb7 | ||
|
|
146905a459 | ||
|
|
55576ec177 | ||
|
|
cc35dafef5 | ||
|
|
251d49eea9 | ||
|
|
cabfeed9a6 | ||
|
|
b4b90b1c41 | ||
|
|
d337afd5d3 | ||
|
|
4473ca09f6 | ||
|
|
d30192f7f9 | ||
|
|
7291617a77 | ||
|
|
95da8966fe | ||
|
|
db7ffc98fa | ||
|
|
cb43a41b0c | ||
|
|
a7e29b92bf | ||
|
|
4899330b6e | ||
|
|
c56bc0be7f | ||
|
|
fd8ce39990 | ||
|
|
f9ed63ac76 | ||
|
|
641d4c2f7f | ||
|
|
980510d71a | ||
|
|
07001a229d | ||
|
|
e2ec50730f | ||
|
|
20884cf6c7 | ||
|
|
f0118d52e9 | ||
|
|
d4e922eed4 | ||
|
|
b88b27689a | ||
|
|
a29733be9c | ||
|
|
33e0a82967 | ||
|
|
13b5b170b6 | ||
|
|
e2f2090da1 | ||
|
|
eabb6a239d | ||
|
|
2f59e42024 | ||
|
|
20b5e2ab8f | ||
|
|
0d5dd3082c | ||
|
|
5cb712bee4 | ||
|
|
00793c39f8 | ||
|
|
e37220cde6 | ||
|
|
dd20988b1c | ||
|
|
32785376d8 | ||
|
|
4a39bcc6ee | ||
|
|
984a2dcec1 | ||
|
|
c8bdb95b38 | ||
|
|
e89f4d260c | ||
|
|
eca42f32bb | ||
|
|
6ad9770bb6 | ||
|
|
5b47340b76 | ||
|
|
69f54dddaf | ||
|
|
b3cb47ac47 | ||
|
|
69e2ad1007 | ||
|
|
40784b2054 | ||
|
|
ab49e09da6 | ||
|
|
6ed8e54dac | ||
|
|
c80d1aae4c | ||
|
|
a62b3c6ce0 | ||
|
|
8a0568ce01 | ||
|
|
c740474145 | ||
|
|
4a373b45e1 | ||
|
|
32733023ca | ||
|
|
0ffb0fa24f | ||
|
|
b5177e3870 | ||
|
|
c326e58d5e | ||
|
|
4d4046986f | ||
|
|
36cbd3a606 | ||
|
|
715d47dd2c | ||
|
|
71d84f58a4 | ||
|
|
306305a95c | ||
|
|
2bc9fa1629 | ||
|
|
fd45ae2a62 | ||
|
|
46100cfd1d | ||
|
|
1d62322b3c | ||
|
|
d784d2b633 | ||
|
|
07186e12a9 | ||
|
|
0635a663f0 | ||
|
|
dcd8d80b9d | ||
|
|
58ae2b6c68 | ||
|
|
c76a569fb0 | ||
|
|
66e79d2df7 | ||
|
|
fc06ae1000 | ||
|
|
dac7e17500 | ||
|
|
8f51fcd65a | ||
|
|
a4f8faa48e | ||
|
|
b5ab917dfb | ||
|
|
376a30f8a9 | ||
|
|
9ebf4d2426 | ||
|
|
42c14faf41 | ||
|
|
4a658dc02a | ||
|
|
f250cba405 | ||
|
|
cf6e691498 | ||
|
|
5b2e6511b0 | ||
|
|
52a9514536 | ||
|
|
ffd37b9fd9 | ||
|
|
e2b80652a5 | ||
|
|
bd37b88e4d | ||
|
|
a0adc54a08 | ||
|
|
9431b2d530 | ||
|
|
594ef40f2e | ||
|
|
aacdcea37b | ||
|
|
309a456a0d | ||
|
|
f198421f10 | ||
|
|
fa0460b1f0 | ||
|
|
9fb1aeb827 | ||
|
|
59ad67c08c | ||
|
|
ec57618c98 | ||
|
|
3b378961b0 | ||
|
|
cbb1cd78de | ||
|
|
d6834d0287 | ||
|
|
a9a89b501a | ||
|
|
7de4385160 | ||
|
|
7ac5a2bba5 | ||
|
|
e0af5db1a9 | ||
|
|
87cd07dec7 | ||
|
|
e80f8e5121 | ||
|
|
2325a5c532 | ||
|
|
b1db262b37 | ||
|
|
2e564bd076 | ||
|
|
e09e9114aa | ||
|
|
efd91a8f72 | ||
|
|
fbfde81bc8 | ||
|
|
cf62807ad5 | ||
|
|
2d87ba18b3 | ||
|
|
74f1dfde72 | ||
|
|
db4b04d837 | ||
|
|
0b0e16c3ea | ||
|
|
1d16955b6d | ||
|
|
6dfa3beb2b | ||
|
|
5cd875813f | ||
|
|
08a6f084eb | ||
|
|
181f837d8c | ||
|
|
198509e760 | ||
|
|
67cb47173c | ||
|
|
7ed76c180e | ||
|
|
f244a43771 | ||
|
|
48e856fca2 | ||
|
|
7d83cb3d0b | ||
|
|
3af3c9a89d | ||
|
|
2d3bef9033 | ||
|
|
ce9afbcaca | ||
|
|
ba79d5389d | ||
|
|
2f4c40c1db | ||
|
|
6c9e8a4aa8 | ||
|
|
223c38c50d | ||
|
|
c1981dee9b | ||
|
|
45b3373145 | ||
|
|
54a566cfb1 | ||
|
|
5a51c2940a | ||
|
|
7232070bd1 | ||
|
|
33ab8920ca | ||
|
|
db4d47cc52 | ||
|
|
e210c9973b | ||
|
|
98d05ce545 | ||
|
|
9268d80974 | ||
|
|
1a221a6967 | ||
|
|
a039bb1066 | ||
|
|
90a7a61904 | ||
|
|
58e970d685 | ||
|
|
edf0f3c05b | ||
|
|
5bbf5e0dac | ||
|
|
414b8dbf41 | ||
|
|
2e0e1e56b5 | ||
|
|
546dcdf1de | ||
|
|
58f4884ad6 | ||
|
|
95d1625a9f | ||
|
|
05fbdd559f | ||
|
|
300b653ea3 | ||
|
|
1808ea7384 | ||
|
|
4408eae158 | ||
|
|
a68993d099 | ||
|
|
ecfb0f0fdf | ||
|
|
fc3bb962f7 | ||
|
|
23630a4c67 | ||
|
|
b0a7fea724 | ||
|
|
47bbd5b4a8 | ||
|
|
a7400dca45 | ||
|
|
4227377449 | ||
|
|
c1168d181b | ||
|
|
21fb8e9164 | ||
|
|
e569c2957e | ||
|
|
4292302c3c | ||
|
|
f34e4c0e36 | ||
|
|
1790918006 | ||
|
|
400eb81206 | ||
|
|
e07e461e48 | ||
|
|
451e9f7870 | ||
|
|
3c57840340 | ||
|
|
2cbf64da33 | ||
|
|
3ef0a3e685 | ||
|
|
9ebdc82d46 | ||
|
|
fe4b36b85d | ||
|
|
b7ff370561 | ||
|
|
5686c92e05 | ||
|
|
784c5c6b0c | ||
|
|
30541260f0 | ||
|
|
59fc756a0b | ||
|
|
4e346c0ab7 | ||
|
|
19fa12bb26 | ||
|
|
e669b1b9c7 | ||
|
|
f24e4a4ce3 | ||
|
|
8b5430acb8 | ||
|
|
614cb44b55 | ||
|
|
0ce8e8b74d | ||
|
|
4ea53af76e | ||
|
|
016f17d663 | ||
|
|
07f69950a7 | ||
|
|
cec625607d | ||
|
|
a290c7b34b | ||
|
|
5b77ac1b44 | ||
|
|
41c635afb5 | ||
|
|
adbf729cb2 | ||
|
|
88ea716d63 | ||
|
|
003a07ebce | ||
|
|
d3c237bc66 | ||
|
|
bac0fa81fc | ||
|
|
a42915614a | ||
|
|
5ff9bf331f | ||
|
|
36d7b0f8a7 | ||
|
|
67c8a98f20 | ||
|
|
a81a603031 | ||
|
|
e30ce78053 | ||
|
|
3d70de94c6 | ||
|
|
1b6e8797ec | ||
|
|
70975c2944 | ||
|
|
960e2126b4 | ||
|
|
3db1305e58 | ||
|
|
f16780b5e3 | ||
|
|
37a3065f3c | ||
|
|
7ff1649d89 | ||
|
|
a39ae31b45 | ||
|
|
6302a48221 | ||
|
|
c277bee9d2 | ||
|
|
c5f5ce126d | ||
|
|
0004ae716b | ||
|
|
7f53dfac3c | ||
|
|
18003900c2 | ||
|
|
fe104791b5 | ||
|
|
6244657ca5 | ||
|
|
46866dac85 | ||
|
|
c829c073cf | ||
|
|
0772ca4036 | ||
|
|
581733d605 | ||
|
|
b02af27ac3 | ||
|
|
20af0a2ef5 | ||
|
|
c58ab5b07d | ||
|
|
e5e711c938 | ||
|
|
42594abe4e | ||
|
|
0afbdc95b4 | ||
|
|
16a74bb748 | ||
|
|
8711b476be | ||
|
|
df9fba4765 | ||
|
|
7d27139aa9 | ||
|
|
e4638d5fbc | ||
|
|
bc5854dd29 | ||
|
|
ba49d4d140 | ||
|
|
71b7dcffb5 | ||
|
|
7713e613b4 | ||
|
|
91a0aa7387 | ||
|
|
fbd6b920ef | ||
|
|
1b25d1d572 | ||
|
|
e93e72234c | ||
|
|
15d9b0ae3a | ||
|
|
550d87ac6c | ||
|
|
f8e576e890 | ||
|
|
fb8ef4d978 | ||
|
|
5127e87898 | ||
|
|
3f2d4444e4 | ||
|
|
9c7badb0eb | ||
|
|
9d9f77a731 | ||
|
|
c400ce74b1 | ||
|
|
c2e20ee4a3 | ||
|
|
ccd9034339 | ||
|
|
0a1a075f31 | ||
|
|
4aaeec9515 | ||
|
|
ea310d7508 | ||
|
|
0a2e6a0c38 | ||
|
|
f26d582018 | ||
|
|
e9a727301d | ||
|
|
d64d2f9c42 | ||
|
|
5c0d122e84 | ||
|
|
5079c853a7 | ||
|
|
b039ba12a2 | ||
|
|
3323ac6ac1 | ||
|
|
3204311ac1 | ||
|
|
0fc2ad97cd | ||
|
|
30620d0ca4 | ||
|
|
bccc22c5fe | ||
|
|
ecf2418347 | ||
|
|
0c99cb3103 | ||
|
|
034dc08269 | ||
|
|
d1a51b42f6 | ||
|
|
92bfbb2d0c | ||
|
|
91b846e2cd | ||
|
|
7fe7fb4c15 | ||
|
|
0cebd8aa4d | ||
|
|
8662c96d1c | ||
|
|
0cbc9402f3 | ||
|
|
940df02456 | ||
|
|
b4b598f542 | ||
|
|
ef19c35b5a | ||
|
|
fc98120269 | ||
|
|
b8a3d6deaf | ||
|
|
45537ede87 | ||
|
|
29a9c5bc7b | ||
|
|
7ca81285b1 | ||
|
|
49a865cdbf | ||
|
|
a0c30c35ed | ||
|
|
de20424885 | ||
|
|
f7e09ae89c | ||
|
|
c6d4600683 | ||
|
|
bd1837ee36 | ||
|
|
544b24ceb1 | ||
|
|
0825374183 | ||
|
|
b053fb8e61 | ||
|
|
ae11e80bde | ||
|
|
8e296231ba | ||
|
|
49891eff36 | ||
|
|
58df525b49 | ||
|
|
1761f43afa | ||
|
|
37d7d938c5 | ||
|
|
b7ca2310b2 | ||
|
|
c562b3969a | ||
|
|
d1d553e8d7 | ||
|
|
b6e7b258e0 | ||
|
|
c7bbe47221 | ||
|
|
347fa9e5cd | ||
|
|
07ce151508 | ||
|
|
665c9b5e52 | ||
|
|
9399a0c545 | ||
|
|
a540b12895 | ||
|
|
e29d9dcd17 | ||
|
|
1aa0d84977 | ||
|
|
7f31d7c812 | ||
|
|
4987a95d8e | ||
|
|
ef7771febb | ||
|
|
12cba0e148 | ||
|
|
c3a4052227 | ||
|
|
82f048ccef | ||
|
|
7a585a3dfb | ||
|
|
8d3b53f51d | ||
|
|
d73e006935 | ||
|
|
9536e60bd1 | ||
|
|
678ca978a3 | ||
|
|
39420877fd | ||
|
|
6ea03cfba3 | ||
|
|
9214b56aea | ||
|
|
699b4c464f | ||
|
|
9fa54a3148 | ||
|
|
f2019b1059 | ||
|
|
714bbd0fb0 | ||
|
|
80777b4663 | ||
|
|
9473c1fe41 | ||
|
|
98f141d62f | ||
|
|
85dd213b14 | ||
|
|
3cf00911f7 | ||
|
|
bddaad8346 | ||
|
|
5df4efd7ba | ||
|
|
59df6aad05 | ||
|
|
c4af4d03ac | ||
|
|
62679819d9 | ||
|
|
46d46e313c | ||
|
|
27e9d3ce47 | ||
|
|
b6b0c5fe6d | ||
|
|
87b934a955 | ||
|
|
8cc6e9b812 | ||
|
|
b7da17ff31 | ||
|
|
2dd3916f7e | ||
|
|
516552cce6 | ||
|
|
2d44881619 | ||
|
|
0acbf30b03 | ||
|
|
e61f6b1c89 | ||
|
|
973a49526f | ||
|
|
e1902d58c1 | ||
|
|
1e53125499 | ||
|
|
91fb7d9e70 | ||
|
|
eb6b42c4c9 | ||
|
|
679d210667 | ||
|
|
1e6252de7f | ||
|
|
48b645ee1e | ||
|
|
951d2e4937 | ||
|
|
1658883b78 | ||
|
|
3514335247 | ||
|
|
55bec31a3f | ||
|
|
0d36abee4e | ||
|
|
caa6e615ff | ||
|
|
cd3576b995 | ||
|
|
324f3f7794 | ||
|
|
3257110673 | ||
|
|
66b444e2b0 | ||
|
|
23860b1ee8 | ||
|
|
101048339b | ||
|
|
dc78e3b7a0 | ||
|
|
d965faa317 | ||
|
|
5d2bfab0f5 | ||
|
|
841a6eaf8c | ||
|
|
db59bb4aa4 | ||
|
|
61f7099106 | ||
|
|
ef828bdd38 | ||
|
|
1134b45b05 | ||
|
|
b06daff4c7 | ||
|
|
cea414b589 | ||
|
|
b8942b728f | ||
|
|
8e6eabd9e8 | ||
|
|
290dd6c4d1 | ||
|
|
088bc16072 | ||
|
|
daad2fbd71 | ||
|
|
3af94c2a90 | ||
|
|
a3ca76d3c4 | ||
|
|
62ede48196 | ||
|
|
390a86a7a7 | ||
|
|
09631d6b0c | ||
|
|
2947238a02 | ||
|
|
a7af4b4809 | ||
|
|
cb6afe67a7 | ||
|
|
8c5b43295d | ||
|
|
79b94824ef | ||
|
|
33e4b046e8 | ||
|
|
386aea7c78 | ||
|
|
4a7bccd983 | ||
|
|
1f0cae9e76 | ||
|
|
87ae085e6d | ||
|
|
640ac2330f | ||
|
|
2543df9425 | ||
|
|
915ab47a72 | ||
|
|
09ff287da2 | ||
|
|
2896180f80 | ||
|
|
4283b5b0e3 | ||
|
|
bbbd3abf06 | ||
|
|
dd88483ec7 | ||
|
|
00ddec7575 | ||
|
|
ab0ebab240 | ||
|
|
79e83e33ec | ||
|
|
aa402d652d | ||
|
|
690481c138 | ||
|
|
881125aa98 | ||
|
|
77eea4d494 | ||
|
|
b26e16abb8 | ||
|
|
f08c7702ee | ||
|
|
a4399c7ef4 | ||
|
|
d6e50ed9a0 | ||
|
|
6b848b318d | ||
|
|
70ce70cf0e | ||
|
|
5a79bc5ee3 | ||
|
|
5792a86959 | ||
|
|
37c5436087 | ||
|
|
6592102e8f | ||
|
|
06a5a8f70d | ||
|
|
ef54ebada6 | ||
|
|
d4f13de1d9 | ||
|
|
4fcedde529 | ||
|
|
a4518bbefc | ||
|
|
95f771aa26 | ||
|
|
da98942cce | ||
|
|
f3efaf59e1 | ||
|
|
d64032f2a3 | ||
|
|
abad8cc4d5 | ||
|
|
0d9536e2f9 | ||
|
|
67b078b805 | ||
|
|
6f02eeae53 | ||
|
|
5bc03b23ea | ||
|
|
3d4acd8c8f | ||
|
|
448bec8181 | ||
|
|
0a34ee1b64 | ||
|
|
34e8e4d4c3 | ||
|
|
32627c03f4 | ||
|
|
63c314ca18 | ||
|
|
e8453783da | ||
|
|
96522ec3a3 | ||
|
|
17dedab391 | ||
|
|
283d2ee09c | ||
|
|
289ff0127e | ||
|
|
931d7217b1 | ||
|
|
3149a3927e | ||
|
|
c57bced7b1 | ||
|
|
8d794a59dc | ||
|
|
7bb1e24bda | ||
|
|
e0013b9b63 | ||
|
|
7d81aab900 | ||
|
|
cc99da5357 | ||
|
|
9bd21e1d1b | ||
|
|
2148aeea42 | ||
|
|
6a7a5505f9 | ||
|
|
5a6faafa30 | ||
|
|
e2f3dad779 | ||
|
|
ae2aa1f5cd | ||
|
|
0e3a17d922 | ||
|
|
033919a270 | ||
|
|
f5d40a0a12 | ||
|
|
0fd781e80a | ||
|
|
a8f6170fdf | ||
|
|
bd8c565415 | ||
|
|
ffb02fe0ec | ||
|
|
114520302c | ||
|
|
317138ab72 | ||
|
|
a990109f43 | ||
|
|
da68b01502 | ||
|
|
e90bc744d9 | ||
|
|
2b5c56484a | ||
|
|
ea30612013 | ||
|
|
4445612bb8 | ||
|
|
da56393078 | ||
|
|
a86ff1e8d0 | ||
|
|
7fce5650b8 | ||
|
|
b949357e66 | ||
|
|
8e71b68dbf | ||
|
|
b77eed221f | ||
|
|
1fa91474d0 | ||
|
|
fc32a89292 | ||
|
|
ea24010298 | ||
|
|
ae1f80a52c | ||
|
|
2f95431c9b | ||
|
|
d59683eff1 | ||
|
|
de77776cd0 | ||
|
|
74ccfea570 | ||
|
|
1f5a549589 | ||
|
|
3ccdc2e307 | ||
|
|
688b725807 | ||
|
|
ae01ea576c |
No files matched your search
@@ -251,7 +251,7 @@ gosu wekan:wekan /home/wekan/.meteor/meteor -- help
|
||||
# Build app (Development)
|
||||
cd /home/wekan/app
|
||||
gosu wekan:wekan /home/wekan/.meteor/meteor add standard-minifier-js
|
||||
gosu wekan:wekan /home/wekan/.meteor/meteor npm install
|
||||
gosu wekan:wekan /home/wekan/.meteor/meteor npm install --no-audit
|
||||
|
||||
# Put back the original tar
|
||||
mv $(which tar)~ $(which tar)
|
||||
|
||||
+38
-18
@@ -20,7 +20,10 @@
|
||||
"strict": 0,
|
||||
"no-undef": 0,
|
||||
"accessor-pairs": 2,
|
||||
"comma-dangle": [2, "always-multiline"],
|
||||
"comma-dangle": [
|
||||
2,
|
||||
"always-multiline"
|
||||
],
|
||||
"consistent-return": 2,
|
||||
"dot-notation": 2,
|
||||
"eqeqeq": 2,
|
||||
@@ -28,15 +31,28 @@
|
||||
"no-cond-assign": 2,
|
||||
"no-constant-condition": 2,
|
||||
"no-eval": 2,
|
||||
"no-inner-declarations": [0],
|
||||
"no-inner-declarations": [
|
||||
0
|
||||
],
|
||||
"no-unneeded-ternary": 2,
|
||||
"radix": 2,
|
||||
"semi": [2, "always"],
|
||||
"camelcase": [2, { "properties": "never" }],
|
||||
"semi": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"camelcase": [
|
||||
2,
|
||||
{
|
||||
"properties": "never"
|
||||
}
|
||||
],
|
||||
"comma-spacing": 2,
|
||||
"comma-style": 2,
|
||||
"eol-last": 2,
|
||||
"linebreak-style": [2, "unix"],
|
||||
"linebreak-style": [
|
||||
2,
|
||||
"unix"
|
||||
],
|
||||
"meteor/audit-argument-checks": 0,
|
||||
"new-parens": 2,
|
||||
"no-lonely-if": 2,
|
||||
@@ -45,7 +61,13 @@
|
||||
"no-spaced-func": 2,
|
||||
"no-trailing-spaces": 2,
|
||||
"operator-linebreak": 2,
|
||||
"quotes": [2, "single", { "avoidEscape": true }],
|
||||
"quotes": [
|
||||
2,
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"semi-spacing": 2,
|
||||
"space-unary-ops": 2,
|
||||
"arrow-spacing": 2,
|
||||
@@ -57,6 +79,7 @@
|
||||
"prefer-spread": 2,
|
||||
"prefer-template": 2,
|
||||
"no-unused-vars": "warn",
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
@@ -72,11 +95,18 @@
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"meteor": {
|
||||
"extensions": [".js", ".jsx"]
|
||||
"extensions": [
|
||||
".js",
|
||||
".jsx"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": ["prettier", "meteor"],
|
||||
"plugins": [
|
||||
"prettier",
|
||||
"meteor",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"globals": {
|
||||
"Meteor": false,
|
||||
"Session": false,
|
||||
@@ -93,29 +123,22 @@
|
||||
"Template": false,
|
||||
"$": false,
|
||||
"_": false,
|
||||
"autosize": false,
|
||||
"Avatar": true,
|
||||
"Avatars": true,
|
||||
"BlazeComponent": false,
|
||||
"BlazeLayout": false,
|
||||
"CollectionHooks": false,
|
||||
"DocHead": false,
|
||||
"ESSearchResults": false,
|
||||
"FastRender": false,
|
||||
"FlowRouter": false,
|
||||
"FS": false,
|
||||
"getSlug": false,
|
||||
"Migrations": false,
|
||||
"moment": false,
|
||||
"Mousetrap": false,
|
||||
"Picker": false,
|
||||
"Presence": true,
|
||||
"presences": true,
|
||||
"Ps": true,
|
||||
"ReactiveTabs": false,
|
||||
"Restivus": false,
|
||||
"SimpleSchema": false,
|
||||
"SubsManager": false,
|
||||
"T9n": false,
|
||||
"TAPi18n": false,
|
||||
"AccountsTemplates": true,
|
||||
@@ -133,13 +156,11 @@
|
||||
"CSSEvents": true,
|
||||
"EscapeActions": true,
|
||||
"Filter": true,
|
||||
"Mixins": true,
|
||||
"Modal": true,
|
||||
"MultiSelection": true,
|
||||
"Popup": true,
|
||||
"Sidebar": true,
|
||||
"Utils": true,
|
||||
"InlinedForm": true,
|
||||
"UnsavedEdits": true,
|
||||
"Notifications": true,
|
||||
"allowIsBoardAdmin": true,
|
||||
@@ -152,7 +173,6 @@
|
||||
"Checklists": true,
|
||||
"Settings": true,
|
||||
"InvitationCodes": true,
|
||||
"JsonRoutes": true,
|
||||
"Authentication": true,
|
||||
"Integrations": true,
|
||||
"HTTP": true,
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
* text=auto eol=lf
|
||||
*.{cmd,[cC][mM][dD]} text eol=crlf
|
||||
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||
.gitea/CODEOWNERS linguist-detectable=false
|
||||
@@ -0,0 +1,3 @@
|
||||
# Pull requests request review from the eligible staff team(s).
|
||||
# One approval from either an Admin or Developer satisfies branch protection.
|
||||
* @trueog/Admins @trueog/Developers
|
||||
@@ -1,3 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: wekan
|
||||
custom: ['https://wekan.fi/commercial-support/']
|
||||
@@ -1,56 +0,0 @@
|
||||
## Issue
|
||||
|
||||
UPGRADE: https://wekan.fi/upgrade/
|
||||
|
||||
Pull requests welcome to fix any broken links at docs directory, and organizing docs/Features and their screenshots to subdirectories of each feature.
|
||||
|
||||
Please report these issues elsewhere:
|
||||
|
||||
- SECURITY ISSUES, PGP EMAIL: https://github.com/wekan/wekan/blob/main/SECURITY.md
|
||||
- UCS: https://github.com/wekan/univention/issues
|
||||
|
||||
If WeKan Snap is slow, try this: https://github.com/wekan/wekan/wiki/Cron
|
||||
|
||||
Please search existing Open and Closed issues, most questions have already been answered.
|
||||
|
||||
If you can not login for any reason: https://github.com/wekan/wekan/wiki/Forgot-Password
|
||||
Email settings, only SMTP MAIL_URL and MAIL_FROM are in use:
|
||||
https://github.com/wekan/wekan/wiki/Troubleshooting-Mail
|
||||
|
||||
### Server Setup Information
|
||||
|
||||
Please anonymize info, and do not any of your Wekan board URLs, passwords,
|
||||
API tokens etc to this public issue.
|
||||
|
||||
* Did you test in newest Wekan?:
|
||||
* Did you configure root-url correctly so Wekan cards open correctly (see https://github.com/wekan/wekan/wiki/Settings)?
|
||||
* Operating System:
|
||||
* Deployment Method (Snap/Docker/Sandstorm/bundle/source):
|
||||
* Http frontend if any (Caddy, Nginx, Apache, see config examples from Wekan GitHub wiki first):
|
||||
* Node.js Version:
|
||||
* MongoDB Version:
|
||||
* What webbrowser version are you using (Wekan should work on all modern browsers that support Javascript)?
|
||||
|
||||
### Problem description
|
||||
|
||||
Add a recorded animated gif (e.g. with https://github.com/phw/peek) about
|
||||
how it works currently, and screenshot mockups how it should work.
|
||||
|
||||
|
||||
#### Reproduction Steps
|
||||
|
||||
|
||||
|
||||
#### Logs
|
||||
|
||||
Check Right Click / Inspect / Console in you browser - generally Chromium
|
||||
based browsers show more detailed info than Firefox based browsers.
|
||||
|
||||
Please anonymize logs.
|
||||
|
||||
Snap: sudo snap logs wekan.wekan
|
||||
|
||||
Docker: sudo docker logs wekan-app
|
||||
|
||||
If logs are very long, attach them in .zip file
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
name: 🐛 Bug Report
|
||||
description: Report a bug to help improve WeKan
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for reporting a bug! Please ensure you are using the [latest version of WeKan](https://wekan.fi/install) and [Upgrade](https://wekan.fi/upgrade) before submitting.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Bug Description
|
||||
description: A clear and concise description of what the bug is. Mention versions of WeKan, Node.js, database name and version, frontend webserver version like Caddy etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: How can we recreate this issue?
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant Logs
|
||||
description: |
|
||||
- Please paste any relevant anonymized server logs or browser console errors here.
|
||||
- Snap: sudo snap logs wekan.wekan
|
||||
- Docker: sudo docker logs wekan-app
|
||||
- If logs are very long, attach them in .zip file
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context, anonymized screenshots or GIF video about the bug, and screenshot mockups about how it should work.
|
||||
@@ -0,0 +1,31 @@
|
||||
name: ✨ Feature Request
|
||||
description: Suggest a new feature for WeKan
|
||||
labels: ["Feature:Request"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Problem Statement
|
||||
description: Is your feature request related to a problem? Please describe.
|
||||
placeholder: I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: Any alternative solutions or features you've considered.
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context, like anonymized screenshot mockups about how it should work.
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
name: 🛡️ Security Issue
|
||||
description: Report a security vulnerability
|
||||
labels: ["security", "critical"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## ⚠️ IMPORTANT: Please do not report security vulnerabilities via public issues.
|
||||
|
||||
To protect the WeKan community, we ask that you report security bugs privately. This allows us to fix the issue before it can be exploited by malicious actors.
|
||||
|
||||
### How to report:
|
||||
Please read our **[Security Policy (SECURITY.md)](https://github.com/wekan/wekan/blob/main/SECURITY.md)** for the official reporting process and contact information.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
name: 🗳️ Univention (UCS) Issue
|
||||
description: Problems specifically related to the WeKan app on Univention Corporate Server
|
||||
labels: ["UCS"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 🛑 Is this a UCS-specific issue?
|
||||
|
||||
If your issue is related to the **Univention Corporate Server (UCS) integration**, packaging, or installation via the Univention App Center, it should be reported in the dedicated Univention repository.
|
||||
|
||||
### ➡️ [Report UCS Issues Here](https://github.com/wekan/univention/issues)
|
||||
|
||||
---
|
||||
**Why?**
|
||||
Reporting there ensures that the maintainers specifically focused on the UCS environment see your request.
|
||||
|
||||
If you are certain this is a **core WeKan bug** that affects all platforms (Docker, Snap, etc.), please go back and use the standard [Bug Report](https://github.com/wekan/wekan/issues/new?template=bug-report.yml) template.
|
||||
- type: textarea
|
||||
id: ucs-details
|
||||
attributes:
|
||||
label: Brief Description (Optional)
|
||||
description: If you still wish to post here, please provide a brief summary of why this is a core Wekan issue and not a UCS-specific integration bug.
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
uses: actions/dependency-review-action@v5
|
||||
@@ -32,13 +32,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -48,14 +48,14 @@ jobs:
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
@@ -15,6 +15,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Build the Docker image
|
||||
run: docker build . --file Dockerfile --tag wekan:$(date +%s)
|
||||
@@ -0,0 +1,251 @@
|
||||
name: Playwright E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: playwright-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
name: Meteor unit tests
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node 24
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Cache Meteor installation
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.meteor
|
||||
key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
|
||||
restore-keys: meteor-
|
||||
|
||||
- name: Cache Meteor build (.meteor/local)
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .meteor/local
|
||||
key: meteor-local-unit-${{ hashFiles('.meteor/versions') }}
|
||||
restore-keys: meteor-local-unit-
|
||||
|
||||
- name: Install Meteor
|
||||
run: |
|
||||
if [ ! -x ~/.meteor/meteor ]; then
|
||||
curl https://install.meteor.com/ | sh
|
||||
fi
|
||||
echo "$HOME/.meteor" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run Meteor unit tests
|
||||
# First Meteor build is slow; allow time before the test driver starts.
|
||||
timeout-minutes: 45
|
||||
run: meteor test --once --driver-package meteortesting:mocha
|
||||
env:
|
||||
TEST_WATCH: '0'
|
||||
|
||||
e2e-tests:
|
||||
name: Playwright E2E (${{ matrix.project }})
|
||||
runs-on: ubuntu-24.04
|
||||
needs: unit-tests
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
project: [chromium, firefox]
|
||||
# webkit excluded from CI (requires macOS runner) — run locally with npm run test:e2e:all
|
||||
|
||||
services:
|
||||
mongodb:
|
||||
image: mongo:7
|
||||
ports: ['27017:27017']
|
||||
options: >-
|
||||
--health-cmd "mongosh --eval 'db.adminCommand(\"ping\")'"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node 24
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Cache Meteor installation
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.meteor
|
||||
key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
|
||||
restore-keys: meteor-
|
||||
|
||||
- name: Cache Meteor build (.meteor/local)
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .meteor/local
|
||||
key: meteor-local-e2e-${{ hashFiles('.meteor/versions') }}
|
||||
restore-keys: meteor-local-e2e-
|
||||
|
||||
- name: Install Meteor
|
||||
run: |
|
||||
if [ ! -x ~/.meteor/meteor ]; then
|
||||
curl https://install.meteor.com/ | sh
|
||||
fi
|
||||
echo "$HOME/.meteor" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install app npm dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright test dependencies
|
||||
run: npm ci
|
||||
working-directory: tests/playwright
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps ${{ matrix.project }}
|
||||
working-directory: tests/playwright
|
||||
|
||||
- name: Start WeKan in background
|
||||
run: |
|
||||
MONGO_URL=mongodb://localhost:27017/wekan_test \
|
||||
ROOT_URL=http://localhost:3000 \
|
||||
PORT=3000 \
|
||||
meteor run --settings settings.json > wekan-run.log 2>&1 &
|
||||
# The first Meteor build is slow, so wait up to 20 minutes for the
|
||||
# server to start responding before giving up.
|
||||
for i in $(seq 1 120); do
|
||||
if curl -sf http://localhost:3000 > /dev/null; then
|
||||
echo "WeKan is up after $((i * 10))s"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for WeKan... attempt $i/120"
|
||||
sleep 10
|
||||
done
|
||||
if ! curl -sf http://localhost:3000 > /dev/null; then
|
||||
echo "WeKan failed to start. Last 200 log lines:"
|
||||
tail -n 200 wekan-run.log
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
DISABLE_CAPTCHA: 'true'
|
||||
|
||||
- name: Run Playwright tests (${{ matrix.project }})
|
||||
run: |
|
||||
npx playwright test \
|
||||
--project=${{ matrix.project }} \
|
||||
--reporter=github
|
||||
working-directory: tests/playwright
|
||||
env:
|
||||
WEKAN_BASE_URL: http://localhost:3000
|
||||
WEKAN_MONGO_URL: mongodb://localhost:27017/wekan_test
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: playwright-report-${{ matrix.project }}
|
||||
path: tests/playwright/playwright-report/
|
||||
retention-days: 14
|
||||
|
||||
- name: Upload test results (screenshots / videos)
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-results-${{ matrix.project }}
|
||||
path: tests/playwright/test-results/
|
||||
retention-days: 7
|
||||
|
||||
e2e-regression:
|
||||
name: Puppeteer regression suite
|
||||
runs-on: ubuntu-24.04
|
||||
needs: unit-tests
|
||||
|
||||
services:
|
||||
mongodb:
|
||||
image: mongo:7
|
||||
ports: ['27017:27017']
|
||||
options: >-
|
||||
--health-cmd "mongosh --eval 'db.adminCommand(\"ping\")'"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node 24
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Cache Meteor installation
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.meteor
|
||||
key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
|
||||
restore-keys: meteor-
|
||||
|
||||
- name: Cache Meteor build (.meteor/local)
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .meteor/local
|
||||
key: meteor-local-regression-${{ hashFiles('.meteor/versions') }}
|
||||
restore-keys: meteor-local-regression-
|
||||
|
||||
- name: Install Meteor
|
||||
run: |
|
||||
if [ ! -x ~/.meteor/meteor ]; then
|
||||
curl https://install.meteor.com/ | sh
|
||||
fi
|
||||
echo "$HOME/.meteor" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Chromium
|
||||
run: sudo apt-get install -y chromium-browser
|
||||
|
||||
- name: Start WeKan in background
|
||||
run: |
|
||||
MONGO_URL=mongodb://localhost:27017/wekan_regression \
|
||||
ROOT_URL=http://localhost:3000 \
|
||||
PORT=3000 \
|
||||
meteor run > wekan-run.log 2>&1 &
|
||||
# The first Meteor build is slow, so wait up to 20 minutes.
|
||||
for i in $(seq 1 120); do
|
||||
if curl -sf http://localhost:3000 > /dev/null; then
|
||||
echo "WeKan is up after $((i * 10))s"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for WeKan... attempt $i/120"
|
||||
sleep 10
|
||||
done
|
||||
if ! curl -sf http://localhost:3000 > /dev/null; then
|
||||
echo "WeKan failed to start. Last 200 log lines:"
|
||||
tail -n 200 wekan-run.log
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
DISABLE_CAPTCHA: 'true'
|
||||
|
||||
- name: Run list-regressions e2e
|
||||
run: node tests/e2e/list-regressions.js
|
||||
env:
|
||||
WEKAN_BASE_URL: http://localhost:3000
|
||||
WEKAN_MONGO_URL: mongodb://localhost:27017/wekan_regression
|
||||
CHROMIUM_PATH: /usr/bin/chromium-browser
|
||||
HEADLESS: 'true'
|
||||
@@ -0,0 +1,633 @@
|
||||
name: Release All Platforms
|
||||
|
||||
# Triggered manually via GitHub CLI or the Actions UI.
|
||||
# Usage:
|
||||
# gh workflow run release-all.yml -f old_version=9.35 -f new_version=9.36
|
||||
#
|
||||
# Or use the convenience script (bumps versions, commits, pushes, then triggers):
|
||||
# ./releases/release-all.sh 9.35 9.36
|
||||
#
|
||||
# Builds only the platforms MongoDB ships server binaries for (amd64, arm64,
|
||||
# win64, mac); MongoDB 7.x is the default database. s390x/ppc64le are not built
|
||||
# because MongoDB has no server binaries for them.
|
||||
#
|
||||
# This workflow does EVERYTHING for a release remotely (nothing is built or
|
||||
# pushed on the maintainer's machine), running jobs in parallel where possible:
|
||||
# - bump: runs releases/version.sh + releases/rebuild-docs.sh on the runner
|
||||
# (bumps Node/MongoDB/WeKan versions, rebuilds API docs), then
|
||||
# commits and pushes the bump to wekan/wekan main
|
||||
# - prepare: tags the bumped main as v<new_version> and extracts release notes
|
||||
# - website: updates the wekan/wekan.fi repo (install page + API docs) and pushes
|
||||
# - charts: updates and publishes the wekan/charts Helm chart and pushes
|
||||
# - build-*: builds the amd64 bundle (full Meteor 3 build), then rebuilds native
|
||||
# modules for arm64, win64, mac-arm64 from that amd64 base
|
||||
# - release: creates the GitHub Release with the newest CHANGELOG.md notes and
|
||||
# uploads every wekan-<version>-<platform>.zip bundle
|
||||
# - docker: builds and pushes the multi-arch image to Docker Hub, Quay.io, GHCR
|
||||
# - snap: builds the snap on Launchpad (amd64 + arm64), pushes it to the Snap
|
||||
# Store and attaches the .snap files to the GitHub Release
|
||||
#
|
||||
# Required GitHub Secrets:
|
||||
# GHCR_AUTH - base64("username:token") for ghcr.io
|
||||
# DOCKERHUB_AUTH - base64("username:token") for Docker Hub
|
||||
# QUAY_AUTH - base64("username:token") for Quay.io
|
||||
# SNAP_AUTH - Snap Store credentials (output of `snapcraft export-login`)
|
||||
# LP_CREDENTIALS - base64 of the Launchpad credentials file used by
|
||||
# `snapcraft remote-build` (see releases/snap-build.sh notes)
|
||||
# WEKAN_REPO_TOKEN - Personal Access Token with write access to wekan/wekan.fi
|
||||
# and wekan/charts, used by the website + charts jobs to push
|
||||
# to those repos (the built-in GITHUB_TOKEN only covers
|
||||
# wekan/wekan). See releases/create-github-secrets.sh.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
old_version:
|
||||
description: 'Previous version number (e.g. 9.35, without v prefix)'
|
||||
required: true
|
||||
type: string
|
||||
new_version:
|
||||
description: 'New version number (e.g. 9.36, without v prefix)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
# Node.js major version that Meteor 3.5-beta bundles. Used for native module
|
||||
# rebuilds on every platform — it must match the amd64 build's Node ABI.
|
||||
env:
|
||||
NODE_VERSION: '24'
|
||||
|
||||
jobs:
|
||||
|
||||
# ─── 0. Bump versions + rebuild API docs, commit to main ────────────────────
|
||||
# Runs releases/version.sh (probes upstream for the newest Node.js/MongoDB,
|
||||
# bumps every version reference) and releases/rebuild-docs.sh (regenerates
|
||||
# public/api/wekan.yml + wekan.html), then commits and pushes to main so the
|
||||
# tag, builds, website and charts jobs all see the bumped sources.
|
||||
bump:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install version-bump and docs build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3 python3-pip curl wget
|
||||
|
||||
- name: Bump versions and rebuild API docs
|
||||
env:
|
||||
# Builds fetch dependencies from upstream themselves, so skip the slow
|
||||
# pre-download of Node/MongoDB tarballs that the local flow does.
|
||||
RELEASE_SKIP_DEP_DOWNLOAD: '1'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
./releases/version.sh "${{ inputs.old_version }}" "${{ inputs.new_version }}"
|
||||
./releases/rebuild-docs.sh "${{ inputs.new_version }}"
|
||||
|
||||
- name: Commit and push version bump
|
||||
run: |
|
||||
git add --all
|
||||
if git diff --cached --quiet; then
|
||||
echo "No version changes to commit."
|
||||
else
|
||||
git commit -m "Bump versions for v${{ inputs.new_version }}"
|
||||
git push origin main
|
||||
fi
|
||||
|
||||
# ─── 1. Tag the release and extract release notes ───────────────────────────
|
||||
prepare:
|
||||
needs: bump
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
changelog: ${{ steps.changelog.outputs.content }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Normalise version from input
|
||||
id: version
|
||||
run: |
|
||||
VERSION="${{ inputs.new_version }}"
|
||||
VERSION="${VERSION#v}"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create and push tag v${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
|
||||
echo "Tag v$VERSION already exists, leaving it as is."
|
||||
else
|
||||
git tag -a "v$VERSION" -m "v$VERSION"
|
||||
git push origin "v$VERSION"
|
||||
fi
|
||||
|
||||
- name: Extract release notes from CHANGELOG.md
|
||||
id: changelog
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
# Prefer a version-specific "# v<version> ..." section, otherwise fall
|
||||
# back to the topmost "# Upcoming/Upcomig WeKan ® release" section.
|
||||
NOTES=$(python3 - <<PYEOF
|
||||
import re, sys
|
||||
version = "$VERSION"
|
||||
with open("CHANGELOG.md", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
patterns = [
|
||||
rf"(# v{re.escape(version)} .*?)(?=\n# v[0-9]|\Z)",
|
||||
r"(# Upcom\w* WeKan.*?)(?=\n# v[0-9]|\Z)",
|
||||
]
|
||||
for pat in patterns:
|
||||
m = re.search(pat, content, re.DOTALL)
|
||||
if m:
|
||||
print(m.group(1).strip())
|
||||
sys.exit(0)
|
||||
print(f"WeKan v{version}")
|
||||
PYEOF
|
||||
)
|
||||
{
|
||||
echo "content<<GHEOF"
|
||||
echo "$NOTES"
|
||||
echo "GHEOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ─── 1b. Update the wekan.fi website repo (parallel) ────────────────────────
|
||||
# Bumps the version/Meteor/Node/MongoDB strings on the install page and copies
|
||||
# the freshly rebuilt API docs into wekan.fi/api/v<new>, then commits + pushes
|
||||
# to wekan/wekan.fi. Runs as soon as `bump` has pushed the bumped sources.
|
||||
website:
|
||||
needs: bump
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout wekan repo (bumped main, with rebuilt API docs)
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
path: wekan
|
||||
|
||||
- name: Checkout wekan.fi website repo
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: wekan/wekan.fi
|
||||
token: ${{ secrets.WEKAN_REPO_TOKEN }}
|
||||
path: wekan.fi
|
||||
|
||||
- name: Update and push website
|
||||
env:
|
||||
WEBDIR: ${{ github.workspace }}/wekan.fi
|
||||
WEKANREPODIR: ${{ github.workspace }}/wekan
|
||||
RELEASE_PUSH_WEBSITE: '1'
|
||||
run: |
|
||||
cd wekan.fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
cd ..
|
||||
./wekan/releases/release-website.sh "${{ inputs.old_version }}" "${{ inputs.new_version }}"
|
||||
|
||||
# ─── 1c. Update and publish the Helm chart repo (parallel) ──────────────────
|
||||
# Bumps the WeKan version in wekan/charts, packages the chart and publishes it
|
||||
# to the gh-pages Helm index, then pushes both branches. Runs in parallel with
|
||||
# the platform builds; packaging the chart does not need the built artifacts.
|
||||
charts:
|
||||
needs: bump
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout wekan repo (for releases/release-charts.sh + version refs)
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
path: wekan
|
||||
|
||||
- name: Checkout wekan/charts repo (main + gh-pages)
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: wekan/charts
|
||||
token: ${{ secrets.WEKAN_REPO_TOKEN }}
|
||||
fetch-depth: 0
|
||||
path: charts
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v4
|
||||
|
||||
- name: Fetch gh-pages branch (release-charts.sh switches to it)
|
||||
run: |
|
||||
cd charts
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git fetch origin '+refs/heads/*:refs/remotes/origin/*'
|
||||
git branch --track gh-pages origin/gh-pages 2>/dev/null || true
|
||||
|
||||
- name: Release and publish chart
|
||||
env:
|
||||
CHARTS_DIR: ${{ github.workspace }}/charts
|
||||
run: |
|
||||
./wekan/releases/release-charts.sh "${{ inputs.new_version }}"
|
||||
|
||||
# ─── 2. Build amd64 bundle (full Meteor 3 build) ────────────────────────────
|
||||
# Meteor 3.5-beta installer supports only Linux x86_64/aarch64, macOS, Windows.
|
||||
# The full Meteor build always runs here on x86_64; every other platform then
|
||||
# only rebuilds the native npm modules from this bundle.
|
||||
build-amd64:
|
||||
needs: prepare
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 180
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: refs/tags/v${{ needs.prepare.outputs.version }}
|
||||
|
||||
- name: Cache Meteor installation
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.meteor
|
||||
key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
|
||||
restore-keys: meteor-
|
||||
|
||||
- name: Cache npm packages
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential g++ make python3 curl wget zip unzip
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install Meteor
|
||||
run: |
|
||||
if [ ! -f ~/.meteor/meteor ]; then
|
||||
curl https://install.meteor.com/ | sh
|
||||
fi
|
||||
echo "$HOME/.meteor" >> $GITHUB_PATH
|
||||
|
||||
- name: Build WeKan bundle
|
||||
run: |
|
||||
rm -rf .build node_modules .meteor/local
|
||||
(meteor update --npm 2>/dev/null || true) && meteor npm install
|
||||
meteor build .build --directory
|
||||
|
||||
- name: Install server npm modules
|
||||
run: |
|
||||
cd .build/bundle/programs/server
|
||||
npm install
|
||||
|
||||
- name: Create amd64 zip
|
||||
run: |
|
||||
cd .build
|
||||
zip -r wekan-${VERSION}-amd64.zip bundle
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bundle-amd64
|
||||
path: .build/wekan-${{ needs.prepare.outputs.version }}-amd64.zip
|
||||
retention-days: 1
|
||||
|
||||
# ─── 3. Build arm64 bundle (rebuild native modules natively) ─────────────────
|
||||
build-arm64:
|
||||
needs: [prepare, build-amd64]
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential g++ make python3 curl unzip zip
|
||||
|
||||
- name: Download amd64 bundle
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: bundle-amd64
|
||||
|
||||
- name: Rebuild native modules for arm64 in container
|
||||
run: |
|
||||
unzip wekan-${VERSION}-amd64.zip
|
||||
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
|
||||
# which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
|
||||
BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
|
||||
if [ -d "$BCRYPT_DIR" ]; then
|
||||
rm -rf "$BCRYPT_DIR"
|
||||
TMP=$(mktemp -d)
|
||||
pushd "$TMP" > /dev/null
|
||||
npm pack bcrypt@6.0.0
|
||||
mkdir bcrypt && tar xzf bcrypt-*.tgz --strip-components=1 -C bcrypt
|
||||
cd bcrypt && npm install --ignore-scripts && cd ..
|
||||
cp -a bcrypt "$BCRYPT_DIR"
|
||||
popd > /dev/null
|
||||
rm -rf "$TMP"
|
||||
fi
|
||||
docker run --rm --platform linux/arm64 \
|
||||
-v "$(pwd)/bundle:/bundle" \
|
||||
-w /bundle/programs/server \
|
||||
node:${NODE_VERSION}-slim \
|
||||
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
|
||||
|
||||
- name: Create arm64 zip
|
||||
run: |
|
||||
zip -r wekan-${VERSION}-arm64.zip bundle
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bundle-arm64
|
||||
path: wekan-${{ needs.prepare.outputs.version }}-arm64.zip
|
||||
retention-days: 1
|
||||
|
||||
# ─── 5. Build win64 bundle (rebuild native modules on Windows) ───────────────
|
||||
build-win64:
|
||||
needs: [prepare, build-amd64]
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
# Checked out into src/ so start-wekan.bat and snapcraft.yaml (which pins
|
||||
# the MongoDB tools/mongosh versions) are available; the downloaded bundle
|
||||
# is extracted alongside it in the workspace root.
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: refs/tags/v${{ needs.prepare.outputs.version }}
|
||||
path: src
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install 7-zip
|
||||
run: choco install -y 7zip
|
||||
|
||||
- name: Download amd64 bundle
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: bundle-amd64
|
||||
|
||||
- name: Fix bcrypt for Node.js 24
|
||||
shell: bash
|
||||
run: |
|
||||
7z x wekan-${VERSION}-amd64.zip
|
||||
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
|
||||
# which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
|
||||
BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
|
||||
if [ -d "$BCRYPT_DIR" ]; then
|
||||
rm -rf "$BCRYPT_DIR"
|
||||
TMP=$(mktemp -d)
|
||||
pushd "$TMP" > /dev/null
|
||||
npm pack bcrypt@6.0.0
|
||||
mkdir bcrypt && tar xzf bcrypt-*.tgz --strip-components=1 -C bcrypt
|
||||
cd bcrypt && npm install --ignore-scripts && cd ..
|
||||
cp -r bcrypt "$BCRYPT_DIR"
|
||||
popd > /dev/null
|
||||
rm -rf "$TMP"
|
||||
fi
|
||||
|
||||
- name: Rebuild native modules for Windows
|
||||
shell: bash
|
||||
run: |
|
||||
cd bundle/programs/server
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
|
||||
- name: Add MongoDB tools, mongosh and start-wekan.bat to the bundle
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Use the same MongoDB tools/mongosh versions as the snap, which
|
||||
# releases/version.sh keeps at the newest release for each bundle.
|
||||
TOOLS_VER=$(grep -oE 'mongodb-database-tools-ubuntu2204-\$\{TOOLS_ARCH\}-[0-9]+\.[0-9]+\.[0-9]+' src/snapcraft.yaml \
|
||||
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
MONGOSH_VER=$(grep -oE 'mongosh-[0-9]+\.[0-9]+\.[0-9]+-linux' src/snapcraft.yaml \
|
||||
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
echo "MongoDB Database Tools: $TOOLS_VER mongosh: $MONGOSH_VER"
|
||||
|
||||
# MongoDB Database Tools (Windows): bsondump, mongodump, mongoexport,
|
||||
# mongofiles, mongoimport, mongorestore, mongostat, mongotop.
|
||||
TOOLS_DIR="mongodb-database-tools-windows-x86_64-${TOOLS_VER}"
|
||||
curl -fL -o tools.zip \
|
||||
"https://fastdl.mongodb.org/tools/db/${TOOLS_DIR}.zip"
|
||||
7z x -y tools.zip > /dev/null
|
||||
cp "${TOOLS_DIR}/bin/"*.exe bundle/
|
||||
|
||||
# mongosh (Windows): mongosh.exe + mongosh_crypt_v1.dll.
|
||||
MONGOSH_DIR="mongosh-${MONGOSH_VER}-win32-x64"
|
||||
curl -fL -o mongosh.zip \
|
||||
"https://downloads.mongodb.com/compass/${MONGOSH_DIR}.zip"
|
||||
7z x -y mongosh.zip > /dev/null
|
||||
cp "${MONGOSH_DIR}/bin/mongosh.exe" bundle/
|
||||
cp "${MONGOSH_DIR}/bin/mongosh_crypt_v1.dll" bundle/
|
||||
|
||||
# Windows launcher from the repo root.
|
||||
cp src/start-wekan.bat bundle/
|
||||
|
||||
echo "=== bundle contents ==="
|
||||
ls -1 bundle/
|
||||
|
||||
- name: Create win64 zip
|
||||
shell: cmd
|
||||
run: 7z a wekan-%VERSION%-win64.zip bundle
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bundle-win64
|
||||
path: wekan-${{ needs.prepare.outputs.version }}-win64.zip
|
||||
retention-days: 1
|
||||
|
||||
# ─── 6. Build mac-arm64 bundle (rebuild native modules on Apple Silicon) ─────
|
||||
build-mac-arm64:
|
||||
needs: [prepare, build-amd64]
|
||||
runs-on: macos-14 # macos-14 runners are Apple Silicon (arm64)
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Download amd64 bundle
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: bundle-amd64
|
||||
|
||||
- name: Rebuild native modules for macOS arm64
|
||||
run: |
|
||||
unzip -q wekan-${VERSION}-amd64.zip
|
||||
# Fix: bcrypt 5.0.1 (Meteor accounts-password) ships node-addon-api@3
|
||||
# which fails to compile on Node.js 24. Replace with bcrypt 6.0.0.
|
||||
BCRYPT_DIR="$(pwd)/bundle/programs/server/npm/node_modules/meteor/accounts-password/node_modules/bcrypt"
|
||||
if [ -d "$BCRYPT_DIR" ]; then
|
||||
rm -rf "$BCRYPT_DIR"
|
||||
TMP=$(mktemp -d)
|
||||
pushd "$TMP" > /dev/null
|
||||
npm pack bcrypt@6.0.0
|
||||
mkdir bcrypt && tar xzf bcrypt-*.tgz --strip-components=1 -C bcrypt
|
||||
cd bcrypt && npm install --ignore-scripts && cd ..
|
||||
cp -a bcrypt "$BCRYPT_DIR"
|
||||
popd > /dev/null
|
||||
rm -rf "$TMP"
|
||||
fi
|
||||
cd bundle/programs/server
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
cd ../../..
|
||||
|
||||
- name: Create mac-arm64 zip
|
||||
run: zip -r wekan-${VERSION}-mac-arm64.zip bundle
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bundle-mac-arm64
|
||||
path: wekan-${{ needs.prepare.outputs.version }}-mac-arm64.zip
|
||||
retention-days: 1
|
||||
|
||||
# ─── 7. Create GitHub Release and upload all bundles ────────────────────────
|
||||
release:
|
||||
needs:
|
||||
- prepare
|
||||
- build-amd64
|
||||
- build-arm64
|
||||
- build-win64
|
||||
- build-mac-arm64
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- name: Download all bundles
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: bundle-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Create GitHub Release with all bundles
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: v${{ needs.prepare.outputs.version }}
|
||||
name: v${{ needs.prepare.outputs.version }}
|
||||
body: ${{ needs.prepare.outputs.changelog }}
|
||||
files: |
|
||||
wekan-${{ needs.prepare.outputs.version }}-amd64.zip
|
||||
wekan-${{ needs.prepare.outputs.version }}-arm64.zip
|
||||
wekan-${{ needs.prepare.outputs.version }}-win64.zip
|
||||
wekan-${{ needs.prepare.outputs.version }}-mac-arm64.zip
|
||||
|
||||
# ─── 8. Build and push the multi-arch Docker image to all registries ────────
|
||||
# Mirrors releases/docker-build.sh step 3 (multi-arch buildx --push).
|
||||
# Needs `release` because the Dockerfile downloads wekan-<version>-<arch>.zip
|
||||
# from the GitHub Release; building before those assets exist 404s and (without
|
||||
# set -e in the Dockerfile) shipped a broken image missing /build/main.js.
|
||||
docker:
|
||||
needs: [prepare, release]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: refs/tags/v${{ needs.prepare.outputs.version }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub, Quay.io and GHCR
|
||||
env:
|
||||
DOCKERHUB_AUTH: ${{ secrets.DOCKERHUB_AUTH }}
|
||||
QUAY_AUTH: ${{ secrets.QUAY_AUTH }}
|
||||
GHCR_AUTH: ${{ secrets.GHCR_AUTH }}
|
||||
run: |
|
||||
login() {
|
||||
# $1 = registry host, $2 = base64("user:token")
|
||||
local host="$1" creds user token
|
||||
creds="$(printf '%s' "$2" | base64 -d)"
|
||||
user="${creds%%:*}"
|
||||
token="${creds#*:}"
|
||||
printf '%s' "$token" | docker login "$host" -u "$user" --password-stdin
|
||||
}
|
||||
login docker.io "$DOCKERHUB_AUTH"
|
||||
login quay.io "$QUAY_AUTH"
|
||||
login ghcr.io "$GHCR_AUTH"
|
||||
|
||||
- name: Build and push multi-arch image
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-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 \
|
||||
.
|
||||
|
||||
# ─── 9. Build the snap on Launchpad, push to the store, attach to release ────
|
||||
# The snap's `wekan` part downloads wekan-<version>-<arch>.zip from the GitHub
|
||||
# Release, so this job runs only after `release` has published those assets.
|
||||
# `snapcraft remote-build` builds every platform listed in snapcraft.yaml
|
||||
# (amd64 + arm64) on Launchpad.
|
||||
snap:
|
||||
needs: [prepare, release]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: refs/tags/v${{ needs.prepare.outputs.version }}
|
||||
# remote-build (Launchpad) rejects shallow clones, so fetch full history
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install snapcraft
|
||||
run: sudo snap install snapcraft --classic
|
||||
|
||||
- name: Provide Launchpad credentials for remote-build
|
||||
env:
|
||||
LP_CREDENTIALS: ${{ secrets.LP_CREDENTIALS }}
|
||||
run: |
|
||||
mkdir -p ~/.local/share/snapcraft
|
||||
printf '%s' "$LP_CREDENTIALS" | base64 -d > ~/.local/share/snapcraft/launchpad-credentials
|
||||
chmod 600 ~/.local/share/snapcraft/launchpad-credentials
|
||||
|
||||
- name: Build snaps on Launchpad (amd64 + arm64)
|
||||
run: |
|
||||
snapcraft remote-build --launchpad-accept-public-upload
|
||||
|
||||
- name: Push snaps to the Snap Store (edge + beta channels)
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_AUTH }}
|
||||
run: |
|
||||
for f in wekan_${VERSION}_*.snap; do
|
||||
echo "Uploading $f to the Snap Store (edge,beta)..."
|
||||
snapcraft upload --release=edge,beta "$f"
|
||||
done
|
||||
|
||||
- name: Attach snaps to the GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release upload "v${VERSION}" wekan_${VERSION}_*.snap --clobber
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
name: Test suite
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
# the following are optional jobs and need to be configured according
|
||||
# to this project's settings:
|
||||
#
|
||||
# lintcode:
|
||||
# name: Javascript lint
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v5
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
# with:
|
||||
# node-version: '12.x'
|
||||
#
|
||||
# - name: cache dependencies
|
||||
# uses: actions/cache@v1
|
||||
# with:
|
||||
# path: ~/.npm
|
||||
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-node-
|
||||
#
|
||||
# - run: npm install
|
||||
# - run: npm run lint:code
|
||||
#
|
||||
# lintstyle:
|
||||
# name: SCSS lint
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: [lintcode]
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v5
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
# with:
|
||||
# node-version: '12.x'
|
||||
#
|
||||
# - name: cache dependencies
|
||||
# uses: actions/cache@v1
|
||||
# with:
|
||||
# path: ~/.npm
|
||||
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-node-
|
||||
# - run: npm install
|
||||
# - run: npm run lint:style
|
||||
#
|
||||
# lintdocs:
|
||||
# name: documentation lint
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: [lintcode,lintstyle]
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v5
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
# with:
|
||||
# node-version: '12.x'
|
||||
#
|
||||
# - name: cache dependencies
|
||||
# uses: actions/cache@v1
|
||||
# with:
|
||||
# path: ~/.npm
|
||||
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-node-
|
||||
#
|
||||
# - run: npm install
|
||||
# - run: npm run lint:markdown
|
||||
|
||||
tests:
|
||||
name: Meteor ${{ matrix.meteor }} tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
# CHECKOUTS
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# CACHING
|
||||
- name: Install Meteor
|
||||
id: cache-meteor-install
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.meteor
|
||||
key: v1-meteor-${{ hashFiles('.meteor/versions') }}
|
||||
restore-keys: |
|
||||
v1-meteor-
|
||||
|
||||
- name: Cache NPM dependencies
|
||||
id: cache-meteor-npm
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: v1-npm-${{ hashFiles('package-lock.json') }}
|
||||
restore-keys: |
|
||||
v1-npm-
|
||||
|
||||
- name: Cache Meteor build
|
||||
id: cache-meteor-build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.meteor/local/resolver-result-cache.json
|
||||
.meteor/local/plugin-cache
|
||||
.meteor/local/isopacks
|
||||
.meteor/local/bundler-cache/scanner
|
||||
key: v1-meteor_build_cache-${{ github.ref }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
v1-meteor_build_cache-
|
||||
|
||||
- name: Setup meteor
|
||||
uses: meteorengineer/setup-meteor@v2
|
||||
with:
|
||||
meteor-release: '2.2'
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: meteor npm ci
|
||||
|
||||
- name: Run Tests
|
||||
run: sh ./test-wekan.sh -cv
|
||||
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-folder
|
||||
path: .coverage/
|
||||
|
||||
coverage:
|
||||
name: Coverage report
|
||||
runs-on: ubuntu-latest
|
||||
needs: [tests]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Download coverage
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: coverage-folder
|
||||
path: .coverage/
|
||||
|
||||
|
||||
- name: Coverage Report
|
||||
uses: VeryGoodOpenSource/very_good_coverage@v3.0.0
|
||||
with:
|
||||
path: ".coverage/lcov.info"
|
||||
min_coverage: 1 # TODO add tests and increase to 95!
|
||||
+25
@@ -1,4 +1,5 @@
|
||||
*~
|
||||
/*.log
|
||||
*.sw*
|
||||
.meteor-spk
|
||||
*.sublime-workspace
|
||||
@@ -35,7 +36,31 @@ ehthumbs.db
|
||||
.devcontainer/volumes*/
|
||||
.coverage
|
||||
|
||||
# Rspack build artifacts
|
||||
_build/
|
||||
public/build-assets/
|
||||
public/build-chunks/
|
||||
private/build-assets/
|
||||
private/build-chunks/
|
||||
public/client-rspack.js.map
|
||||
|
||||
# Helm chart
|
||||
# Chart dependencies
|
||||
/helm/wekan/**/*.tgz
|
||||
/helm/wekan/charts
|
||||
|
||||
# Meteor Modern-Tools build context directories
|
||||
_build
|
||||
*/build-assets
|
||||
*/build-chunks
|
||||
.rsdoctor
|
||||
|
||||
# Tests
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
# Claude AI
|
||||
.claude/
|
||||
|
||||
# Tools like mongosh etc so that they work inside Flatpak sandbox
|
||||
.tools/
|
||||
+36
-67
@@ -3,32 +3,29 @@
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
meteor-base@1.5.1
|
||||
meteor-base@1.5.2
|
||||
|
||||
# Build system
|
||||
ecmascript@0.16.8
|
||||
standard-minifier-js@2.8.1
|
||||
mquandalle:jade
|
||||
coffeescript@2.4.1!
|
||||
ecmascript@0.18.0
|
||||
standard-minifier-js@3.2.0
|
||||
rspack@1.1.0
|
||||
|
||||
# Polyfills
|
||||
es5-shim@4.8.0
|
||||
blaze@3.0.2
|
||||
|
||||
# Collections
|
||||
aldeed:collection2
|
||||
cottz:publish-relations
|
||||
dburles:collection-helpers
|
||||
idmontie:migrations
|
||||
easy:search
|
||||
mongo@1.16.8
|
||||
mquandalle:collection-mutations
|
||||
aldeed:schema-index@4.0.0-beta.5
|
||||
reywood:publish-composite
|
||||
mongo@2.4.0-rc350.1
|
||||
|
||||
# Account system
|
||||
accounts-password@2.4.0
|
||||
useraccounts:core
|
||||
useraccounts:flow-routing
|
||||
useraccounts:unstyled
|
||||
simple:rest-accounts-password
|
||||
accounts-password@3.3.0-rc350.1
|
||||
accounts-express@1.0.0-rc350.1
|
||||
communitypackages:core
|
||||
communitypackages:unstyled
|
||||
communitypackages:flow-routing
|
||||
# Routing
|
||||
ostrio:flow-router-extra
|
||||
wekan-ldap
|
||||
wekan-accounts-cas
|
||||
wekan-accounts-sandstorm
|
||||
@@ -37,60 +34,32 @@ wekan-oidc
|
||||
wekan-accounts-oidc
|
||||
|
||||
# Utilities
|
||||
check@1.3.2
|
||||
jquery@3.0.0!
|
||||
random@1.2.1
|
||||
reactive-dict@1.3.1
|
||||
session@1.2.1
|
||||
tracker@1.3.3
|
||||
underscore@1.0.13
|
||||
arillo:flow-router-helpers
|
||||
audit-argument-checks@1.0.7
|
||||
kadira:dochead
|
||||
mquandalle:autofocus
|
||||
ongoworks:speakingurl
|
||||
raix:handlebar-helpers
|
||||
http@2.0.0! # force new http package
|
||||
|
||||
# Datepicker
|
||||
wekan-bootstrap-datepicker
|
||||
check@1.5.0
|
||||
random@1.2.2
|
||||
reactive-dict@1.3.2
|
||||
session@1.2.3-rc350.1
|
||||
tracker@1.3.4
|
||||
audit-argument-checks@1.0.8
|
||||
|
||||
# UI components
|
||||
ostrio:i18n
|
||||
reactive-var@1.0.12
|
||||
mousetrap:mousetrap
|
||||
mquandalle:jquery-textcomplete
|
||||
mquandalle:mousetrap-bindglobal
|
||||
templates:tabs
|
||||
meteor-autosize
|
||||
shell-server@0.5.0
|
||||
email@2.2.5
|
||||
dynamic-import@0.7.3
|
||||
msavin:usercache
|
||||
# Keep stylus in 1.1.0, because building v2 takes extra 52 minutes.
|
||||
meteorhacks:subs-manager
|
||||
meteorhacks:aggregate@1.3.0
|
||||
reactive-var@1.0.13
|
||||
shell-server@0.7.0
|
||||
email@3.2.0-rc350.1
|
||||
dynamic-import@0.7.4
|
||||
wekan-markdown
|
||||
konecty:mongo-counter
|
||||
percolate:synced-cron
|
||||
quave:synced-cron
|
||||
ostrio:cookies
|
||||
ostrio:files@2.3.0
|
||||
pascoual:pdfkit
|
||||
lmieulet:meteor-coverage
|
||||
meteortesting:mocha@2.0.3
|
||||
aldeed:simple-schema
|
||||
ostrio:files
|
||||
# lmieulet:meteor-coverage
|
||||
meteortesting:mocha@3.0.0
|
||||
matb33:collection-hooks
|
||||
simple:json-routes
|
||||
kadira:flow-router
|
||||
spacebars
|
||||
service-configuration@1.3.2
|
||||
communitypackages:picker
|
||||
minifier-css@1.6.4
|
||||
blaze
|
||||
kadira:blaze-layout
|
||||
peerlibrary:blaze-components
|
||||
ejson@1.1.3
|
||||
logging@1.3.3
|
||||
service-configuration@1.3.5
|
||||
minifier-css@2.0.2-rc350.1
|
||||
ejson@1.2.0-rc350.1
|
||||
logging@1.3.6
|
||||
wekan-fullcalendar
|
||||
momentjs:moment@2.29.3
|
||||
wekan-fontawesome
|
||||
jam:offline
|
||||
jam:method
|
||||
jam:pub-sub
|
||||
+1
-1
@@ -1 +1 @@
|
||||
METEOR@2.14
|
||||
METEOR@3.5-rc.1
|
||||
+116
-161
@@ -1,166 +1,121 @@
|
||||
accounts-base@2.2.10
|
||||
accounts-oauth@1.4.3
|
||||
accounts-password@2.4.0
|
||||
aldeed:collection2@2.10.0
|
||||
aldeed:collection2-core@1.2.0
|
||||
aldeed:schema-deny@1.1.0
|
||||
aldeed:schema-index@1.1.1
|
||||
aldeed:simple-schema@1.5.4
|
||||
allow-deny@1.1.1
|
||||
arillo:flow-router-helpers@0.5.2
|
||||
audit-argument-checks@1.0.7
|
||||
autoupdate@1.8.0
|
||||
babel-compiler@7.10.5
|
||||
babel-runtime@1.5.1
|
||||
base64@1.0.12
|
||||
binary-heap@1.0.11
|
||||
blaze@2.7.1
|
||||
blaze-tools@1.1.3
|
||||
boilerplate-generator@1.7.2
|
||||
caching-compiler@1.2.2
|
||||
caching-html-compiler@1.2.1
|
||||
callback-hook@1.5.1
|
||||
check@1.3.2
|
||||
coffeescript@2.7.0
|
||||
coffeescript-compiler@2.4.1
|
||||
communitypackages:picker@1.1.1
|
||||
cottz:publish-relations@2.0.8
|
||||
dburles:collection-helpers@1.1.0
|
||||
ddp@1.4.1
|
||||
ddp-client@2.6.1
|
||||
ddp-common@1.4.0
|
||||
ddp-rate-limiter@1.2.1
|
||||
ddp-server@2.7.0
|
||||
deps@1.0.12
|
||||
diff-sequence@1.1.2
|
||||
dynamic-import@0.7.3
|
||||
easy:search@2.2.1
|
||||
easysearch:components@2.2.2
|
||||
easysearch:core@2.2.2
|
||||
ecmascript@0.16.8
|
||||
ecmascript-runtime@0.8.1
|
||||
ecmascript-runtime-client@0.12.1
|
||||
ecmascript-runtime-server@0.11.0
|
||||
ejson@1.1.3
|
||||
email@2.2.5
|
||||
es5-shim@4.8.0
|
||||
fetch@0.1.4
|
||||
geojson-utils@1.0.11
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.1.3
|
||||
htmljs@1.1.1
|
||||
http@2.0.0
|
||||
id-map@1.1.1
|
||||
idmontie:migrations@1.0.3
|
||||
inter-process-messaging@0.1.1
|
||||
jquery@3.0.0
|
||||
kadira:blaze-layout@2.3.0
|
||||
kadira:dochead@1.5.0
|
||||
kadira:flow-router@2.12.1
|
||||
konecty:mongo-counter@0.0.5_3
|
||||
lmieulet:meteor-coverage@1.1.4
|
||||
localstorage@1.2.0
|
||||
logging@1.3.3
|
||||
matb33:collection-hooks@1.3.0
|
||||
mdg:validation-error@0.5.1
|
||||
meteor@1.11.5
|
||||
meteor-autosize@5.0.1
|
||||
meteor-base@1.5.1
|
||||
meteorhacks:aggregate@1.3.0
|
||||
meteorhacks:collection-utils@1.2.0
|
||||
meteorhacks:picker@1.0.3
|
||||
meteorhacks:subs-manager@1.6.4
|
||||
meteortesting:browser-tests@1.4.2
|
||||
meteortesting:mocha@2.1.0
|
||||
meteortesting:mocha-core@8.0.1
|
||||
minifier-css@1.6.4
|
||||
minifier-js@2.7.5
|
||||
minifiers@1.1.8-faster-rebuild.0
|
||||
minimongo@1.9.3
|
||||
modern-browsers@0.1.10
|
||||
modules@0.20.0
|
||||
modules-runtime@0.13.1
|
||||
momentjs:moment@2.29.3
|
||||
mongo@1.16.8
|
||||
mongo-decimal@0.1.3
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.8
|
||||
mongo-livedata@1.0.12
|
||||
mousetrap:mousetrap@1.4.6_1
|
||||
mquandalle:autofocus@1.0.0
|
||||
mquandalle:collection-mutations@0.1.0
|
||||
mquandalle:jade@0.4.9
|
||||
mquandalle:jade-compiler@0.4.5
|
||||
mquandalle:jquery-textcomplete@0.8.0_1
|
||||
mquandalle:mousetrap-bindglobal@0.0.1
|
||||
msavin:usercache@1.8.0
|
||||
npm-mongo@4.17.2
|
||||
oauth@2.2.1
|
||||
oauth2@1.3.2
|
||||
observe-sequence@1.0.21
|
||||
ongoworks:speakingurl@1.1.0
|
||||
ordered-dict@1.1.0
|
||||
ostrio:cookies@2.7.2
|
||||
accounts-base@3.3.0-rc350.1
|
||||
accounts-express@1.0.0-rc350.1
|
||||
accounts-oauth@1.4.7-rc350.1
|
||||
accounts-password@3.3.0-rc350.1
|
||||
aldeed:collection2@4.2.0
|
||||
aldeed:schema-index@4.0.0
|
||||
aldeed:simple-schema@1.13.1
|
||||
allow-deny@2.1.0
|
||||
audit-argument-checks@1.0.8
|
||||
autoupdate@2.0.1
|
||||
babel-compiler@7.14.0
|
||||
babel-runtime@1.5.2
|
||||
base64@1.0.14-rc350.1
|
||||
binary-heap@1.0.13-rc350.1
|
||||
blaze@3.0.3
|
||||
blaze-tools@2.0.1
|
||||
boilerplate-generator@2.1.0
|
||||
caching-compiler@2.0.1
|
||||
caching-html-compiler@2.0.1
|
||||
callback-hook@1.8.0-rc350.1
|
||||
check@1.5.0
|
||||
communitypackages:core@2.0.3
|
||||
communitypackages:flow-routing@2.1.0
|
||||
communitypackages:unstyled@2.0.1
|
||||
core-runtime@1.0.0
|
||||
ddp@1.4.2
|
||||
ddp-client@3.3.0-rc350.1
|
||||
ddp-common@1.4.5-rc350.1
|
||||
ddp-rate-limiter@1.3.0-rc350.1
|
||||
ddp-server@3.3.0-rc350.1
|
||||
diff-sequence@1.1.3
|
||||
dynamic-import@0.7.4
|
||||
ecmascript@0.18.0
|
||||
ecmascript-runtime@0.8.3
|
||||
ecmascript-runtime-client@0.13.0
|
||||
ecmascript-runtime-server@0.11.1
|
||||
ejson@1.2.0-rc350.1
|
||||
email@3.2.0-rc350.1
|
||||
es5-shim@4.8.1
|
||||
facts-base@1.0.2
|
||||
fetch@0.2.0-rc350.1
|
||||
geojson-utils@1.0.12
|
||||
hot-code-push@1.0.5
|
||||
html-tools@2.0.1
|
||||
htmljs@2.0.2
|
||||
id-map@1.2.0
|
||||
inter-process-messaging@0.1.2
|
||||
jam:method@1.9.1
|
||||
jam:offline@0.4.1
|
||||
jam:pub-sub@0.5.1
|
||||
jquery@3.0.2
|
||||
lai:collection-extensions@1.0.0
|
||||
localstorage@1.2.1
|
||||
logging@1.3.6
|
||||
matb33:collection-hooks@2.1.0
|
||||
meteor@2.3.1-rc350.1
|
||||
meteor-base@1.5.2
|
||||
meteortesting:browser-tests@1.8.0
|
||||
meteortesting:mocha@3.3.0
|
||||
meteortesting:mocha-core@8.2.0
|
||||
minifier-css@2.0.2-rc350.1
|
||||
minifier-js@3.2.0
|
||||
minimongo@2.2.0-rc350.1
|
||||
modern-browsers@0.2.3
|
||||
modules@0.20.3
|
||||
modules-runtime@0.13.2
|
||||
mongo@2.4.0-rc350.1
|
||||
mongo-decimal@0.2.0
|
||||
mongo-dev-server@1.1.1
|
||||
mongo-id@1.0.9
|
||||
npm-mongo@6.16.1
|
||||
oauth@3.0.4-rc350.1
|
||||
oauth2@1.3.3
|
||||
observe-sequence@2.0.1
|
||||
ordered-dict@1.2.0
|
||||
ostrio:cookies@2.9.2
|
||||
ostrio:cstorage@4.0.1
|
||||
ostrio:files@2.3.3
|
||||
ostrio:files@3.0.1
|
||||
ostrio:flow-router-extra@3.15.3
|
||||
ostrio:i18n@3.2.1
|
||||
pascoual:pdfkit@1.0.7
|
||||
peerlibrary:assert@0.3.0
|
||||
peerlibrary:base-component@0.17.1
|
||||
peerlibrary:blaze-components@0.23.0
|
||||
peerlibrary:computed-field@0.10.0
|
||||
peerlibrary:data-lookup@0.3.0
|
||||
peerlibrary:reactive-field@0.6.0
|
||||
percolate:synced-cron@1.5.2
|
||||
promise@0.12.2
|
||||
raix:eventemitter@0.1.3
|
||||
raix:handlebar-helpers@0.2.5
|
||||
random@1.2.1
|
||||
rate-limit@1.1.1
|
||||
react-fast-refresh@0.2.8
|
||||
reactive-dict@1.3.1
|
||||
reactive-var@1.0.12
|
||||
reload@1.3.1
|
||||
retry@1.1.0
|
||||
routepolicy@1.1.1
|
||||
service-configuration@1.3.3
|
||||
session@1.2.1
|
||||
sha@1.0.9
|
||||
shell-server@0.5.0
|
||||
simple:authenticate-user-by-token@1.2.1
|
||||
simple:json-routes@2.3.1
|
||||
simple:rest-accounts-password@1.2.2
|
||||
simple:rest-bearer-token-parser@1.1.1
|
||||
simple:rest-json-error-handler@1.1.1
|
||||
socket-stream-client@0.5.2
|
||||
spacebars@1.4.1
|
||||
spacebars-compiler@1.3.1
|
||||
standard-minifier-js@2.8.1
|
||||
templates:tabs@2.3.0
|
||||
templating@1.4.1
|
||||
templating-compiler@1.4.1
|
||||
templating-runtime@1.5.0
|
||||
templating-tools@1.2.2
|
||||
tracker@1.3.3
|
||||
typescript@4.9.5
|
||||
ui@1.0.13
|
||||
underscore@1.0.13
|
||||
url@1.3.2
|
||||
useraccounts:core@1.16.2
|
||||
useraccounts:flow-routing@1.15.0
|
||||
useraccounts:unstyled@1.14.2
|
||||
webapp@1.13.6
|
||||
webapp-hashing@1.1.1
|
||||
wekan-accounts-cas@0.1.0
|
||||
wekan-accounts-lockout@1.0.0
|
||||
promise@1.0.0
|
||||
quave:synced-cron@2.4.0
|
||||
raix:eventemitter@1.0.0
|
||||
random@1.2.2
|
||||
rate-limit@1.2.0-rc350.1
|
||||
react-fast-refresh@0.3.0
|
||||
reactive-dict@1.3.2
|
||||
reactive-var@1.0.13
|
||||
reload@1.3.2
|
||||
retry@1.1.1
|
||||
reywood:publish-composite@1.9.0
|
||||
routepolicy@1.1.2
|
||||
rspack@1.1.0
|
||||
service-configuration@1.3.5
|
||||
session@1.2.3-rc350.1
|
||||
sha@1.0.10
|
||||
shell-server@0.7.0
|
||||
socket-stream-client@0.7.0-rc350.1
|
||||
spacebars@2.0.1
|
||||
spacebars-compiler@2.0.1
|
||||
standard-minifier-js@3.2.0
|
||||
templating@1.4.5
|
||||
templating-compiler@2.0.1
|
||||
templating-runtime@2.0.2
|
||||
templating-tools@2.0.1
|
||||
tools-core@1.1.0
|
||||
tracker@1.3.4
|
||||
typescript@5.10.0
|
||||
url@1.3.6-rc350.1
|
||||
webapp@2.2.0-rc350.1
|
||||
webapp-hashing@1.1.2
|
||||
wekan-accounts-cas@0.2.0
|
||||
wekan-accounts-lockout@1.1.0
|
||||
wekan-accounts-oidc@1.0.10
|
||||
wekan-accounts-sandstorm@0.8.0
|
||||
wekan-bootstrap-datepicker@1.10.0
|
||||
wekan-accounts-sandstorm@0.9.0
|
||||
wekan-fontawesome@6.4.2
|
||||
wekan-fullcalendar@3.10.5
|
||||
wekan-ldap@0.0.2
|
||||
wekan-fullcalendar@5.11.5
|
||||
wekan-ldap@0.1.0
|
||||
wekan-markdown@1.0.9
|
||||
wekan-oidc@1.0.12
|
||||
yasaricli:slugify@0.0.7
|
||||
zimme:active-route@2.3.2
|
||||
zodern:types@1.0.10
|
||||
wekan-oidc@1.1.0
|
||||
zodern:types@1.0.13
|
||||
@@ -0,0 +1,2 @@
|
||||
npm-packages/
|
||||
tests/
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = te_IN: te-IN, es_AR: es-AR, es_419: es-LA, es_TX: es-TX, he_IL: he-IL, zh_CN: zh-CN, ar_EG: ar-EG, cs_CZ: cs-CZ, fa_IR: fa-IR, ms_MY: ms-MY, nl_NL: nl-NL, de_CH: de-CH, en_IT: en-IT, uz_UZ: uz-UZ, fr_CH: fr-CH, hi_IN: hi-IN, et_EE: et-EE, es_PE: es-PE, es_MX: es-MX, gl_ES: gl-ES, mn_MN: mn, sl_SI: sl, zh_TW: zh-TW, ast_ES: ast-ES, es_CL: es-CL, ja_JP: ja, lv_LV: lv, ro_RO: ro-RO, az_AZ: az-AZ, cy_GB: cy-GB, gu_IN: gu-IN, pl_PL: pl-PL, vep: ve-PP, en_BR: en-BR, en@ysv: en-YS, hu_HU: hu, ko_KR: ko-KR, pt_BR: pt-BR, zh_HK: zh-HK, zu_ZA: zu-ZA, en_MY: en-MY, ja-Hira: ja-HI, fi_FI: fi, vec: ve-CC, vi_VN: vi-VN, fr_FR: fr-FR, id_ID: id, zh_Hans: zh-Hans, en_DE: en-DE, en_GB: en-GB, el_GR: el-GR, uk_UA: uk-UA, az@latin: az-LA, de_AT: de-AT, uz@Latn: uz-LA, vls: vl-SS, ar_DZ: ar-DZ, bg_BG: bg, es_PY: es-PY, fy_NL: fy-NL, uz@Arab: uz-AR, ru_UA: ru-UA, war: wa-RR, zh_CN.GB2312: zh-GB
|
||||
lang_map = te_IN: te-IN, es_AR: es-AR, es_419: es-LA, es_TX: es-TX, he_IL: he-IL, zh_CN: zh-CN, ar_EG: ar-EG, cs_CZ: cs-CZ, fa_IR: fa-IR, ms_MY: ms-MY, nl_NL: nl-NL, de_CH: de-CH, en_IT: en-IT, uz_UZ: uz-UZ, fr_CH: fr-CH, hi_IN: hi-IN, et_EE: et-EE, es_PE: es-PE, es_MX: es-MX, gl_ES: gl-ES, mn_MN: mn, zh_TW: zh-TW, ast_ES: ast-ES, es_CL: es-CL, ja_JP: ja, lv_LV: lv, ro_RO: ro-RO, az_AZ: az-AZ, cy_GB: cy-GB, gu_IN: gu-IN, pl_PL: pl-PL, vep: ve-PP, en_BR: en-BR, en@ysv: en-YS, hu_HU: hu, ko_KR: ko-KR, pt_BR: pt-BR, zh_HK: zh-HK, zu_ZA: zu-ZA, en_MY: en-MY, ja-Hira: ja-HI, fi_FI: fi, vec: ve-CC, vi_VN: vi-VN, fr_FR: fr-FR, id_ID: id, zh_Hans: zh-Hans, en_DE: en-DE, en_GB: en-GB, el_GR: el-GR, uk_UA: uk-UA, az@latin: az-LA, de_AT: de-AT, uz@Latn: uz-LA, vls: vl-SS, ar_DZ: ar-DZ, bg_BG: bg, es_PY: es-PY, fy_NL: fy-NL, uz@Arab: uz-AR, ru_UA: ru-UA, war: wa-RR, zh_CN.GB2312: zh-GB
|
||||
|
||||
[o:wekan:p:wekan:r:application]
|
||||
file_filter = imports/i18n/data/<lang>.i18n.json
|
||||
|
||||
Vendored
+4
-1
@@ -9,5 +9,8 @@
|
||||
"TERM": "xterm-256color"
|
||||
},
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"terminal.integrated.shellArgs.linux": ["-l"]
|
||||
"terminal.integrated.shellArgs.linux": [
|
||||
"-l"
|
||||
],
|
||||
"files.simpleDialog.enable": true
|
||||
}
|
||||
+3746
-20
File diff suppressed because it is too large.
Load diff
+6
-1
@@ -3,9 +3,14 @@
|
||||
For all code at WeKan GitHub Organization https://github.com/wekan
|
||||
|
||||
- All code in pull requests need to have permission already to add it to WeKan with MIT license, and will become MIT license.
|
||||
- All code xet7 add is MIT license.
|
||||
- For any dependencies, permissive licenses like https://copyfree.org are preferred
|
||||
- For anything currently that is non-permissive (like GPL, AGPL, SSPL), those will be replaced with permissive-licensed alternatives
|
||||
- Any intelligence allowed: Human, AI or any other. No discrimination, please have world peace. Mentioning participants optional.
|
||||
- All code is checked and validated by xet7 and many different security AI. If code is real working fix or feature, it usually will be merged.
|
||||
- All known vulnerabilities have been fixed at newest WeKan, please upgrade https://wekan.fi/upgrade/
|
||||
- There is no known backdoors. If WeKan is installed by someone else, first registered user is Admin and has Admin Panel access.
|
||||
- Maintainer of WeKan has no access to someone elses installed WeKan.
|
||||
- For someone elses installed WeKan, see [Forgot Password](https://github.com/wekan/wekan/blob/main/docs/Login/Forgot-Password.md)
|
||||
|
||||
# Reporting about violations or something else
|
||||
|
||||
|
||||
+26
-19
@@ -6,24 +6,25 @@ Not paid:
|
||||
- Security reports, see [SECURITY.md](SECURITY.md)
|
||||
- Pull requests
|
||||
- xet7 checking pull requests
|
||||
- Public Community Support
|
||||
- This is because full-featured WeKan FLOSS kanban with MIT license is free to use
|
||||
also for Commercial Use without any payment, with full source code at
|
||||
https://github.com/wekan/wekan for all platforms at https://wekan.fi/install/ .
|
||||
There is nothing proprietary. WeKan is NOT Big Tech.
|
||||
WeKan is maintained by https://github.com/xet7 at Finland https://wekan.fi .
|
||||
- Public FLOSS Developer Community Support:
|
||||
- https://github.com/wekan/wekan/issues
|
||||
|
||||
Paid by customers of WeKan Team:
|
||||
Paid by customers of WeKan to WeKan:
|
||||
|
||||
- Commercial Support at https://wekan.fi/commercial-support/
|
||||
- Support
|
||||
- Private Chat
|
||||
- Features
|
||||
- Fixes
|
||||
- Hosting
|
||||
- Optional Private Paid Commercial Support at https://wekan.fi/commercial-support/
|
||||
- Optional Paid Support/Features/Fixes/PrivateChat . All Paid Features and Fixes are added directly to FLOSS WeKan https://github.com/wekan/wekan
|
||||
- Optional Paid Official SaaS Hosting https://wekan.fi/saas/ , if you need full-featured hosting, that is more than free app https://wekan.fi/app/
|
||||
|
||||
## Contributing Security related
|
||||
|
||||
For responsible security disclosure, please follow this process:
|
||||
https://github.com/wekan/wekan/blob/main/SECURITY.md
|
||||
For responsible security disclosure, please follow this process: [SECURITY.md](SECURITY.md)
|
||||
|
||||
CVE Hall of Fame is at https://wekan.github.io/hall-of-fame/
|
||||
CVE Hall of Fame is at https://wekan.fi/hall-of-fame/
|
||||
|
||||
## Contributing to Documentation Wiki
|
||||
|
||||
@@ -33,7 +34,17 @@ and click send PR.
|
||||
|
||||
## Contributing code
|
||||
|
||||
[Building WeKan and sending PR](https://github.com/wekan/wekan/wiki/Emoji).
|
||||
Use rebuild-wekan.sh to run all tests to verify you don't break any features, before sending PR.
|
||||
|
||||
Only send one feature or fix at one PR. Do not add many unrelated changes.
|
||||
|
||||
Only send those changed code lines, that are related to your one feature or fix.
|
||||
|
||||
Do not format unrelated code. Do not use code formatting or linting like eslist or prettier.
|
||||
|
||||
Send your code ASAP, because there are daily releases of WeKan with major features and fixes added.
|
||||
|
||||
[Building WeKan and sending PR](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md).
|
||||
|
||||
WeKan code contributors Hall of Fame is at ChangeLog, where
|
||||
GitHub usernames are mentioned with changes added:
|
||||
@@ -44,13 +55,9 @@ Changes can be like typo fixes, bugfixes, features, or anything else
|
||||
like for example at open GitHub issues https://github.com/wekan/wekan/issues .
|
||||
Closed issues are already fixed or implemented.
|
||||
|
||||
Also see other docs at wiki, for example:
|
||||
Also see other docs:
|
||||
|
||||
https://github.com/wekan/wekan/wiki/Developer-Documentation
|
||||
|
||||
Do not use code formatting or linting like eslist or prettier.
|
||||
|
||||
Only send minimal changed code lines, that are related to feature or fix.
|
||||
https://github.com/wekan/wekan/tree/main/docs/DeveloperDocs
|
||||
|
||||
WeKan code has MIT license.
|
||||
|
||||
@@ -64,7 +71,7 @@ so that WeKan code will still work OK.
|
||||
## Contributing translations
|
||||
|
||||
Non-English translations are contributed only at
|
||||
https://transifex.com/wekan/wekan
|
||||
https://app.transifex.com/wekan/wekan
|
||||
|
||||
When adding new features, in your PR to
|
||||
https://github.com/wekan/wekan/pulls
|
||||
|
||||
+59
-117
@@ -1,31 +1,26 @@
|
||||
FROM ubuntu:24.04
|
||||
FROM ubuntu:26.04
|
||||
LABEL maintainer="wekan"
|
||||
LABEL org.opencontainers.image.ref.name="ubuntu"
|
||||
LABEL org.opencontainers.image.version="24.04"
|
||||
LABEL org.opencontainers.image.version="26.04"
|
||||
LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
|
||||
|
||||
# 2022-04-25:
|
||||
# - gyp does not yet work with Ubuntu 22.04 ubuntu:rolling,
|
||||
# so changing to 21.10. https://github.com/wekan/wekan/issues/4488
|
||||
|
||||
# 2021-09-18:
|
||||
# - Above Ubuntu base image copied from Docker Hub ubuntu:hirsute-20210825
|
||||
# to Quay to avoid Docker Hub rate limits.
|
||||
# TARGETARCH and TARGETVARIANT are automatically provided by Docker Buildx
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
ARG VERSION=9.42
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ENV BUILD_DEPS="apt-utils gnupg gosu wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
|
||||
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
|
||||
|
||||
ENV \
|
||||
DEBUG=false \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
METEOR_RELEASE=METEOR@2.14 \
|
||||
NODE_VERSION=v24.16.0 \
|
||||
METEOR_RELEASE=METEOR@3.5-beta.12 \
|
||||
USE_EDGE=false \
|
||||
METEOR_EDGE=1.5-beta.17 \
|
||||
NPM_VERSION=6.14.17 \
|
||||
FIBERS_VERSION=4.0.1 \
|
||||
ARCHITECTURE=linux-x64 \
|
||||
NPM_VERSION=11.11.0 \
|
||||
SRC_PATH=./ \
|
||||
WITH_API=true \
|
||||
MONGO_OPLOG_URL="" \
|
||||
RESULTS_PER_PAGE="" \
|
||||
DEFAULT_BOARD_ID="" \
|
||||
ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
|
||||
@@ -163,134 +158,81 @@ ENV \
|
||||
MONGO_PASSWORD_FILE="" \
|
||||
S3_SECRET_FILE=""
|
||||
|
||||
# NODE_OPTIONS="--max_old_space_size=4096"
|
||||
|
||||
#---------------------------------------------
|
||||
# == at docker-compose.yml: AUTOLOGIN WITH OIDC/OAUTH2 ====
|
||||
# https://github.com/wekan/wekan/wiki/autologin
|
||||
#- OIDC_REDIRECTION_ENABLED=true
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Copy the app to the image
|
||||
#COPY ${SRC_PATH} /home/wekan/app
|
||||
|
||||
# Install OS
|
||||
RUN <<EOR
|
||||
set -o xtrace
|
||||
# Fail hard on any error so a missing release zip / failed download can never
|
||||
# produce a "successful" image with an empty /build (Cannot find /build/main.js).
|
||||
set -eo pipefail
|
||||
|
||||
# Add non-root user wekan
|
||||
useradd --user-group --system --home-dir /home/wekan wekan
|
||||
# OS dependencies
|
||||
# Create Wekan user. --create-home is required because --system users do not
|
||||
# get a home directory by default; without it /home/wekan never exists and the
|
||||
# later `chown ... /home/wekan/` aborts the build (now that set -e is active).
|
||||
useradd --user-group --system --create-home --home-dir /home/wekan wekan
|
||||
|
||||
# OS Updates
|
||||
apt-get update --assume-yes
|
||||
apt-get upgrade --assume-yes
|
||||
apt-get install --assume-yes --no-install-recommends ${BUILD_DEPS}
|
||||
|
||||
# Meteor installer doesn't work with the default tar binary, so using bsdtar while installing.
|
||||
# https://github.com/coreos/bugs/issues/1095#issuecomment-350574389
|
||||
# Multi-arch mapping: Docker TARGETARCH -> Node.js arch name + WeKan bundle name
|
||||
# arm/v7 uses TARGETARCH=arm; armhf has no MongoDB Community -> uses FerretDB
|
||||
case "${TARGETARCH}" in
|
||||
"amd64") NODE_ARCH="x64" WEKAN_ARCH="amd64" ;;
|
||||
"arm64") NODE_ARCH="arm64" WEKAN_ARCH="arm64" ;;
|
||||
"arm") NODE_ARCH="armv7l" WEKAN_ARCH="armhf" ;;
|
||||
"ppc64le") NODE_ARCH="ppc64le" WEKAN_ARCH="ppc64le" ;;
|
||||
"s390x") NODE_ARCH="s390x" WEKAN_ARCH="s390x" ;;
|
||||
*) echo "Unsupported architecture: ${TARGETARCH}"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Node.js Installation (official nodejs.org builds for Node 22)
|
||||
cd /tmp
|
||||
wget "https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz"
|
||||
wget "https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc"
|
||||
grep "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt.asc | shasum -a 256 -c -
|
||||
tar xzf "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" -C /usr/local --strip-components=1 --no-same-owner
|
||||
rm -f "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt.asc
|
||||
ln -s "/usr/local/bin/node" "/usr/local/bin/nodejs"
|
||||
|
||||
# NPM configuration
|
||||
npm install -g npm@${NPM_VERSION}
|
||||
chown --recursive wekan:wekan /home/wekan/
|
||||
|
||||
# Temporary Tar swap for Meteor bundle
|
||||
cp $(which tar) $(which tar)~
|
||||
ln -sf $(which bsdtar) $(which tar)
|
||||
|
||||
# Install NodeJS
|
||||
cd /tmp
|
||||
|
||||
# Download nodejs
|
||||
#wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz"
|
||||
wget "https://github.com/wekan/node-v14-esm/releases/download/v14.21.4/node-v14.21.4-linux-x64.tar.gz"
|
||||
#wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt"
|
||||
wget "https://github.com/wekan/node-v14-esm/releases/download/v14.21.4/SHASUMS256.txt"
|
||||
|
||||
# Verify nodejs authenticity
|
||||
#grep "node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz" "SHASUMS256.txt" | shasum -a 256 -c -
|
||||
grep "node-v14.21.4-linux-x64.tar.gz" "SHASUMS256.txt" | shasum -a 256 -c -
|
||||
rm -f "SHASUMS256.txt"
|
||||
|
||||
# Install Node
|
||||
#tar xzf "node-$NODE_VERSION-$ARCHITECTURE.tar.gz" -C /usr/local --strip-components=1 --no-same-owner
|
||||
tar xzf "node-v14.21.4-linux-x64.tar.gz" -C /usr/local --strip-components=1 --no-same-owner
|
||||
#rm "node-$NODE_VERSION-$ARCHITECTURE.tar.gz" "SHASUMS256.txt"
|
||||
rm "node-v14.21.4-linux-x64.tar.gz" "SHASUMS256.txt"
|
||||
ln -s "/usr/local/bin/node" "/usr/local/bin/nodejs"
|
||||
#mkdir -p "/opt/nodejs/lib/node_modules/fibers/.node-gyp" "/root/.node-gyp/${NODE_VERSION} /home/wekan/.config"
|
||||
#mkdir -p "/opt/nodejs/lib/node_modules/fibers/.node-gyp" "/root/.node-gyp/v14.21.4 /home/wekan/.config"
|
||||
|
||||
# Install node dependencies
|
||||
#npm install -g npm@${NPM_VERSION} --production
|
||||
npm install -g npm@$6.14.17 --production
|
||||
chown --recursive wekan:wekan /home/wekan/.config
|
||||
|
||||
# Install Meteor
|
||||
cd /home/wekan
|
||||
chown --recursive wekan:wekan /home/wekan
|
||||
echo "Starting meteor ${METEOR_RELEASE} installation... \n"
|
||||
#gosu wekan:wekan curl https://install.meteor.com/ | /bin/sh
|
||||
# Specify Meteor version 2.14 to be compatible: https://github.com/wekan/wekan/pull/5816/files
|
||||
#gosu wekan:wekan npm -g install meteor@2.14 --unsafe-perm
|
||||
#mv /root/.meteor /home/wekan/
|
||||
#chown --recursive wekan:wekan /home/wekan/.meteor
|
||||
|
||||
#sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' /home/wekan/app/packages/meteor-useraccounts-core/package.js
|
||||
#cd /home/wekan/.meteor
|
||||
#gosu wekan:wekan /home/wekan/.meteor/meteor -- help
|
||||
|
||||
# Build app (Production)
|
||||
#cd /home/wekan/app
|
||||
# WeKan Bundle Installation
|
||||
mkdir -p /home/wekan/app
|
||||
cd /home/wekan/app
|
||||
#mkdir -p /home/wekan/.npm
|
||||
#chown --recursive wekan:wekan /home/wekan/.npm
|
||||
#chmod u+w *.json
|
||||
#gosu wekan:wekan meteor npm install --production
|
||||
#gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build
|
||||
#cd /home/wekan/app_build/bundle/programs/server/
|
||||
#chmod u+w *.json
|
||||
#gosu wekan:wekan meteor npm install --production
|
||||
#cd node_modules/fibers
|
||||
#node build.js
|
||||
#cd ../..
|
||||
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
||||
#rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
|
||||
#mv /home/wekan/app_build/bundle /build
|
||||
wget "https://github.com/wekan/wekan/releases/download/v7.99/wekan-7.99-amd64.zip"
|
||||
unzip wekan-7.99-amd64.zip
|
||||
rm wekan-7.99-amd64.zip
|
||||
wget "https://github.com/wekan/wekan/releases/download/v${VERSION}/wekan-${VERSION}-${WEKAN_ARCH}.zip"
|
||||
unzip "wekan-${VERSION}-${WEKAN_ARCH}.zip"
|
||||
rm "wekan-${VERSION}-${WEKAN_ARCH}.zip"
|
||||
npm install --prefix ./bundle/programs/server
|
||||
mv /home/wekan/app/bundle /build
|
||||
|
||||
# Put back the original tar
|
||||
# Restore original tar
|
||||
mv $(which tar)~ $(which tar)
|
||||
|
||||
# Cleanup
|
||||
# Remove unused Go-based pebble binary shipped by base image to reduce CVE surface.
|
||||
apt-get remove --purge --assume-yes pebble || true
|
||||
rm -f /usr/bin/pebble
|
||||
apt-get remove --purge --assume-yes ${BUILD_DEPS}
|
||||
#npm uninstall -g api2html
|
||||
apt-get autoremove --assume-yes
|
||||
apt-get clean --assume-yes
|
||||
rm -Rf /tmp/*
|
||||
rm -Rf /var/lib/apt/lists/*
|
||||
rm -Rf /var/cache/apt
|
||||
rm -Rf /var/lib/apt/lists
|
||||
rm -Rf /home/wekan/app_build
|
||||
rm -Rf /home/wekan/app
|
||||
rm -Rf /home/wekan/.meteor
|
||||
|
||||
mkdir /data
|
||||
chown wekan --recursive /data
|
||||
mkdir -p /data
|
||||
chown wekan:wekan --recursive /data
|
||||
EOR
|
||||
|
||||
USER wekan
|
||||
|
||||
ENV PORT=8080
|
||||
EXPOSE $PORT
|
||||
|
||||
STOPSIGNAL SIGKILL
|
||||
WORKDIR /home/wekan/app
|
||||
WORKDIR /build
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
# https://github.com/wekan/wekan/issues/3585#issuecomment-1021522132
|
||||
# Add more Node heap:
|
||||
# NODE_OPTIONS="--max_old_space_size=4096"
|
||||
# Add more stack:
|
||||
# bash -c "ulimit -s 65500; exec node --stack-size=65500 main.js"
|
||||
#---------------------------------------------------------------------
|
||||
#
|
||||
# CMD ["node", "/build/main.js"]
|
||||
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 /build/main.js"]
|
||||
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 --max-old-space-size=8192 /build/main.js"]
|
||||
CMD ["bash", "-c", "ulimit -s 65500; exec node /build/main.js"]
|
||||
CMD ["bash", "-c", "ulimit -s 65500; exec node main.js"]
|
||||
@@ -1,98 +0,0 @@
|
||||
FROM arm64v8/ubuntu:23.04 AS builder
|
||||
#FROM amd64/alpine:latest AS builder
|
||||
|
||||
# Set the environment variables for builder
|
||||
ENV QEMU_VERSION=v7.2.0-1 \
|
||||
QEMU_ARCHITECTURE=aarch64 \
|
||||
NODE_ARCHITECTURE=linux-arm64 \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
WEKAN_VERSION=latest \
|
||||
WEKAN_ARCHITECTURE=arm64
|
||||
|
||||
# Install dependencies
|
||||
#RUN apk update && apk add ca-certificates outils-sha1 && \
|
||||
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||
RUN apt update && apt install ca-certificates wget unzip -y && \
|
||||
\
|
||||
# Download qemu static for our architecture
|
||||
wget https://github.com/multiarch/qemu-user-static/releases/download/${QEMU_VERSION}/qemu-${QEMU_ARCHITECTURE}-static.tar.gz -O - | tar -xz && \
|
||||
\
|
||||
# Download wekan and shasum
|
||||
wget https://releases.wekan.team/raspi3/wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
|
||||
wget https://releases.wekan.team/raspi3/SHA256SUMS.txt && \
|
||||
# Verify wekan
|
||||
grep wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip SHA256SUMS.txt | sha256sum -c - && \
|
||||
\
|
||||
# Unzip wekan
|
||||
unzip wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
|
||||
\
|
||||
# Download node and shasums
|
||||
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
|
||||
\
|
||||
# Verify nodejs authenticity
|
||||
grep node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz SHASUMS256.txt | sha256sum -c - && \
|
||||
\
|
||||
# Extract node and remove tar.gz
|
||||
tar xvzf node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz
|
||||
|
||||
# Build wekan dockerfile
|
||||
FROM --platform=linux/arm64 arm64v8/ubuntu:23.04
|
||||
LABEL maintainer="wekan"
|
||||
|
||||
# Set the environment variables (defaults where required)
|
||||
ENV QEMU_ARCHITECTURE=aarch64 \
|
||||
NODE_ARCHITECTURE=linux-arm64 \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
NODE_ENV=production \
|
||||
NPM_VERSION=latest \
|
||||
WITH_API=true \
|
||||
PORT=8080 \
|
||||
ROOT_URL=http://localhost \
|
||||
MONGO_URL=mongodb://127.0.0.1:27017/wekan \
|
||||
LDAP_AUTHENTIFICATION_PASSWORD_FILE="" \
|
||||
OAUTH2_SECRET_FILE="" \
|
||||
MAIL_SERVICE_PASSWORD_FILE="" \
|
||||
MONGO_PASSWORD_FILE="" \
|
||||
S3_SECRET_FILE=""
|
||||
|
||||
# Copy qemu-static to image
|
||||
COPY --from=builder qemu-${QEMU_ARCHITECTURE}-static /usr/bin
|
||||
|
||||
# Copy the app to the image
|
||||
COPY --from=builder bundle /home/wekan/bundle
|
||||
|
||||
# Copy
|
||||
COPY --from=builder node-${NODE_VERSION}-${NODE_ARCHITECTURE} /opt/nodejs
|
||||
|
||||
RUN \
|
||||
set -o xtrace && \
|
||||
# Add non-root user wekan
|
||||
useradd --user-group --system --home-dir /home/wekan wekan && \
|
||||
\
|
||||
# Install Node
|
||||
ln -s /opt/nodejs/bin/node /usr/bin/node && \
|
||||
ln -s /opt/nodejs/bin/npm /usr/bin/npm && \
|
||||
mkdir -p /opt/nodejs/lib/node_modules/fibers/.node-gyp /root/.node-gyp/8.16.1 /home/wekan/.config && \
|
||||
chown wekan --recursive /home/wekan/.config
|
||||
|
||||
# \
|
||||
# # Install Node dependencies
|
||||
# #npm install -g npm@${NPM_VERSION} && \
|
||||
# \
|
||||
# # Install Health Check dependencies
|
||||
# #apk add curl
|
||||
#
|
||||
#HEALTHCHECK --start-period=30s --interval=30s --timeout=10s --retries=3 \
|
||||
# CMD curl --fail "http://localhost:$PORT" || exit 1
|
||||
|
||||
EXPOSE $PORT
|
||||
USER wekan
|
||||
|
||||
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 --max-old-space-size=8192 /home/wekan/bundle/main.js"]
|
||||
CMD ["bash", "-c", "ulimit -s 65500; exec node /home/wekan/bundle/main.js"]
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
FROM arm64v8/ubuntu:23.04 AS builder
|
||||
#FROM --platform=linux/amd64 amd64/ubuntu:23.04 AS builder
|
||||
#FROM --platform=linux/amd64 ghcr.io/wekan/wekan:main AS builder
|
||||
#FROM arm64v8/ubuntu:23.04 AS builder
|
||||
#FROM amd64/alpine:latest AS builder
|
||||
|
||||
# Set the environment variables for builder
|
||||
ENV QEMU_VERSION=v7.2.0-1 \
|
||||
QEMU_ARCHITECTURE=s390x \
|
||||
NODE_ARCHITECTURE=linux-s390x \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
WEKAN_VERSION=latest \
|
||||
WEKAN_ARCHITECTURE=s390x
|
||||
|
||||
# Install dependencies
|
||||
#RUN apk update && apk add ca-certificates outils-sha1 && \
|
||||
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||
RUN apt update && apt install ca-certificates wget unzip -y && \
|
||||
\
|
||||
# Download qemu static for our architecture
|
||||
wget https://github.com/multiarch/qemu-user-static/releases/download/${QEMU_VERSION}/qemu-${QEMU_ARCHITECTURE}-static.tar.gz -O - | tar -xz && \
|
||||
\
|
||||
# Download wekan and shasum
|
||||
wget https://releases.wekan.team/${WEKAN_ARCHITECTURE}/wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
|
||||
wget https://releases.wekan.team/${WEKAN_ARCHITECTURE}/SHA256SUMS.txt && \
|
||||
# Verify wekan
|
||||
grep wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip SHA256SUMS.txt | sha256sum -c - && \
|
||||
\
|
||||
# Unzip wekan
|
||||
unzip wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
|
||||
\
|
||||
# Download node and shasums
|
||||
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
|
||||
\
|
||||
# Verify nodejs authenticity
|
||||
grep node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz SHASUMS256.txt | sha256sum -c - && \
|
||||
\
|
||||
# Extract node and remove tar.gz
|
||||
tar xvzf node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz
|
||||
|
||||
# Build wekan dockerfile
|
||||
FROM --platform=linux/s390x s390x/ubuntu:23.04
|
||||
LABEL maintainer="wekan"
|
||||
|
||||
# Set the environment variables (defaults where required)
|
||||
ENV QEMU_ARCHITECTURE=s390x \
|
||||
NODE_ARCHITECTURE=linux-s390x \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
NODE_ENV=production \
|
||||
NPM_VERSION=latest \
|
||||
WITH_API=true \
|
||||
PORT=8080 \
|
||||
ROOT_URL=http://localhost \
|
||||
MONGO_URL=mongodb://127.0.0.1:27017/wekan \
|
||||
LDAP_AUTHENTIFICATION_PASSWORD_FILE="" \
|
||||
OAUTH2_SECRET_FILE="" \
|
||||
MAIL_SERVICE_PASSWORD_FILE="" \
|
||||
MONGO_PASSWORD_FILE="" \
|
||||
S3_SECRET_FILE=""
|
||||
|
||||
# Copy qemu-static to image
|
||||
COPY --from=builder qemu-${QEMU_ARCHITECTURE}-static /usr/bin
|
||||
|
||||
# Copy the app to the image
|
||||
COPY --from=builder bundle /home/wekan/bundle
|
||||
|
||||
# Copy
|
||||
COPY --from=builder node-${NODE_VERSION}-${NODE_ARCHITECTURE} /opt/nodejs
|
||||
|
||||
RUN \
|
||||
set -o xtrace && \
|
||||
# Add non-root user wekan
|
||||
useradd --user-group --system --home-dir /home/wekan wekan && \
|
||||
\
|
||||
# Install Node
|
||||
ln -s /opt/nodejs/bin/node /usr/bin/node && \
|
||||
ln -s /opt/nodejs/bin/npm /usr/bin/npm && \
|
||||
mkdir -p /opt/nodejs/lib/node_modules/fibers/.node-gyp /root/.node-gyp/8.16.1 /home/wekan/.config && \
|
||||
chown wekan --recursive /home/wekan/.config
|
||||
|
||||
# \
|
||||
# # Install Node dependencies
|
||||
# #npm install -g npm@${NPM_VERSION} && \
|
||||
# \
|
||||
# # Install Health Check dependencies
|
||||
# #apk add curl
|
||||
#
|
||||
#HEALTHCHECK --start-period=30s --interval=30s --timeout=10s --retries=3 \
|
||||
# CMD curl --fail "http://localhost:$PORT" || exit 1
|
||||
|
||||
EXPOSE $PORT
|
||||
USER wekan
|
||||
|
||||
CMD ["bash", "-c", "ulimit -s 65500; exec node /home/wekan/bundle/main.js"]
|
||||
@@ -47,12 +47,13 @@ See bottom of https://github.com/wekan/wekan/issues/3874
|
||||
## FAQ
|
||||
|
||||
**NOTE**:
|
||||
|
||||
- Please read the [FAQ](https://github.com/wekan/wekan/blob/main/docs/FAQ/FAQ.md) first
|
||||
- Please don't feed the [trolls](https://github.com/wekan/wekan/blob/main/docs/FAQ/FAQ.md#why-am-i-called-a-troll) and [spammers](https://github.com/wekan/wekan/blob/main/docs/FAQ/FAQ.md#why-am-i-called-a-spammer) that are mentioned in the FAQ :)
|
||||
|
||||
## About WeKan ®
|
||||
|
||||
WeKan ® is an completely [Open Source][open_source] and [Free software][free_software]
|
||||
WeKan ® is a completely [Open Source][open_source] and [Free software][free_software]
|
||||
collaborative kanban board application with MIT license.
|
||||
|
||||
Whether you’re maintaining a personal todo list, planning your holidays with some friends,
|
||||
@@ -76,15 +77,15 @@ that by providing one-click installation on various platforms.
|
||||
- 1 GB RAM minimum free for WeKan ®. Production server should have minimum total 4 GB RAM.
|
||||
For thousands of users, for example with [Docker](https://github.com/wekan/wekan/blob/main/docker-compose.yml): 3 frontend servers,
|
||||
each having 2 CPU and 2 wekan-app containers. One backend wekan-db server with many CPUs.
|
||||
- Enough disk space and alerts about low disk space. If you run out disk space, MongoDB database gets corrupted.
|
||||
- Enough disk space and alerts about low disk space. If you run out of disk space, MongoDB database gets corrupted.
|
||||
- SECURITY: Updating to newest WeKan ® version very often. Please check you do not have automatic updates of Sandstorm or Snap turned off.
|
||||
Old versions have security issues because of old versions Node.js etc. Only newest WeKan ® is supported.
|
||||
WeKan ® on Sandstorm is not usually affected by any Standalone WeKan ® (Snap/Docker/Source) security issues.
|
||||
- [Reporting all new bugs immediately](https://github.com/wekan/wekan/issues).
|
||||
New features and fixes are added to WeKan ® [many times a day](https://github.com/wekan/wekan/blob/main/CHANGELOG.md).
|
||||
- [Backups](https://github.com/wekan/wekan/blob/main/docs/Backup/Backup.md) of WeKan ® database once a day miminum.
|
||||
- [Backups](https://github.com/wekan/wekan/blob/main/docs/Backup/Backup.md) of WeKan ® database once a day minimum.
|
||||
Bugs, updates, users deleting list or card, harddrive full, harddrive crash etc can eat your data. There is no undo yet.
|
||||
Some bug can cause WeKan ® board to not load at all, requiring manual fixing of database content.
|
||||
Some bugs can cause WeKan ® board to not load at all, requiring manual fixing of database content.
|
||||
|
||||
## Roadmap and Demo
|
||||
|
||||
@@ -92,7 +93,7 @@ that by providing one-click installation on various platforms.
|
||||
|
||||
[Developer Documentation][dev_docs]
|
||||
|
||||
- There is many companies and individuals contributing code to WeKan ®, to add features and bugfixes
|
||||
- There are many companies and individuals contributing code to WeKan ®, to add features and bugfixes
|
||||
[many times a day](https://github.com/wekan/wekan/blob/main/CHANGELOG.md).
|
||||
- [Please add Add new Feature Requests and Bug Reports immediately](https://github.com/wekan/wekan/issues).
|
||||
- [Commercial Support](https://wekan.fi/commercial-support/).
|
||||
@@ -102,12 +103,75 @@ By working directly with WeKan ® you get the benefit of active maintenance and
|
||||
|
||||
## Getting Started with Development
|
||||
|
||||
The default branch uses [Meteor 2 with Node.js 14](https://wekan.fi/install/).
|
||||
The default branch uses Meteor 3.5-beta.7 with Node.js 24.x.
|
||||
See [CHANGELOG.md](https://github.com/wekan/wekan/blob/main/CHANGELOG.md) for the latest runtime updates.
|
||||
|
||||
To contribute, [create a fork](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#2-create-fork-of-httpsgithubcomwekanwekan-at-github-web-page) and run `./rebuild-wekan.sh` (or `./rebuild-wekan.bat` on Windows) as detailed [here](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#3-select-option-1-to-install-dependencies-and-then-enter). Once you're ready, please test your code and [submit a pull request (PR)](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#7-test).
|
||||
|
||||
Please refer to the [developer documentation](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Developer-Documentation.md) for more information.
|
||||
|
||||
## First-Time Setup for Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before building WeKan from source, ensure you have:
|
||||
|
||||
- **Git** - for cloning the repository
|
||||
- **Node.js 24.x** - WeKan requires Node.js 24.x
|
||||
- **Meteor** - the JavaScript framework WeKan is built with
|
||||
|
||||
### Installing Node.js 24.x
|
||||
|
||||
```bash
|
||||
# Using nvm (recommended)
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
||||
source ~/.zshrc # or ~/.bashrc
|
||||
nvm install 24
|
||||
nvm use 24
|
||||
```
|
||||
|
||||
### Installing Meteor
|
||||
|
||||
```bash
|
||||
curl https://install.meteor.com/ | sh
|
||||
```
|
||||
|
||||
### Building WeKan
|
||||
|
||||
The `rebuild-wekan.sh` script guides you through a three-stage process:
|
||||
|
||||
1. **Install dependencies (Option 1)** - Downloads all required Meteor packages and npm modules
|
||||
2. **Build WeKan (Option 2)** - Compiles the application
|
||||
3. **Run WeKan (Option 3)** - Starts the development server at http://localhost:4000
|
||||
|
||||
```bash
|
||||
# Clone your fork
|
||||
git clone git@github.com:YOUR_USERNAME/wekan.git
|
||||
cd wekan
|
||||
|
||||
# Make the script executable
|
||||
chmod +x rebuild-wekan.sh
|
||||
|
||||
# Step 1: Install dependencies
|
||||
./rebuild-wekan.sh
|
||||
# Press 1 and Enter
|
||||
|
||||
# Step 2: Build WeKan (after dependencies complete)
|
||||
./rebuild-wekan.sh
|
||||
# Press 2 and Enter
|
||||
|
||||
# Step 3: Run WeKan in development mode
|
||||
./rebuild-wekan.sh
|
||||
# Press 3 and Enter
|
||||
```
|
||||
|
||||
|
||||
### WSL Users
|
||||
|
||||
WSL users can use Snap Candidate. See [install docs](https://wekan.fi/install/).
|
||||
|
||||
The development server will start at http://localhost:4000. Any changes you make to the source code will automatically trigger a rebuild and refresh your browser.
|
||||
|
||||
## Screenshot
|
||||
|
||||
[More screenshots at Features page](https://github.com/wekan/wekan/tree/main/docs/Features)
|
||||
|
||||
+150
-87
@@ -1,14 +1,34 @@
|
||||
About money, see [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
## About Money: No bounty
|
||||
|
||||
Security is very important to us. If you discover any issue regarding security, please disclose
|
||||
the information responsibly by sending an email from Protonmail to security@wekan.fi
|
||||
that is Protomail email address, or by using this PGP key
|
||||
[security-at-wekan.fi.asc](security-at-wekan.fi.asc) to security@wekan.fi
|
||||
and not by creating a GitHub issue. We will respond swiftly to fix verifiable security issues.
|
||||
There is no bounty, like described at [CONTRIBUTING.md](CONTRIBUTING.md),
|
||||
because WeKan is NOT Big Tech. WeKan is FLOSS.
|
||||
|
||||
We thank you with a place at our hall of fame page, that is at https://wekan.fi/hall-of-fame
|
||||
## Steps for Coordinated Vulnerability Disclosure
|
||||
|
||||
## How should reports be formatted?
|
||||
### 1. Security Researcher:
|
||||
|
||||
- **Report**: Click "New draft security advisory" at [WeKan Security Advisories](https://github.com/wekan/wekan/security/advisories).
|
||||
More info at [GitHub Docs about reporting privately](https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/report-privately).
|
||||
- **Proof of Concept (PoC)**: If possible, include a fix or a reproduction script/code.
|
||||
- **If GitHub does not work**: Please send report to **security@wekan.fi**
|
||||
- **Optional Email Encryption**: An optional PGP key is available at [security-at-wekan.fi.asc](security-at-wekan.fi.asc).
|
||||
|
||||
### 2. WeKan Security Team:
|
||||
|
||||
- **Remediation**: Please wait for a new WeKan release that addresses the issue.
|
||||
Fixes are announced at the top of the [ChangeLog](https://github.com/wekan/wekan/blob/main/CHANGELOG.md).
|
||||
- **Recognition**: We will acknowledge your contribution by adding you to our
|
||||
[Hall of Fame](https://wekan.fi/hall-of-fame/).
|
||||
- **CVE Policy**: WeKan Security requests CVE at GitHub when releasing Security Advisory.
|
||||
|
||||
### 3. Post-Release and Public Disclosure:
|
||||
|
||||
- Publicity: If you publish blog posts, give talks, or write articles about the
|
||||
reported vulnerabilities, please send the links to us. We will link them
|
||||
to your entry in the [Hall of Fame](https://wekan.fi/hall-of-fame/) for additional context.
|
||||
- Advisories reported at GitHub are listed at [WeKan Security Advisories](https://github.com/wekan/wekan/security/advisories).
|
||||
|
||||
## How Should Reports Be Formatted?
|
||||
|
||||
```
|
||||
Name: %name
|
||||
@@ -26,35 +46,40 @@ CWSS (optional): %cwss
|
||||
|
||||
Anyone who reports a unique security issue in scope and does not disclose it to
|
||||
a third party before we have patched and updated may be upon their approval
|
||||
added to the Wekan Hall of Fame.
|
||||
added to the WeKan Hall of Fame https://wekan.fi/hall-of-fame/
|
||||
|
||||
## Which domains are in scope?
|
||||
|
||||
No public domains, because all those are donated to Wekan Open Source project,
|
||||
No public domains, because all those are donated to WeKan Open Source project,
|
||||
and we don't have any permissions to do security scans on those donated servers.
|
||||
|
||||
Please don't perform research that could impact other users. Secondly, please keep
|
||||
the reports short and succinct. If we fail to understand the logics of your bug, we will tell you.
|
||||
Please don't perform research that could impact other users. Second, please keep
|
||||
the reports short and succinct. If we fail to understand the logic of your bug, we will tell you.
|
||||
|
||||
You can [Install Wekan](https://github.com/wekan/wekan/releases) to your own computer
|
||||
You can [Install WeKan](https://wekan.fi/install/) on your own computer
|
||||
and scan it's vulnerabilities there.
|
||||
|
||||
## About Wekan versions
|
||||
## About WeKan Versions
|
||||
|
||||
There are only 2 versions of Wekan: Standalone Wekan, and Sandstorm Wekan.
|
||||
There are 2 versions of WeKan: Standalone WeKan, and Sandstorm WeKan.
|
||||
|
||||
### Standalone Wekan Security
|
||||
## Meteor Security
|
||||
|
||||
Standalone Wekan includes all non-Sandstorm platforms. Some Standalone Wekan platforms
|
||||
https://guide.meteor.com/security
|
||||
|
||||
### Standalone WeKan Security
|
||||
|
||||
Standalone WeKan includes all non-Sandstorm platforms. Some Standalone WeKan platforms
|
||||
like Snap and Docker have their own specific sandboxing etc features.
|
||||
|
||||
Standalone Wekan by default does not load any files from Internet, like fonts, CSS, etc.
|
||||
This also means all Standalone Wekan functionality works in offline local networks.
|
||||
WeKan is used at most countries of the world https://snapcraft.io/wekan
|
||||
and by by companies that have 30k users.
|
||||
Standalone WeKan by default does not load any files from Internet, like fonts, CSS, etc.
|
||||
This also means all Standalone WeKan functionality works in offline local networks.
|
||||
WeKan is used in most countries of the world https://snapcraft.io/wekan
|
||||
and by companies that have 30k users.
|
||||
|
||||
- Wekan private board attachments are not accessible without logging in.
|
||||
- There is feature to set board public, so that board is visible without logging in in readonly mode, with realtime updates.
|
||||
- WeKan private board attachments are not accessible without logging in.
|
||||
- There is feature to set board public, so that board is visible without logging
|
||||
in readonly mode, with realtime updates.
|
||||
- Admin Panel has feature to disable all public boards, so all boards are private.
|
||||
|
||||
## SSL/TLS
|
||||
@@ -63,32 +88,28 @@ and by by companies that have 30k users.
|
||||
- If you are thinking about TLS MITM, look at https://github.com/caddyserver/caddy/issues/2530
|
||||
- Let's Encrypt TLS requires publicly accessible webserver, that Let's Encrypt TLS validation servers check.
|
||||
- If firewall limits to only allowed IP addresses, you may need non-Let's Encrypt TLS cert.
|
||||
- For On Premise:
|
||||
- https://caddyserver.com/docs/automatic-https#local-https
|
||||
- https://github.com/wekan/wekan/wiki/Caddy-Webserver-Config
|
||||
- https://github.com/wekan/wekan/wiki/Azure
|
||||
- https://github.com/wekan/wekan/wiki/Traefik-and-self-signed-SSL-certs
|
||||
|
||||
## XSS
|
||||
|
||||
- Dompurify https://www.npmjs.com/package/dompurify
|
||||
- WeKan uses dompurify npm package to filter for XSS at fields like cards, as you can see from
|
||||
- DOMPurify https://www.npmjs.com/package/dompurify
|
||||
- WeKan uses DOMPurify npm package to filter for XSS at fields like cards, as you can see from
|
||||
[package.json](https://github.com/wekan/wekan/blob/main/package.json). Other used versions can be seen from
|
||||
[Meteor versions file](https://github.com/wekan/wekan/blob/main/.meteor/versions).
|
||||
- Forms can include markdown links, html, image tags etc like you see at https://wekan.github.io .
|
||||
- Forms can include markdown links, html, image tags etc like you see at https://wekan.fi .
|
||||
- It's possible to add attachments to cards, and markdown/html links to files.
|
||||
- Dompurify cleans up viewed code, so Javascript in input fields does not execute
|
||||
- https://wekan.github.io/hall-of-fame/fieldbleed/
|
||||
- DOMPurify cleans up viewed code, so JavaScript in input fields does not execute
|
||||
- https://wekan.fi/hall-of-fame/fieldbleed/
|
||||
- Reaction in comment is now checked, that it does not have extra added code
|
||||
- https://wekan.github.io/hall-of-fame/reactionbleed/
|
||||
- https://wekan.fi/hall-of-fame/reactionbleed/
|
||||
- https://github.com/wekan/wekan/blob/main/packages/markdown/src/template-integration.js#L76
|
||||
|
||||
## QA about PubSub
|
||||
## QA About PubSub
|
||||
|
||||
Q:
|
||||
|
||||
Hello,
|
||||
I have just seen the Meteor DevTools Evolved extension and was wondering if anyone had asked themselves the question of security.
|
||||
I have just seen the Meteor DevTools Evolved extension and was wondering if anyone
|
||||
had asked themselves the question of security.
|
||||
Insofar as all data is shown in the minimongo tab in plain text.
|
||||
How can data be hidden from this extension?
|
||||
|
||||
@@ -96,32 +117,36 @@ A:
|
||||
|
||||
## PubSub
|
||||
|
||||
- It is not security issue to show some text or image, that user has permission to see. It is a security issue, if browserside is some text or image that user should not see.
|
||||
- Meteor has browserside minimongo database, made with Javascript, updated with Publish/Subscribe, PubSub.
|
||||
- Publish/Subscribe means, that realtime web framework reads database changes stream, and then immediately updates webpage,
|
||||
like like dashboards, chat, kanban. That is the point in any realtime web framework in any programming language.
|
||||
- Yes, you should check with Meteor DevTools Evolved Chromium/Firefox extension that at minimongo is only text that user has permission to see.
|
||||
- Do checking as logged in user, and logged out user.
|
||||
- It is not a security issue to show text or images that the user has permission to see.
|
||||
It is a security issue, if browserside is some text or image that user should not see.
|
||||
- Meteor has browserside minimongo database, made with JavaScript, updated with Publish/Subscribe, PubSub.
|
||||
- Publish/Subscribe means, that realtime web framework reads database changes stream,
|
||||
and then immediately updates webpage, like dashboards, chat, kanban.
|
||||
That is the point of any realtime web framework in any programming language.
|
||||
- Yes, you should check with Meteor DevTools Evolved Chromium/Firefox extension that
|
||||
at minimongo is only text that user has permission to see.
|
||||
- Check as a logged-in user and as a logged-out user.
|
||||
- Check permissions and sanitize before allowing some change, because someone could modify content of input field,
|
||||
PubSub/websocket data (for example with Burp Suite Community Edition), etc.
|
||||
- If you have REST API, also check that only those that have login token, and have permission, can view or edit text
|
||||
- If you have a REST API, also check that only those who have a login token and permission can view or edit text.
|
||||
- You should not include any data user is not allowed to see. Not to webpage text, not to websockets/PubSub, etc.
|
||||
- Minimongo should not have password hashes PubSub https://wekan.github.io/hall-of-fame/userbleed/
|
||||
- PubSub uses Websockets, so you need those to be enabled at webserver like Caddy/Nginx/Apache etc, examples of settings
|
||||
- Minimongo should not have password hashes PubSub https://wekan.fi/hall-of-fame/userbleed/
|
||||
- PubSub uses WebSockets, so you need those to be enabled on the webserver like Caddy/Nginx/Apache etc, examples of settings
|
||||
at right menu of https://github.com/wekan/wekan/wiki
|
||||
- Clientside https://github.com/wekan/wekan/tree/main/client/components subscribes to
|
||||
PubSub https://github.com/wekan/wekan/tree/main/server/publications or calls meteor methods at https://github.com/wekan/wekan/tree/main/models
|
||||
- Clientside https://github.com/wekan/wekan/tree/main/client/components subscribes to
|
||||
PubSub https://github.com/wekan/wekan/tree/main/server/publications or calls meteor methods
|
||||
at https://github.com/wekan/wekan/tree/main/models
|
||||
- For Admin:
|
||||
- You can have input field for password https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
|
||||
- You can save password to database https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
|
||||
- Check that only current user or Admin can change password https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
|
||||
- Note that currentUser uses code like Meteor.user() in .js file
|
||||
- Do not have password hashes in PubSub https://github.com/wekan/wekan/blob/main/server/publications/users.js
|
||||
- Do not include password hashes in PubSub https://github.com/wekan/wekan/blob/main/server/publications/users.js
|
||||
- Only show Admin Panel to Admin https://github.com/wekan/wekan/blob/main/client/components/settings/settingBody.jade#L3
|
||||
- If there is a lot of data, use pagination https://github.com/wekan/wekan/blob/main/client/components/settings/peopleBody.js
|
||||
- Only have limited amount of data published in PubSub. Limit in MongoDB query in publications how much is published. Too much could make browser too slow.
|
||||
- Use Environment variables for any email etc passwords.
|
||||
- But what if you would like to remove minimongo? And only use Meteor methods for saving? In that case, you don't have realtime updates,
|
||||
- If there is a large amount of data, use pagination https://github.com/wekan/wekan/blob/main/client/components/settings/peopleBody.js
|
||||
- Only publish a limited amount of data in PubSub. Limit in MongoDB query in publications how much is published. Too much could make browser too slow.
|
||||
- Environment variables for email and other passwords.
|
||||
- But what if you would like to remove Minimongo and only use Meteor methods for saving? In that case, you don't have realtime updates,
|
||||
and you need to write much more code to load and save data yourself, handle any multi user data saving conflicts yourself,
|
||||
and many Meteor Atmospherejs.com PubSub using packages would not work anymore https://github.com/wekan/we
|
||||
|
||||
@@ -129,14 +154,14 @@ A:
|
||||
|
||||
- With PubSub, there is checking, that someone modifying Websockets content, like permission isAdmin, can not change to Admin.
|
||||
- https://github.com/wekan/wekan/commit/cbad4cf5943d47b916f64b4582f8ca76a9dfd743
|
||||
- https://wekan.github.io/hall-of-fame/adminbleed/
|
||||
- https://wekan.fi/hall-of-fame/adminbleed/
|
||||
|
||||
## Permissions and Roles
|
||||
|
||||
- For any user permissions, it's best to use Meteor package package https://github.com/Meteor-Community-Packages/meteor-roles .
|
||||
- Currently WeKan has custom hardcoded permissions, WeKan does not yet use that meteor-roles package.
|
||||
- For any user permissions, it's best to use Meteor package https://github.com/Meteor-Community-Packages/meteor-roles .
|
||||
- Currently, WeKan has custom hardcoded permissions and does not yet use the meteor-roles package.
|
||||
- Using permissions at WeKan sidebar https://github.com/wekan/wekan/blob/main/client/components/sidebar/sidebar.js#L1854-L1875
|
||||
- List of roles https://github.com/wekan/wekan/wiki/REST-API-Role . Change at board or Admin Panel. Also Organizations/Teams.
|
||||
- List of roles: https://github.com/wekan/wekan/wiki/REST-API-Role . Change at board or Admin Panel. Also Organizations/Teams.
|
||||
- Worker role: https://github.com/wekan/wekan/issues/2788
|
||||
- Not implemented yet: Granular Roles https://github.com/wekan/wekan/issues/3022
|
||||
- Check is user logged in, with `if (Meteor.user()) {`
|
||||
@@ -145,8 +170,8 @@ A:
|
||||
|
||||
## Environment variables
|
||||
|
||||
- For any passwords, use environment variables, those are serverside
|
||||
- Do not copy environment variable to public variable that is visible browserside https://github.com/wekan/wekan/blob/main/server/max-size.js
|
||||
- For any passwords, use environment variables; those are serverside
|
||||
- Do not copy environment variables to public variables that are visible on the browserside https://github.com/wekan/wekan/blob/main/server/max-size.js
|
||||
|
||||
```
|
||||
Meteor.startup(() => {
|
||||
@@ -157,42 +182,91 @@ Meteor.startup(() => {
|
||||
```
|
||||
|
||||
- For serverside, you can set Meteor.settings.variablename, without text public
|
||||
- For WeKan kanban, there is feature for setting board public, it can be viewed by anyone, there is realtime updates. But
|
||||
- For WeKan kanban, there is a feature for setting a board to public; it can be viewed by anyone, with realtime updates. But
|
||||
- Some of those permissions are checked at users.js models at https://github.com/wekan/wekan/tree/main/models
|
||||
- Environment variables are used for email server passwords, etc, at all platforms https://github.com/wekan/wekan/commit/a781c0e7dcfdbe34c1483ee83cec12455b7026f7
|
||||
|
||||
## Escape HTML comment tags so that HTML comments are visible
|
||||
|
||||
- Someone reported, that it is problem that content of HTML comments in edit mode, are not visible at at view mode, so this makes HTML comments visible.
|
||||
- Someone reported that it was a problem that the content of HTML comments in edit mode was not visible in view mode, so this change makes HTML comments visible.
|
||||
- https://github.com/wekan/wekan/commit/167863d95711249e69bb3511175d73b34acbbdb3
|
||||
- https://wekan.github.io/hall-of-fame/invisiblebleed/
|
||||
- https://wekan.fi/hall-of-fame/invisiblebleed/
|
||||
|
||||
## Attachments: XSS in filename is sanitized
|
||||
|
||||
- https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
|
||||
- https://wekan.github.io/hall-of-fame/filebleed/
|
||||
- https://wekan.fi/hall-of-fame/filebleed/
|
||||
|
||||
### Attachments: Forced download to prevent stored XSS
|
||||
|
||||
- To prevent browser-side execution of uploaded content under the app origin, all attachment downloads are served with safe headers:
|
||||
- `Content-Type: application/octet-stream`
|
||||
- `Content-Disposition: attachment`
|
||||
- `X-Content-Type-Options: nosniff`
|
||||
- A restrictive `Content-Security-Policy` with `sandbox`
|
||||
- This means attachments are downloaded instead of rendered inline by default. This mitigates HTML/JS/SVG based stored XSS vectors.
|
||||
- Avatars and inline images remain supported but SVG uploads are blocked and never rendered inline.
|
||||
|
||||
## Users: Client update restrictions
|
||||
|
||||
- Client-side updates to user documents are limited to safe fields only:
|
||||
- `username`
|
||||
- `profile.*`
|
||||
- Sensitive fields are blocked from any client updates and can only be modified by server methods with authorization:
|
||||
- `orgs`, `teams`, `roles`, `isAdmin`, `createdThroughApi`, `loginDisabled`, `authenticationMethod`, `services.*`, `emails.*`, `sessionData.*`
|
||||
- Attempts to update forbidden fields from the client are denied.
|
||||
- Admin operations like managing org/team membership or toggling flags must use server methods that check permissions.
|
||||
|
||||
## Voting: integrity and authorization
|
||||
|
||||
- Client updates to card `vote` fields are blocked to prevent forged votes and inconsistent policy enforcement.
|
||||
- Voting is performed via a server method that enforces:
|
||||
- Authentication and board membership, or an explicit per-card flag allowing non-members to vote.
|
||||
- Only the caller's own userId is added/removed from `vote.positive`/`vote.negative`.
|
||||
- This prevents members from fabricating other users' votes and ensures non-members cannot vote unless explicitly allowed.
|
||||
|
||||
## Planning Poker: integrity and authorization
|
||||
|
||||
- Client updates to card `poker` fields are blocked. All poker actions go through server methods that enforce:
|
||||
- Authentication and board membership for configuration and results.
|
||||
- For casting a poker vote, either board membership or an explicit per-card flag allowing non-members to participate.
|
||||
- Only the caller's own userId is added/removed from the selected estimation bucket (e.g., one, two, five, etc.).
|
||||
- Methods cover setting/unsetting poker question/end, casting votes, replaying, and setting final estimation.
|
||||
|
||||
## Attachment API: authentication and DoS prevention
|
||||
|
||||
- The attachment API (`/api/attachment/*`) requires proper authentication using `X-User-Id` and `X-Auth-Token` headers.
|
||||
- Authentication validates tokens by hashing with `Accounts._hashLoginToken` and matching against stored login tokens, preventing identity spoofing.
|
||||
- Request handlers implement:
|
||||
- 30-second timeout to prevent hanging connections.
|
||||
- Request body size limits (50MB for uploads, 10MB for metadata operations).
|
||||
- Proper error handling and guaranteed response completion.
|
||||
- Request error event handlers to clean up failed connections.
|
||||
- This prevents:
|
||||
- DoS attacks via concurrent unauthenticated or malformed requests.
|
||||
- Identity spoofing by using arbitrary bearer tokens or user IDs.
|
||||
- Resource exhaustion from hanging connections or excessive payloads.
|
||||
- Access control: all attachment operations verify board membership before allowing access.
|
||||
|
||||
## Brute force login protection
|
||||
|
||||
- https://github.com/wekan/wekan/commit/23e5e1e3bd081699ce39ce5887db7e612616014d
|
||||
- https://github.com/wekan/wekan/tree/main/packages/wekan-accounts-lockout
|
||||
|
||||
### Sandstorm Wekan Security
|
||||
### Sandstorm WeKan Security
|
||||
|
||||
On Sandstorm platform using environment variable Standalone Wekan features like Admin Panel etc are
|
||||
turned off, because Sandstorm platform provides SSO for all apps running on Sandstorm.
|
||||
On the Sandstorm platform, Standalone WeKan features like the Admin Panel are turned off using environment variables, because Sandstorm platform provides SSO for all apps running on Sandstorm.
|
||||
|
||||
[Sandstorm](https://sandstorm.io) is separate Open Source platform that has been
|
||||
[Sandstorm](https://sandstorm.io) is a separate Open Source platform that has been
|
||||
[security audited](https://sandstorm.io/news/2017-03-02-security-review) and found bugs fixed.
|
||||
Sandstorm also has passwordless login, LDAP, SAML, Google etc auth options already.
|
||||
At Sandstorm code is read-only and signed by app maintainers, only grain content can be modified.
|
||||
Wekan at Sandstorm runs in sandboxed grain, it does not have access elsewhere without user-visible
|
||||
PowerBox request or opening randomly-generated API key URL.
|
||||
Sandstorm also has passwordless login, LDAP, SAML, Google, and other auth options already.
|
||||
On Sandstorm, code is read-only and signed by app maintainers; only grain content can be modified.
|
||||
WeKan on Sandstorm runs in a sandboxed grain; it does not have access elsewhere without a user-visible PowerBox request or opening a randomly-generated API key URL.
|
||||
Also read [Sandstorm Security Practices](https://docs.sandstorm.io/en/latest/using/security-practices/) and
|
||||
[Sandstorm Security non-events](https://docs.sandstorm.io/en/latest/using/security-non-events/).
|
||||
For Sandstorm specific security issues you can contact [kentonv](https://github.com/kentonv) by email.
|
||||
For Sandstorm specific security issues you can contact [kentonv](https://github.com/kentonv) by email.
|
||||
|
||||
## What Wekan bugs are eligible?
|
||||
## What WeKan bugs are eligible?
|
||||
|
||||
Any typical web security bugs. If any of the previously mentioned is somehow problematic and
|
||||
a security issue, we'd like to know about it, and also how to fix it:
|
||||
@@ -204,23 +278,12 @@ a security issue, we'd like to know about it, and also how to fix it:
|
||||
- Authentication bypass
|
||||
- Server-side code execution
|
||||
|
||||
## What Wekan bugs are NOT eligible?
|
||||
## What WeKan bugs are NOT eligible?
|
||||
|
||||
Typical already known or "no impact" bugs such as:
|
||||
Typical already-known or 'no impact' bugs such as:
|
||||
|
||||
- [Wekan API old tokens not replaced correctly](https://github.com/wekan/wekan/issues/1437)
|
||||
- Missing Cookie flags on non-session cookies or 3rd party cookies
|
||||
- Logout CSRF
|
||||
- Social engineering
|
||||
- Denial of service
|
||||
- SSL BEAST/CRIME/etc. Wekan does not have SSL built-in, it uses Caddy/Nginx/Apache etc at front.
|
||||
Integrated Caddy support is updated often.
|
||||
- Email spoofing, SPF, DMARC & DKIM. Wekan does not include email server.
|
||||
- SSL BEAST/CRIME/etc. WeKan does not have SSL built-in; it uses Caddy/Nginx/Apache on the front end.
|
||||
|
||||
Wekan is Open Source with MIT license, and free to use also for commercial use.
|
||||
We welcome all fixes to improve security by email to security@wekan.team
|
||||
|
||||
## Bonus Points
|
||||
|
||||
If your Responsible Security Disclosure includes code for fixing security issue,
|
||||
you get bonus points, as seen on [Hall of Fame](https://wekan.github.io/hall-of-fame).
|
||||
WeKan is Open Source with MIT license, and free to use also for commercial use.
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v7.99.0"
|
||||
appVersion: "v9.42.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -25,39 +25,99 @@ syntax = """=== Wekan API Python CLI: Shows IDs for addcard ===
|
||||
# AUTHORID is USERID that writes card or custom field.
|
||||
If *nix: chmod +x api.py => ./api.py users
|
||||
Syntax:
|
||||
User API:
|
||||
User API:
|
||||
python3 api.py user # Current user and list of current user boards
|
||||
python3 api.py users # All users
|
||||
python3 api.py boards # All Public Boards
|
||||
python3 api.py boards USERID # Boards of USERID
|
||||
python3 api.py board BOARDID # Info of BOARDID
|
||||
|
||||
Board/List/Card API:
|
||||
python3 api.py swimlanes BOARDID # Swimlanes of BOARDID
|
||||
python3 api.py lists BOARDID # Lists of BOARDID
|
||||
python3 api.py list BOARDID LISTID # Info of LISTID
|
||||
python3 api.py createlist BOARDID LISTTITLE # Create list
|
||||
python3 api.py addcard AUTHORID BOARDID SWIMLANEID LISTID CARDTITLE CARDDESCRIPTION
|
||||
python3 api.py editcard BOARDID LISTID CARDID NEWCARDTITLE NEWCARDDESCRIPTION
|
||||
python3 api.py getcard BOARDID LISTID CARDID # Get card info
|
||||
python3 api.py cardsbyswimlane BOARDID SWIMLANEID # Retrieve cards list on a swimlane
|
||||
python3 api.py deleteallcards BOARDID SWIMLANEID ( * Be careful will delete ALL CARDS INSIDE the swimlanes automatically in every list * ) # Delete all cards on a swimlane
|
||||
python3 api.py get_list_cards_count BOARDID LISTID # Retrieve how many cards in a list
|
||||
python3 api.py get_board_cards_count BOARDID # Retrieve how many cards in a board
|
||||
python3 api.py editboardtitle BOARDID NEWBOARDTITLE # Edit board title
|
||||
python3 api.py copyboard BOARDID NEWBOARDTITLE # Copy a board
|
||||
|
||||
Custom Fields / Labels API:
|
||||
python3 api.py customfields BOARDID # Custom Fields of BOARDID
|
||||
python3 api.py customfield BOARDID CUSTOMFIELDID # Info of CUSTOMFIELDID
|
||||
python3 api.py addcustomfieldtoboard AUTHORID BOARDID NAME TYPE SETTINGS SHOWONCARD AUTOMATICALLYONCARD SHOWLABELONMINICARD SHOWSUMATTOPOFLIST # Add Custom Field to Board
|
||||
python3 api.py editcustomfield BOARDID LISTID CARDID CUSTOMFIELDID NEWCUSTOMFIELDVALUE # Edit Custom Field
|
||||
python3 api.py listattachments BOARDID # List attachments
|
||||
python3 api.py cardsbyswimlane SWIMLANEID LISTID # Retrieve cards list on a swimlane
|
||||
python3 api.py getcard BOARDID LISTID CARDID # Get card info
|
||||
python3 api.py addlabel BOARDID LISTID CARDID LABELID # Add label to a card
|
||||
python3 api.py addcardwithlabel AUTHORID BOARDID SWIMLANEID LISTID CARDTITLE CARDDESCRIPTION LABELIDS # Add a card and a label
|
||||
python3 api.py editboardtitle BOARDID NEWBOARDTITLE # Edit board title
|
||||
python3 api.py copyboard BOARDID NEWBOARDTITLE # Copy a board
|
||||
python3 api.py createlabel BOARDID LABELCOLOR LABELNAME (Color available: `white`, `green`, `yellow`, `orange`, `red`, `purple`, `blue`, `sky`, `lime`, `pink`, `black`, `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`, `slateblue`, `magenta`, `gold`, `navy`, `gray`, `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`) # Create a new label
|
||||
python3 api.py editcardcolor BOARDID LISTID CARDID COLOR (Color available: `white`, `green`, `yellow`, `orange`, `red`, `purple`, `blue`, `sky`, `lime`, `pink`, `black`, `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`, `slateblue`, `magenta`, `gold`, `navy`, `gray`, `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`) # Edit card color
|
||||
python3 api.py addchecklist BOARDID CARDID TITLE ITEM1 ITEM2 ITEM3 ITEM4 (You can add multiple items or just one, or also without any item, just TITLE works as well. * If items or Title contains spaces, you should add ' between them.) # Add checklist + item on a card
|
||||
python3 api.py deleteallcards BOARDID SWIMLANEID ( * Be careful will delete ALL CARDS INSIDE the swimlanes automatically in every list * ) # Delete all cards on a swimlane
|
||||
python3 api.py checklistid BOARDID CARDID # Retrieve Checklist ID attached to a card
|
||||
python3 api.py checklistinfo BOARDID CARDID CHECKLISTID # Get checklist info
|
||||
python3 api.py get_list_cards_count BOARDID LISTID # Retrieve how many cards in a list
|
||||
python3 api.py get_board_cards_count BOARDID # Retrieve how many cards in a board
|
||||
python3 api.py createlabel BOARDID LABELCOLOR LABELNAME (Color available: `white`, `green`, `yellow`, `orange`, `red`, `purple`, `blue`, `sky`, `lime`, `pink`, `black`, `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`, `slateblue`, `magenta`, `gold`, `navy`, `gray`, `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`) # Create a new label
|
||||
python3 api.py addlabel BOARDID LISTID CARDID LABELID # Add label to a card
|
||||
python3 api.py addcardwithlabel AUTHORID BOARDID SWIMLANEID LISTID CARDTITLE CARDDESCRIPTION LABELIDS # Add a card and a label
|
||||
python3 api.py editcardcolor BOARDID LISTID CARDID COLOR (Color available: `white`, `green`, `yellow`, `orange`, `red`, `purple`, `blue`, `sky`, `lime`, `pink`, `black`, `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`, `slateblue`, `magenta`, `gold`, `navy`, `gray`, `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`) # Edit card color
|
||||
|
||||
Checklist API:
|
||||
python3 api.py addchecklist BOARDID CARDID TITLE ITEM1 ITEM2 ITEM3 ITEM4 (You can add multiple items or just one, or also without any item, just TITLE works as well. * If items or Title contains spaces, you should add ' between them.) # Add checklist + item on a card
|
||||
python3 api.py checklistid BOARDID CARDID # Retrieve Checklist ID attached to a card
|
||||
python3 api.py checklistinfo BOARDID CARDID CHECKLISTID # Get checklist info
|
||||
|
||||
Attachment API:
|
||||
python3 api.py listattachments BOARDID # List attachments
|
||||
python3 api.py uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND] # Upload attachment to card
|
||||
python3 api.py downloadattachment ATTACHMENTID OUTPUTPATH # Download attachment to local file
|
||||
python3 api.py attachmentinfo ATTACHMENTID # Get attachment information
|
||||
python3 api.py listcardattachments BOARDID SWIMLANEID LISTID CARDID # List attachments for specific card
|
||||
python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move] # Copy or move attachment
|
||||
python3 api.py deleteattachment ATTACHMENTID # Delete attachment
|
||||
|
||||
Board Member API (Issue #5998):
|
||||
python3 api.py addboardmember BOARDID USERID ROLE # Add/activate board member. ROLE: admin, normal, comment, readonly, worker, normalassignedonly, commentassignedonly, readassignedonly, nocomments
|
||||
python3 api.py removeboardmember BOARDID USERID # Remove (deactivate) board member
|
||||
python3 api.py setboardmemberrole BOARDID USERID ROLE # Change permission/role of existing board member
|
||||
|
||||
Card Member / Assignee API (board member -> card member/assignee):
|
||||
python3 api.py setcardmembers BOARDID LISTID CARDID MEMBERIDS # Set card members. MEMBERIDS = comma-separated userIds, or '' to clear
|
||||
python3 api.py setcardassignees BOARDID LISTID CARDID ASSIGNEEIDS # Set card assignees. ASSIGNEEIDS = comma-separated userIds, or '' to clear
|
||||
|
||||
Card Dates API (Issue #5846):
|
||||
python3 api.py setcarddate BOARDID LISTID CARDID DATETYPE [DATEVALUE] # DATETYPE: received, start, due, end. DATEVALUE: ISO 8601, e.g. 2026-06-07T12:00:00.000Z. Omit DATEVALUE (or pass '') to clear the date.
|
||||
|
||||
Card Labels API (Issue #5819):
|
||||
python3 api.py setcardlabels BOARDID LISTID CARDID LABELIDS # Set (replace) card labels. LABELIDS = comma-separated labelIds, or '' to clear
|
||||
|
||||
Card Copy/Move API:
|
||||
python3 api.py movecard BOARDID LISTID CARDID NEWBOARDID NEWSWIMLANEID NEWLISTID # Move card to another board/swimlane/list
|
||||
|
||||
Bulk Card API (Issue #4743, #5819):
|
||||
python3 api.py bulkaddcards BOARDID LISTID AUTHORID SWIMLANEID CARDSJSONFILE # Create many cards in one request. CARDSJSONFILE = JSON file with a list of titles, or a list of card objects {title,description,members,assignees,...}
|
||||
python3 api.py bulkdeletecards BOARDID CARDIDS # Delete many cards in one request. CARDIDS = comma-separated cardIds
|
||||
python3 api.py bulkcardlabels BOARDID CARDIDS ADDLABELIDS REMOVELABELIDS # Merge-add/remove labels across many cards. CARDIDS/ADDLABELIDS/REMOVELABELIDS = comma-separated, or '' for none
|
||||
|
||||
Linked Card API (Issue #5897):
|
||||
python3 api.py linkcard BOARDID LISTID SWIMLANEID AUTHORID LINKEDCARDID # Create a linked card in BOARDID/LISTID that references LINKEDCARDID (may be on another board)
|
||||
|
||||
Card Member/Assignee Merge API (Issue #5998):
|
||||
python3 api.py addcardmember BOARDID LISTID CARDID MEMBERID # Add one board member to a card (validated)
|
||||
python3 api.py removecardmember BOARDID LISTID CARDID MEMBERID # Remove one member from a card
|
||||
python3 api.py addcardassignee BOARDID LISTID CARDID ASSIGNEEID # Add one assignee to a card (validated)
|
||||
python3 api.py removecardassignee BOARDID LISTID CARDID ASSIGNEEID # Remove one assignee from a card
|
||||
|
||||
Copy/Move Swimlane/List/Card API (position = 0-based index from top-left):
|
||||
python3 api.py copycard BOARDID LISTID CARDID TOSWIMLANEID [TOBOARDID] [TOLISTID] [POSITION] # Copy a card (deep copy)
|
||||
python3 api.py copyswimlane BOARDID SWIMLANEID [TOBOARDID] [POSITION] # Copy a swimlane (deep copy)
|
||||
python3 api.py moveswimlane BOARDID SWIMLANEID [TOBOARDID] [POSITION] # Move a swimlane
|
||||
python3 api.py copylist BOARDID LISTID TOSWIMLANEID [TOBOARDID] [POSITION] # Copy a list (deep copy)
|
||||
python3 api.py movelist BOARDID LISTID [TOSWIMLANEID] [TOBOARDID] [POSITION] # Move a list
|
||||
|
||||
My Cards / Due Cards API (Issue #4815):
|
||||
python3 api.py mycards [due] [FROM] [TO] # Current user's cards. 'due' = only cards with a due date; FROM/TO = ISO 8601 due-date range
|
||||
|
||||
Card Settings API (Issue #3062):
|
||||
python3 api.py getcardsettings BOARDID # Get board-level card settings (allows* toggles)
|
||||
python3 api.py setcardsetting BOARDID KEY VALUE # Set one board card setting, e.g. allowsDueDate true
|
||||
|
||||
|
||||
Admin API:
|
||||
python3 api.py users # All users
|
||||
python3 api.py boards # All Public Boards
|
||||
python3 api.py newuser USERNAME EMAIL PASSWORD
|
||||
"""
|
||||
|
||||
@@ -717,9 +777,9 @@ if arguments == 2:
|
||||
print(f"Error: {e}")
|
||||
# ------- GET BOARD CARDS COUNT END -----------
|
||||
|
||||
if arguments == 1:
|
||||
if arguments >= 1:
|
||||
|
||||
if sys.argv[1] == 'users':
|
||||
if arguments == 1 and sys.argv[1] == 'users':
|
||||
|
||||
# ------- LIST OF USERS START -----------
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
@@ -730,7 +790,7 @@ if arguments == 1:
|
||||
print(data2)
|
||||
# ------- LIST OF USERS END -----------
|
||||
|
||||
if sys.argv[1] == 'user':
|
||||
if arguments == 1 and sys.argv[1] == 'user':
|
||||
# ------- LIST OF ALL USERS START -----------
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
print(user)
|
||||
@@ -740,7 +800,7 @@ if arguments == 1:
|
||||
print(data2)
|
||||
# ------- LIST OF ALL USERS END -----------
|
||||
|
||||
if sys.argv[1] == 'boards':
|
||||
if arguments == 1 and sys.argv[1] == 'boards':
|
||||
|
||||
# ------- LIST OF PUBLIC BOARDS START -----------
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
@@ -750,3 +810,681 @@ if arguments == 1:
|
||||
data2 = body.text.replace('}',"}\n")
|
||||
print(data2)
|
||||
# ------- LIST OF PUBLIC BOARDS END -----------
|
||||
|
||||
# ------- NEW ATTACHMENT API ENDPOINTS START -----------
|
||||
|
||||
if sys.argv[1] == 'uploadattachment':
|
||||
# ------- UPLOAD ATTACHMENT START -----------
|
||||
if arguments < 6:
|
||||
print("Usage: python3 api.py uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND]")
|
||||
print("Storage backends: fs, gridfs, s3")
|
||||
exit(1)
|
||||
|
||||
boardid = sys.argv[2]
|
||||
swimlaneid = sys.argv[3]
|
||||
listid = sys.argv[4]
|
||||
cardid = sys.argv[5]
|
||||
filepath = sys.argv[6]
|
||||
storage_backend = sys.argv[7] if arguments > 6 else None
|
||||
|
||||
# Read file and convert to base64
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
file_data = f.read()
|
||||
import base64
|
||||
base64_data = base64.b64encode(file_data).decode('utf-8')
|
||||
except FileNotFoundError:
|
||||
print(f"Error: File '{filepath}' not found")
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error reading file: {e}")
|
||||
exit(1)
|
||||
|
||||
# Get file info
|
||||
import os
|
||||
filename = os.path.basename(filepath)
|
||||
import mimetypes
|
||||
file_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
|
||||
|
||||
# Prepare request data
|
||||
upload_data = {
|
||||
'boardId': boardid,
|
||||
'swimlaneId': swimlaneid,
|
||||
'listId': listid,
|
||||
'cardId': cardid,
|
||||
'fileData': base64_data,
|
||||
'fileName': filename,
|
||||
'fileType': file_type
|
||||
}
|
||||
|
||||
if storage_backend:
|
||||
upload_data['storageBackend'] = storage_backend
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
upload_url = wekanurl + 'api/attachment/upload'
|
||||
|
||||
try:
|
||||
response = requests.post(upload_url, headers=headers, json=upload_data)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
print(f"Upload successful!")
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"File: {result.get('fileName')}")
|
||||
print(f"Size: {result.get('fileSize')} bytes")
|
||||
print(f"Storage: {result.get('storageBackend')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Upload failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- UPLOAD ATTACHMENT END -----------
|
||||
|
||||
if sys.argv[1] == 'downloadattachment':
|
||||
# ------- DOWNLOAD ATTACHMENT START -----------
|
||||
if arguments < 3:
|
||||
print("Usage: python3 api.py downloadattachment ATTACHMENTID OUTPUTPATH")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
outputpath = sys.argv[3]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
download_url = wekanurl + f'api/attachment/download/{attachmentid}'
|
||||
|
||||
try:
|
||||
response = requests.get(download_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
# Decode base64 data and save to file
|
||||
import base64
|
||||
file_data = base64.b64decode(result.get('base64Data'))
|
||||
|
||||
with open(outputpath, 'wb') as f:
|
||||
f.write(file_data)
|
||||
|
||||
print(f"Download successful!")
|
||||
print(f"File saved to: {outputpath}")
|
||||
print(f"Original filename: {result.get('fileName')}")
|
||||
print(f"Size: {result.get('fileSize')} bytes")
|
||||
print(f"Storage: {result.get('storageBackend')}")
|
||||
else:
|
||||
print(f"Download failed: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Download failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- DOWNLOAD ATTACHMENT END -----------
|
||||
|
||||
if sys.argv[1] == 'attachmentinfo':
|
||||
# ------- ATTACHMENT INFO START -----------
|
||||
if arguments < 2:
|
||||
print("Usage: python3 api.py attachmentinfo ATTACHMENTID")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
info_url = wekanurl + f'api/attachment/info/{attachmentid}'
|
||||
|
||||
try:
|
||||
response = requests.get(info_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
print("=== ATTACHMENT INFO ===")
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"File Name: {result.get('fileName')}")
|
||||
print(f"File Size: {result.get('fileSize')} bytes")
|
||||
print(f"File Type: {result.get('fileType')}")
|
||||
print(f"Storage Backend: {result.get('storageBackend')}")
|
||||
print(f"Board ID: {result.get('boardId')}")
|
||||
print(f"Swimlane ID: {result.get('swimlaneId')}")
|
||||
print(f"List ID: {result.get('listId')}")
|
||||
print(f"Card ID: {result.get('cardId')}")
|
||||
print(f"Created At: {result.get('createdAt')}")
|
||||
print(f"Is Image: {result.get('isImage')}")
|
||||
print(f"Versions: {len(result.get('versions', []))}")
|
||||
else:
|
||||
print(f"Failed to get attachment info: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- ATTACHMENT INFO END -----------
|
||||
|
||||
if sys.argv[1] == 'listcardattachments':
|
||||
# ------- LIST CARD ATTACHMENTS START -----------
|
||||
if arguments < 5:
|
||||
print("Usage: python3 api.py listcardattachments BOARDID SWIMLANEID LISTID CARDID")
|
||||
exit(1)
|
||||
|
||||
boardid = sys.argv[2]
|
||||
swimlaneid = sys.argv[3]
|
||||
listid = sys.argv[4]
|
||||
cardid = sys.argv[5]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
list_url = wekanurl + f'api/attachment/list/{boardid}/{swimlaneid}/{listid}/{cardid}'
|
||||
|
||||
try:
|
||||
response = requests.get(list_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
attachments = result.get('attachments', [])
|
||||
print(f"=== CARD ATTACHMENTS ({len(attachments)}) ===")
|
||||
for attachment in attachments:
|
||||
print(f"ID: {attachment.get('attachmentId')}")
|
||||
print(f"Name: {attachment.get('fileName')}")
|
||||
print(f"Size: {attachment.get('fileSize')} bytes")
|
||||
print(f"Type: {attachment.get('fileType')}")
|
||||
print(f"Storage: {attachment.get('storageBackend')}")
|
||||
print(f"Created: {attachment.get('createdAt')}")
|
||||
print("---")
|
||||
else:
|
||||
print(f"Failed to list attachments: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- LIST CARD ATTACHMENTS END -----------
|
||||
|
||||
if sys.argv[1] == 'copymoveattachment':
|
||||
# ------- COPY/MOVE ATTACHMENT START -----------
|
||||
if arguments < 6:
|
||||
print("Usage: python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move]")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
targetboardid = sys.argv[3]
|
||||
targetswimlaneid = sys.argv[4]
|
||||
targetlistid = sys.argv[5]
|
||||
targetcardid = sys.argv[6]
|
||||
operation = sys.argv[7] if arguments > 6 else 'copy'
|
||||
|
||||
if operation not in ['copy', 'move']:
|
||||
print("Operation must be 'copy' or 'move'")
|
||||
exit(1)
|
||||
|
||||
# Prepare request data
|
||||
request_data = {
|
||||
'attachmentId': attachmentid,
|
||||
'targetBoardId': targetboardid,
|
||||
'targetSwimlaneId': targetswimlaneid,
|
||||
'targetListId': targetlistid,
|
||||
'targetCardId': targetcardid
|
||||
}
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
api_url = wekanurl + f'api/attachment/{operation}'
|
||||
|
||||
try:
|
||||
response = requests.post(api_url, headers=headers, json=request_data)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
print(f"{operation.capitalize()} successful!")
|
||||
if operation == 'copy':
|
||||
print(f"Source Attachment ID: {result.get('sourceAttachmentId')}")
|
||||
print(f"New Attachment ID: {result.get('newAttachmentId')}")
|
||||
else:
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"Source Board: {result.get('sourceBoardId')}")
|
||||
print(f"Target Board: {result.get('targetBoardId')}")
|
||||
print(f"File: {result.get('fileName')}")
|
||||
print(f"Size: {result.get('fileSize')} bytes")
|
||||
else:
|
||||
print(f"{operation.capitalize()} failed: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"{operation.capitalize()} failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- COPY/MOVE ATTACHMENT END -----------
|
||||
|
||||
if sys.argv[1] == 'deleteattachment':
|
||||
# ------- DELETE ATTACHMENT START -----------
|
||||
if arguments < 2:
|
||||
print("Usage: python3 api.py deleteattachment ATTACHMENTID")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
delete_url = wekanurl + f'api/attachment/delete/{attachmentid}'
|
||||
|
||||
try:
|
||||
response = requests.delete(delete_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
print("Delete successful!")
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"File: {result.get('fileName')}")
|
||||
else:
|
||||
print(f"Delete failed: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Delete failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- DELETE ATTACHMENT END -----------
|
||||
|
||||
# ------- NEW ATTACHMENT API ENDPOINTS END -----------
|
||||
|
||||
# ------- BOARD MEMBER / CARD FIELD API ENDPOINTS START -----------
|
||||
# These commands wrap REST endpoints that already exist in Wekan.
|
||||
# See CHANGELOG.md "Upcoming" section for related issues and design notes.
|
||||
|
||||
# Map a human readable ROLE to the board member permission boolean flags
|
||||
# used by /api/boards/:boardId/members/:userId/add and .../members/:memberId
|
||||
def board_role_flags(role):
|
||||
flags = {
|
||||
'isAdmin': 'false',
|
||||
'isNoComments': 'false',
|
||||
'isCommentOnly': 'false',
|
||||
'isWorker': 'false',
|
||||
'isNormalAssignedOnly': 'false',
|
||||
'isCommentAssignedOnly': 'false',
|
||||
'isReadOnly': 'false',
|
||||
'isReadAssignedOnly': 'false',
|
||||
}
|
||||
role = (role or 'normal').lower()
|
||||
rolemap = {
|
||||
'admin': 'isAdmin',
|
||||
'nocomments': 'isNoComments',
|
||||
'comment': 'isCommentOnly',
|
||||
'commentonly': 'isCommentOnly',
|
||||
'worker': 'isWorker',
|
||||
'normalassignedonly': 'isNormalAssignedOnly',
|
||||
'commentassignedonly': 'isCommentAssignedOnly',
|
||||
'readonly': 'isReadOnly',
|
||||
'readassignedonly': 'isReadAssignedOnly',
|
||||
}
|
||||
if role in ('normal', 'member'):
|
||||
return flags # all false == Normal board member
|
||||
if role in rolemap:
|
||||
flags[rolemap[role]] = 'true'
|
||||
return flags
|
||||
print("Invalid ROLE. Valid: admin, normal, comment, readonly, worker, "
|
||||
"normalassignedonly, commentassignedonly, readassignedonly, nocomments")
|
||||
sys.exit(1)
|
||||
|
||||
if sys.argv[1] == 'addboardmember':
|
||||
# ------- ADD BOARD MEMBER START -----------
|
||||
if arguments < 3:
|
||||
print("Usage: python3 api.py addboardmember BOARDID USERID ROLE")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
userid = sys.argv[3]
|
||||
role = sys.argv[4] if arguments > 3 else 'normal'
|
||||
add_member_url = wekanurl + apiboards + boardid + s + 'members' + s + userid + s + 'add'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
post_data = {'action': 'add'}
|
||||
post_data.update(board_role_flags(role))
|
||||
body = requests.post(add_member_url, data=post_data, headers=headers)
|
||||
print("=== ADD BOARD MEMBER ===\n")
|
||||
print(body.text)
|
||||
# ------- ADD BOARD MEMBER END -----------
|
||||
|
||||
if sys.argv[1] == 'removeboardmember':
|
||||
# ------- REMOVE BOARD MEMBER START -----------
|
||||
if arguments < 3:
|
||||
print("Usage: python3 api.py removeboardmember BOARDID USERID")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
userid = sys.argv[3]
|
||||
remove_member_url = wekanurl + apiboards + boardid + s + 'members' + s + userid + s + 'remove'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
post_data = {'action': 'remove'}
|
||||
body = requests.post(remove_member_url, data=post_data, headers=headers)
|
||||
print("=== REMOVE BOARD MEMBER ===\n")
|
||||
print(body.text)
|
||||
# ------- REMOVE BOARD MEMBER END -----------
|
||||
|
||||
if sys.argv[1] == 'setboardmemberrole':
|
||||
# ------- SET BOARD MEMBER ROLE START -----------
|
||||
if arguments < 4:
|
||||
print("Usage: python3 api.py setboardmemberrole BOARDID USERID ROLE")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
userid = sys.argv[3]
|
||||
role = sys.argv[4]
|
||||
set_role_url = wekanurl + apiboards + boardid + s + 'members' + s + userid
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
post_data = board_role_flags(role)
|
||||
body = requests.post(set_role_url, data=post_data, headers=headers)
|
||||
print("=== SET BOARD MEMBER ROLE ===\n")
|
||||
print(body.text)
|
||||
# ------- SET BOARD MEMBER ROLE END -----------
|
||||
|
||||
if sys.argv[1] in ('setcardmembers', 'setcardassignees'):
|
||||
# ------- SET CARD MEMBERS / ASSIGNEES START -----------
|
||||
if arguments < 4:
|
||||
print("Usage: python3 api.py {} BOARDID LISTID CARDID IDS".format(sys.argv[1]))
|
||||
print("IDS = comma-separated userIds, or '' to clear")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
cardid = sys.argv[4]
|
||||
ids_raw = sys.argv[5] if arguments > 4 else ''
|
||||
ids = [x for x in ids_raw.split(',') if x] if ids_raw else []
|
||||
field = 'members' if sys.argv[1] == 'setcardmembers' else 'assignees'
|
||||
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
# Empty array clears the field; send as JSON so the array type is preserved.
|
||||
put_data = {field: ids if ids else []}
|
||||
body = requests.put(edcard, json=put_data, headers=headers)
|
||||
print("=== SET CARD {} ===\n".format(field.upper()))
|
||||
print(body.text)
|
||||
# ------- SET CARD MEMBERS / ASSIGNEES END -----------
|
||||
|
||||
if sys.argv[1] == 'setcarddate':
|
||||
# ------- SET CARD DATE START -----------
|
||||
if arguments < 5:
|
||||
print("Usage: python3 api.py setcarddate BOARDID LISTID CARDID DATETYPE DATEVALUE")
|
||||
print("DATETYPE: received, start, due, end")
|
||||
print("DATEVALUE: ISO 8601, e.g. 2026-06-07T12:00:00.000Z")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
cardid = sys.argv[4]
|
||||
datetype = sys.argv[5].lower()
|
||||
datevalue = sys.argv[6] if arguments > 5 else ''
|
||||
datemap = {
|
||||
'received': 'receivedAt',
|
||||
'start': 'startAt',
|
||||
'due': 'dueAt',
|
||||
'end': 'endAt',
|
||||
}
|
||||
if datetype not in datemap:
|
||||
print("Invalid DATETYPE. Valid: received, start, due, end")
|
||||
sys.exit(1)
|
||||
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
put_data = {datemap[datetype]: datevalue}
|
||||
body = requests.put(edcard, data=put_data, headers=headers)
|
||||
print("=== SET CARD DATE {} ===\n".format(datetype.upper()))
|
||||
print(body.text)
|
||||
# ------- SET CARD DATE END -----------
|
||||
|
||||
if sys.argv[1] == 'setcardlabels':
|
||||
# ------- SET CARD LABELS START -----------
|
||||
if arguments < 4:
|
||||
print("Usage: python3 api.py setcardlabels BOARDID LISTID CARDID LABELIDS")
|
||||
print("LABELIDS = comma-separated labelIds, or '' to clear")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
cardid = sys.argv[4]
|
||||
labelids_raw = sys.argv[5] if arguments > 4 else ''
|
||||
labelids = [x for x in labelids_raw.split(',') if x] if labelids_raw else []
|
||||
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
# NOTE: this REPLACES the card's labels. Bulk add/remove (merge) is not yet
|
||||
# implemented server-side, see Issue #5819 in CHANGELOG.md "Upcoming".
|
||||
put_data = {'labelIds': labelids if labelids else []}
|
||||
body = requests.put(edcard, json=put_data, headers=headers)
|
||||
print("=== SET CARD LABELS ===\n")
|
||||
print(body.text)
|
||||
# ------- SET CARD LABELS END -----------
|
||||
|
||||
if sys.argv[1] == 'movecard':
|
||||
# ------- MOVE CARD START -----------
|
||||
if arguments < 7:
|
||||
print("Usage: python3 api.py movecard BOARDID LISTID CARDID NEWBOARDID NEWSWIMLANEID NEWLISTID")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
cardid = sys.argv[4]
|
||||
newboardid = sys.argv[5]
|
||||
newswimlaneid = sys.argv[6]
|
||||
newlistid = sys.argv[7]
|
||||
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
put_data = {
|
||||
'newBoardId': newboardid,
|
||||
'newSwimlaneId': newswimlaneid,
|
||||
'newListId': newlistid,
|
||||
}
|
||||
body = requests.put(edcard, data=put_data, headers=headers)
|
||||
print("=== MOVE CARD ===\n")
|
||||
print(body.text)
|
||||
# ------- MOVE CARD END -----------
|
||||
|
||||
if sys.argv[1] == 'bulkaddcards':
|
||||
# ------- BULK ADD CARDS START -----------
|
||||
if arguments < 5:
|
||||
print("Usage: python3 api.py bulkaddcards BOARDID LISTID AUTHORID SWIMLANEID CARDSJSONFILE")
|
||||
print("CARDSJSONFILE = JSON file with a list of titles, e.g. [\"Card A\", \"Card B\"],")
|
||||
print(" or a list of card objects [{\"title\": \"Card A\", \"description\": \"...\"}, ...]")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
authorid = sys.argv[4]
|
||||
swimlaneid = sys.argv[5]
|
||||
cardsjsonfile = sys.argv[6] if arguments > 5 else None
|
||||
if not cardsjsonfile:
|
||||
print("CARDSJSONFILE is required")
|
||||
exit(1)
|
||||
try:
|
||||
with open(cardsjsonfile, 'r') as f:
|
||||
raw_cards = json.load(f)
|
||||
except Exception as e:
|
||||
print("Error reading CARDSJSONFILE: {}".format(e))
|
||||
exit(1)
|
||||
if not isinstance(raw_cards, list) or len(raw_cards) == 0:
|
||||
print("CARDSJSONFILE must contain a non-empty JSON list")
|
||||
exit(1)
|
||||
cards = []
|
||||
for item in raw_cards:
|
||||
if isinstance(item, str):
|
||||
cards.append({'title': item})
|
||||
elif isinstance(item, dict):
|
||||
cards.append(item)
|
||||
else:
|
||||
print("Each card must be a string (title) or an object")
|
||||
exit(1)
|
||||
bulk_url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + 'bulk'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
post_data = {'authorId': authorid, 'swimlaneId': swimlaneid, 'cards': cards}
|
||||
body = requests.post(bulk_url, json=post_data, headers=headers)
|
||||
print("=== BULK ADD CARDS ===\n")
|
||||
print(body.text)
|
||||
# ------- BULK ADD CARDS END -----------
|
||||
|
||||
if sys.argv[1] == 'bulkdeletecards':
|
||||
# ------- BULK DELETE CARDS START -----------
|
||||
if arguments < 3:
|
||||
print("Usage: python3 api.py bulkdeletecards BOARDID CARDIDS")
|
||||
print("CARDIDS = comma-separated cardIds")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
cardids_raw = sys.argv[3]
|
||||
cardids = [x for x in cardids_raw.split(',') if x]
|
||||
bulk_url = wekanurl + apiboards + boardid + s + cs + s + 'bulk'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
# requests sends a body with DELETE via the json= parameter.
|
||||
body = requests.delete(bulk_url, json={'cardIds': cardids}, headers=headers)
|
||||
print("=== BULK DELETE CARDS ===\n")
|
||||
print(body.text)
|
||||
# ------- BULK DELETE CARDS END -----------
|
||||
|
||||
if sys.argv[1] == 'bulkcardlabels':
|
||||
# ------- BULK CARD LABELS START -----------
|
||||
if arguments < 5:
|
||||
print("Usage: python3 api.py bulkcardlabels BOARDID CARDIDS ADDLABELIDS REMOVELABELIDS")
|
||||
print("CARDIDS / ADDLABELIDS / REMOVELABELIDS = comma-separated, or '' for none")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
cardids = [x for x in sys.argv[3].split(',') if x]
|
||||
add_labelids = [x for x in sys.argv[4].split(',') if x]
|
||||
remove_labelids = [x for x in sys.argv[5].split(',') if x]
|
||||
bulk_url = wekanurl + apiboards + boardid + s + cs + s + 'labels'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
post_data = {'cardIds': cardids, 'addLabelIds': add_labelids, 'removeLabelIds': remove_labelids}
|
||||
body = requests.post(bulk_url, json=post_data, headers=headers)
|
||||
print("=== BULK CARD LABELS ===\n")
|
||||
print(body.text)
|
||||
# ------- BULK CARD LABELS END -----------
|
||||
|
||||
if sys.argv[1] == 'linkcard':
|
||||
# ------- CREATE LINKED CARD START -----------
|
||||
if arguments < 6:
|
||||
print("Usage: python3 api.py linkcard BOARDID LISTID SWIMLANEID AUTHORID LINKEDCARDID")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
swimlaneid = sys.argv[4]
|
||||
authorid = sys.argv[5]
|
||||
linkedcardid = sys.argv[6]
|
||||
link_url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
post_data = {'authorId': authorid, 'swimlaneId': swimlaneid, 'linkedId': linkedcardid}
|
||||
body = requests.post(link_url, data=post_data, headers=headers)
|
||||
print("=== CREATE LINKED CARD ===\n")
|
||||
print(body.text)
|
||||
# ------- CREATE LINKED CARD END -----------
|
||||
|
||||
if sys.argv[1] in ('addcardmember', 'removecardmember', 'addcardassignee', 'removecardassignee'):
|
||||
# ------- ADD/REMOVE CARD MEMBER / ASSIGNEE START -----------
|
||||
if arguments < 5:
|
||||
print("Usage: python3 api.py {} BOARDID LISTID CARDID USERID".format(sys.argv[1]))
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
cardid = sys.argv[4]
|
||||
userid = sys.argv[5]
|
||||
field = 'members' if 'member' in sys.argv[1] else 'assignees'
|
||||
url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid + s + field + s + userid
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
if sys.argv[1].startswith('add'):
|
||||
body = requests.post(url, headers=headers)
|
||||
else:
|
||||
body = requests.delete(url, headers=headers)
|
||||
print("=== {} ===\n".format(sys.argv[1].upper()))
|
||||
print(body.text)
|
||||
# ------- ADD/REMOVE CARD MEMBER / ASSIGNEE END -----------
|
||||
|
||||
if sys.argv[1] == 'copycard':
|
||||
# ------- COPY CARD START -----------
|
||||
if arguments < 4:
|
||||
print("Usage: python3 api.py copycard BOARDID LISTID CARDID TOSWIMLANEID [TOBOARDID] [TOLISTID] [POSITION]")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
cardid = sys.argv[4]
|
||||
toswimlaneid = sys.argv[5] if arguments > 4 else None
|
||||
if not toswimlaneid:
|
||||
print("TOSWIMLANEID is required")
|
||||
exit(1)
|
||||
toboardid = sys.argv[6] if arguments > 5 else boardid
|
||||
tolistid = sys.argv[7] if arguments > 6 else listid
|
||||
url = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid + s + 'copy'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
post_data = {'toBoardId': toboardid, 'toSwimlaneId': toswimlaneid, 'toListId': tolistid}
|
||||
if arguments > 7:
|
||||
post_data['position'] = int(sys.argv[8])
|
||||
body = requests.post(url, json=post_data, headers=headers)
|
||||
print("=== COPY CARD ===\n")
|
||||
print(body.text)
|
||||
# ------- COPY CARD END -----------
|
||||
|
||||
if sys.argv[1] in ('copyswimlane', 'moveswimlane'):
|
||||
# ------- COPY/MOVE SWIMLANE START -----------
|
||||
if arguments < 3:
|
||||
print("Usage: python3 api.py {} BOARDID SWIMLANEID [TOBOARDID] [POSITION]".format(sys.argv[1]))
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
swimlaneid = sys.argv[3]
|
||||
toboardid = sys.argv[4] if arguments > 3 else boardid
|
||||
action = 'copy' if sys.argv[1] == 'copyswimlane' else 'move'
|
||||
url = wekanurl + apiboards + boardid + s + sws + s + swimlaneid + s + action
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
post_data = {'toBoardId': toboardid}
|
||||
if arguments > 4:
|
||||
post_data['position'] = int(sys.argv[5])
|
||||
body = requests.post(url, json=post_data, headers=headers)
|
||||
print("=== {} ===\n".format(sys.argv[1].upper()))
|
||||
print(body.text)
|
||||
# ------- COPY/MOVE SWIMLANE END -----------
|
||||
|
||||
if sys.argv[1] in ('copylist', 'movelist'):
|
||||
# ------- COPY/MOVE LIST START -----------
|
||||
if arguments < 3:
|
||||
print("Usage: python3 api.py {} BOARDID LISTID [TOSWIMLANEID] [TOBOARDID] [POSITION]".format(sys.argv[1]))
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
listid = sys.argv[3]
|
||||
toswimlaneid = sys.argv[4] if arguments > 3 else None
|
||||
toboardid = sys.argv[5] if arguments > 4 else boardid
|
||||
action = 'copy' if sys.argv[1] == 'copylist' else 'move'
|
||||
url = wekanurl + apiboards + boardid + s + l + s + listid + s + action
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
post_data = {'toBoardId': toboardid}
|
||||
if toswimlaneid:
|
||||
post_data['toSwimlaneId'] = toswimlaneid
|
||||
if arguments > 5:
|
||||
post_data['position'] = int(sys.argv[6])
|
||||
body = requests.post(url, json=post_data, headers=headers)
|
||||
print("=== {} ===\n".format(sys.argv[1].upper()))
|
||||
print(body.text)
|
||||
# ------- COPY/MOVE LIST END -----------
|
||||
|
||||
if sys.argv[1] == 'mycards':
|
||||
# ------- MY CARDS / DUE CARDS START -----------
|
||||
params = {}
|
||||
if arguments >= 2 and sys.argv[2].lower() == 'due':
|
||||
params['due'] = 'true'
|
||||
if arguments >= 3:
|
||||
params['from'] = sys.argv[3]
|
||||
if arguments >= 4:
|
||||
params['to'] = sys.argv[4]
|
||||
url = wekanurl + 'api/user/cards'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
body = requests.get(url, headers=headers, params=params)
|
||||
print("=== MY CARDS ===\n")
|
||||
data2 = body.text.replace('}', "}\n")
|
||||
print(data2)
|
||||
# ------- MY CARDS / DUE CARDS END -----------
|
||||
|
||||
if sys.argv[1] == 'getcardsettings':
|
||||
# ------- GET CARD SETTINGS START -----------
|
||||
if arguments < 2:
|
||||
print("Usage: python3 api.py getcardsettings BOARDID")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
url = wekanurl + apiboards + boardid + s + 'cardSettings'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
body = requests.get(url, headers=headers)
|
||||
print("=== CARD SETTINGS ===\n")
|
||||
data2 = body.text.replace(',', ",\n")
|
||||
print(data2)
|
||||
# ------- GET CARD SETTINGS END -----------
|
||||
|
||||
if sys.argv[1] == 'setcardsetting':
|
||||
# ------- SET CARD SETTING START -----------
|
||||
if arguments < 4:
|
||||
print("Usage: python3 api.py setcardsetting BOARDID KEY VALUE")
|
||||
print("e.g. python3 api.py setcardsetting BOARDID allowsDueDate true")
|
||||
exit(1)
|
||||
boardid = sys.argv[2]
|
||||
key = sys.argv[3]
|
||||
value = sys.argv[4]
|
||||
url = wekanurl + apiboards + boardid + s + 'cardSettings'
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
body = requests.put(url, json={key: value}, headers=headers)
|
||||
print("=== SET CARD SETTING ===\n")
|
||||
print(body.text)
|
||||
# ------- SET CARD SETTING END -----------
|
||||
|
||||
# ------- BOARD MEMBER / CARD FIELD API ENDPOINTS END -----------
|
||||
+165
-2
@@ -1,6 +1,169 @@
|
||||
// PWA
|
||||
// Fix dynamic-import chunk loading when Wekan runs under a sub-path (ROOT_URL includes a pathname,
|
||||
// e.g. https://example.com/wekan). Rspack uses __webpack_public_path__ to prefix chunk URLs; by
|
||||
// default it is set to /build-chunks/ (no context path). We update it here, at module evaluation
|
||||
// time, before any import() expression runs (those are deferred to Meteor.startup callbacks).
|
||||
/* global __webpack_public_path__:writable */
|
||||
try {
|
||||
const _cfg = typeof window !== 'undefined' && window.__meteor_runtime_config__;
|
||||
const _rootUrl = _cfg && _cfg.ROOT_URL;
|
||||
if (_rootUrl) {
|
||||
const _rootPath = new URL(_rootUrl).pathname.replace(/\/+$/, '');
|
||||
if (_rootPath && _rootPath !== '/') {
|
||||
__webpack_public_path__ = _rootPath + '/build-chunks/';
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Fix bug in jam:offline 0.4.1: s?.message.includes() crashes when s.message is
|
||||
// undefined (e.g. during WebSocket reconnect errors that don't carry a .message).
|
||||
// Wrap _debug so the optional chain is complete: s?.message?.includes().
|
||||
(function patchJamOfflineDebug() {
|
||||
const patched = Meteor._debug;
|
||||
Meteor._debug = function (m, s) {
|
||||
try {
|
||||
return patched.call(this, m, s);
|
||||
} catch (e) {
|
||||
// jam:offline's override threw — fall back to the bare console log.
|
||||
if (typeof console !== 'undefined') {
|
||||
console.log(m, s);
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
// PWA — use Meteor.absoluteUrl so the path is correct under sub-URL deployments
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function() {
|
||||
navigator.serviceWorker.register('/pwa-service-worker.js');
|
||||
const swPath = new URL('pwa-service-worker.js', Meteor.absoluteUrl()).pathname;
|
||||
navigator.serviceWorker.register(swPath);
|
||||
});
|
||||
}
|
||||
|
||||
// Import Blaze helpers (replaces removed raix:handlebar-helpers package)
|
||||
import '/client/config/blazeHelpers';
|
||||
|
||||
// Import board converter for on-demand conversion
|
||||
import '/client/lib/boardConverter';
|
||||
import '/client/features/boardConversion';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
// Import migration manager and progress UI - COMMENTED OUT
|
||||
// import '/client/lib/attachmentMigrationManager';
|
||||
// import '/client/components/settings/migrationProgress';
|
||||
|
||||
// Import cron settings - COMMENTED OUT
|
||||
// import '/client/components/settings/cronSettings';
|
||||
// Custom head tags
|
||||
|
||||
// Mirror Meteor login token into a cookie for server-side file route auth
|
||||
// This enables cookie-based auth for /cdn/storage/* without leaking ROOT_URL
|
||||
// Token already lives in localStorage; cookie adds same-origin send-on-request semantics
|
||||
Meteor.startup(() => {
|
||||
const COOKIE_NAME = 'meteor_login_token';
|
||||
const USER_ID_COOKIE = 'meteor_user_id';
|
||||
const TOKEN_EXPIRES_COOKIE = 'meteor_login_token_expires';
|
||||
|
||||
const getCookie = (name) => {
|
||||
try {
|
||||
const parts = document.cookie ? document.cookie.split(';') : [];
|
||||
for (const part of parts) {
|
||||
const [k, ...rest] = part.trim().split('=');
|
||||
if (decodeURIComponent(k) === name) {
|
||||
return decodeURIComponent(rest.join('='));
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return '';
|
||||
};
|
||||
|
||||
const cookieAttrs = () => {
|
||||
const attrs = ['Path=/', 'SameSite=Lax'];
|
||||
try {
|
||||
if (window.location && window.location.protocol === 'https:') {
|
||||
attrs.push('Secure');
|
||||
}
|
||||
} catch (_) {}
|
||||
return attrs.join('; ');
|
||||
};
|
||||
|
||||
const setCookie = (name, value) => {
|
||||
if (!value) return;
|
||||
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; ${cookieAttrs()}`;
|
||||
};
|
||||
const clearCookie = (name) => {
|
||||
document.cookie = `${encodeURIComponent(name)}=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; ${cookieAttrs()}`;
|
||||
};
|
||||
|
||||
const syncCookie = () => {
|
||||
try {
|
||||
const token = Accounts && typeof Accounts._storedLoginToken === 'function' ? Accounts._storedLoginToken() : null;
|
||||
if (token) setCookie(COOKIE_NAME, token); else clearCookie(COOKIE_NAME);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const bootstrapTokenFromCookie = () => {
|
||||
try {
|
||||
const currentToken = Accounts && typeof Accounts._storedLoginToken === 'function'
|
||||
? Accounts._storedLoginToken()
|
||||
: null;
|
||||
if (currentToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = getCookie(COOKIE_NAME);
|
||||
const userId = getCookie(USER_ID_COOKIE);
|
||||
const tokenExpiresRaw = getCookie(TOKEN_EXPIRES_COOKIE);
|
||||
const tokenExpires = tokenExpiresRaw ? new Date(tokenExpiresRaw) : null;
|
||||
|
||||
if (!token || !userId || !tokenExpires || Number.isNaN(tokenExpires.getTime())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Accounts && typeof Accounts._storeLoginToken === 'function') {
|
||||
Accounts._storeLoginToken(userId, token, tokenExpires);
|
||||
if (Meteor.status && !Meteor.status().connected && typeof Meteor.reconnect === 'function') {
|
||||
Meteor.reconnect();
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
bootstrapTokenFromCookie();
|
||||
|
||||
// Initial sync on startup
|
||||
syncCookie();
|
||||
|
||||
// Keep cookie in sync on login/logout
|
||||
if (Accounts && typeof Accounts.onLogin === 'function') Accounts.onLogin(syncCookie);
|
||||
if (Accounts && typeof Accounts.onLogout === 'function') Accounts.onLogout(syncCookie);
|
||||
|
||||
// Sync across tabs/windows when localStorage changes
|
||||
window.addEventListener('storage', (ev) => {
|
||||
if (ev && typeof ev.key === 'string' && ev.key.indexOf('Meteor.loginToken') !== -1) {
|
||||
syncCookie();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Subscribe to per-user small publications
|
||||
Meteor.startup(() => {
|
||||
Tracker.autorun(() => {
|
||||
if (Meteor.userId()) {
|
||||
Meteor.subscribe('userGreyIcons');
|
||||
Meteor.subscribe('userDesktopDragHandles');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize mobile mode on startup for iOS devices
|
||||
// This ensures mobile mode is applied correctly on page load
|
||||
Tracker.afterFlush(() => {
|
||||
if (typeof Utils !== 'undefined' && Utils.initializeUserSettings) {
|
||||
Utils.initializeUserSettings();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -9,11 +9,11 @@ template(name="activities")
|
||||
+cardActivities
|
||||
|
||||
template(name="boardActivities")
|
||||
each activityData in currentBoard.activities
|
||||
each activityData in boardActivitiesList
|
||||
+activity(activity=activityData card=card mode=mode)
|
||||
|
||||
template(name="cardActivities")
|
||||
each activityData in activities
|
||||
each activityData in cardActivitiesList
|
||||
+activity(activity=activityData card=card mode=mode)
|
||||
|
||||
template(name="activity")
|
||||
@@ -189,14 +189,15 @@ template(name="activity")
|
||||
if(currentData.timeKey)
|
||||
| {{_ activity.activityType }}
|
||||
= ' '
|
||||
i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
|
||||
i(title=currentData.timeValue).activity-meta {{ displayDate currentData.timeValue 'LLL' }}
|
||||
if (currentData.timeOldValue)
|
||||
= ' '
|
||||
| {{{_ "previous_as" }}}
|
||||
= ' '
|
||||
i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
|
||||
i(title=currentData.timeOldValue).activity-meta {{ displayDate currentData.timeOldValue 'LLL' }}
|
||||
= ' @'
|
||||
else if(currentData.timeValue)
|
||||
| {{_ activity.activityType currentData.timeValue}}
|
||||
|
||||
div(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
|
||||
if($neq mode 'none')
|
||||
div(title=activity.createdAt).activity-meta {{ displayDate activity.createdAt }}
|
||||
@@ -1,168 +1,192 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Template } from 'meteor/templating';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import { getSidebarInstance } from '/client/features/sidebar/service';
|
||||
|
||||
const activitiesPerPage = 500;
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
// XXX Should we use ReactiveNumber?
|
||||
this.page = new ReactiveVar(1);
|
||||
this.loadNextPageLocked = false;
|
||||
// TODO is sidebar always available? E.g. on small screens/mobile devices
|
||||
const sidebar = Sidebar;
|
||||
sidebar && sidebar.callFirstWith(null, 'resetNextPeak');
|
||||
this.autorun(() => {
|
||||
let mode = this.data()?.mode;
|
||||
if (mode) {
|
||||
const capitalizedMode = Utils.capitalize(mode);
|
||||
let searchId;
|
||||
const showActivities = this.showActivities();
|
||||
if (mode === 'linkedcard' || mode === 'linkedboard') {
|
||||
const currentCard = Utils.getCurrentCard();
|
||||
searchId = currentCard.linkedId;
|
||||
mode = mode.replace('linked', '');
|
||||
} else if (mode === 'card') {
|
||||
searchId = Utils.getCurrentCardId();
|
||||
} else {
|
||||
searchId = Session.get(`current${capitalizedMode}`);
|
||||
}
|
||||
const limit = this.page.get() * activitiesPerPage;
|
||||
if (searchId === null) return;
|
||||
Template.activities.onCreated(function () {
|
||||
// Register with sidebar so it can call loadNextPage on us
|
||||
const Sidebar = getSidebarInstance();
|
||||
if (Sidebar) {
|
||||
Sidebar.activitiesInstance = this;
|
||||
}
|
||||
|
||||
this.subscribe('activities', mode, searchId, limit, showActivities, () => {
|
||||
this.loadNextPageLocked = false;
|
||||
|
||||
// TODO the guard can be removed as soon as the TODO above is resolved
|
||||
if (!sidebar) return;
|
||||
// If the sibear peak hasn't increased, that mean that there are no more
|
||||
// activities, and we can stop calling new subscriptions.
|
||||
// XXX This is hacky! We need to know excatly and reactively how many
|
||||
// activities there are, we probably want to denormalize this number
|
||||
// dirrectly into card and board documents.
|
||||
const nextPeakBefore = sidebar.callFirstWith(null, 'getNextPeak');
|
||||
sidebar.calculateNextPeak();
|
||||
const nextPeakAfter = sidebar.callFirstWith(null, 'getNextPeak');
|
||||
if (nextPeakBefore === nextPeakAfter) {
|
||||
sidebar.callFirstWith(null, 'resetNextPeak');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
loadNextPage() {
|
||||
// XXX Should we use ReactiveNumber?
|
||||
this.page = new ReactiveVar(1);
|
||||
this.loadNextPageLocked = false;
|
||||
this.loadNextPage = () => {
|
||||
if (this.loadNextPageLocked === false) {
|
||||
this.page.set(this.page.get() + 1);
|
||||
this.loadNextPageLocked = true;
|
||||
}
|
||||
},
|
||||
showActivities() {
|
||||
let ret = false;
|
||||
let mode = this.data()?.mode;
|
||||
};
|
||||
|
||||
// TODO is sidebar always available? E.g. on small screens/mobile devices
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar && sidebar.infiniteScrolling) {
|
||||
sidebar.infiniteScrolling.resetNextPeak();
|
||||
}
|
||||
this.autorun(() => {
|
||||
const data = Template.currentData();
|
||||
let mode = data?.mode;
|
||||
if (mode) {
|
||||
const capitalizedMode = Utils.capitalize(mode);
|
||||
let searchId;
|
||||
const showActivities = _showActivities(data);
|
||||
if (mode === 'linkedcard' || mode === 'linkedboard') {
|
||||
const currentCard = Utils.getCurrentCard();
|
||||
ret = currentCard.showActivities ?? false;
|
||||
searchId = currentCard.linkedId;
|
||||
mode = mode.replace('linked', '');
|
||||
} else if (mode === 'card') {
|
||||
ret = this.data()?.card?.showActivities ?? false;
|
||||
searchId = Utils.getCurrentCardId();
|
||||
} else {
|
||||
ret = Utils.getCurrentBoard().showActivities ?? false;
|
||||
searchId = Session.get(`current${capitalizedMode}`);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
activities() {
|
||||
const ret = this.data().card.activities();
|
||||
return ret;
|
||||
},
|
||||
}).register('activities');
|
||||
const limit = this.page.get() * activitiesPerPage;
|
||||
if (searchId === null) return;
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
this.subscribe('activities', mode, searchId, limit, showActivities, () => {
|
||||
this.loadNextPageLocked = false;
|
||||
|
||||
// TODO the guard can be removed as soon as the TODO above is resolved
|
||||
if (!sidebar || !sidebar.infiniteScrolling) return;
|
||||
// If the sidebar peak hasn't increased, that means that there are no more
|
||||
// activities, and we can stop calling new subscriptions.
|
||||
const nextPeakBefore = sidebar.infiniteScrolling.getNextPeak();
|
||||
sidebar.calculateNextPeak();
|
||||
const nextPeakAfter = sidebar.infiniteScrolling.getNextPeak();
|
||||
if (nextPeakBefore === nextPeakAfter) {
|
||||
sidebar.infiniteScrolling.resetNextPeak();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function _showActivities(data) {
|
||||
let ret = false;
|
||||
let mode = data?.mode;
|
||||
if (mode) {
|
||||
if (mode === 'linkedcard' || mode === 'linkedboard') {
|
||||
const currentCard = Utils.getCurrentCard();
|
||||
ret = currentCard.showActivities ?? false;
|
||||
} else if (mode === 'card') {
|
||||
// For card mode, get showActivities from data context or fallback to current card
|
||||
const card = data?.card || Utils.getCurrentCard();
|
||||
ret = card?.showActivities ?? false;
|
||||
} else {
|
||||
ret = Utils.getCurrentBoard().showActivities ?? false;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
Template.activities.helpers({
|
||||
showActivities() {
|
||||
const data = Template.currentData();
|
||||
return _showActivities(data);
|
||||
},
|
||||
|
||||
activities() {
|
||||
const activities = this.card?.activities?.();
|
||||
return activities || [];
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardActivities.helpers({
|
||||
boardActivitiesList() {
|
||||
const board = Utils.getCurrentBoard();
|
||||
const activities = board?.activities?.();
|
||||
return activities || [];
|
||||
},
|
||||
});
|
||||
|
||||
Template.cardActivities.helpers({
|
||||
cardActivitiesList() {
|
||||
const data = Template.currentData();
|
||||
const card = data?.card;
|
||||
const activities = card?.activities?.();
|
||||
return activities || [];
|
||||
},
|
||||
});
|
||||
|
||||
Template.activity.helpers({
|
||||
checkItem() {
|
||||
const checkItemId = this.currentData().activity.checklistItemId;
|
||||
const checkItemId = this.activity.checklistItemId;
|
||||
const checkItem = ReactiveCache.getChecklistItem(checkItemId);
|
||||
return checkItem && checkItem.title;
|
||||
},
|
||||
|
||||
boardLabelLink() {
|
||||
const data = this.currentData();
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
if (data.mode !== 'board') {
|
||||
// data.mode: card, linkedcard, linkedboard
|
||||
return createBoardLink(data.activity.board(), data.activity.listName ? data.activity.listName : null);
|
||||
if (this.mode !== 'board') {
|
||||
return createBoardLink(this.activity.board(), this.activity.listName ? this.activity.listName : null);
|
||||
}
|
||||
else if (currentBoardId != data.activity.boardId) {
|
||||
// data.mode: board
|
||||
// current activitie is linked
|
||||
return createBoardLink(data.activity.board(), data.activity.listName ? data.activity.listName : null);
|
||||
else if (currentBoardId != this.activity.boardId) {
|
||||
return createBoardLink(this.activity.board(), this.activity.listName ? this.activity.listName : null);
|
||||
}
|
||||
return TAPi18n.__('this-board');
|
||||
},
|
||||
|
||||
cardLabelLink() {
|
||||
const data = this.currentData();
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
if (data.mode == 'card') {
|
||||
// data.mode: card
|
||||
if (this.mode == 'card') {
|
||||
return TAPi18n.__('this-card');
|
||||
}
|
||||
else if (data.mode !== 'board') {
|
||||
// data.mode: linkedcard, linkedboard
|
||||
return createCardLink(data.activity.card(), null);
|
||||
else if (this.mode !== 'board') {
|
||||
return createCardLink(this.activity.card(), null);
|
||||
}
|
||||
else if (currentBoardId != data.activity.boardId) {
|
||||
// data.mode: board
|
||||
// current activitie is linked
|
||||
return createCardLink(data.activity.card(), data.activity.board().title);
|
||||
else if (currentBoardId != this.activity.boardId) {
|
||||
return createCardLink(this.activity.card(), this.activity.board().title);
|
||||
}
|
||||
return createCardLink(this.currentData().activity.card(), null);
|
||||
return createCardLink(this.activity.card(), null);
|
||||
},
|
||||
|
||||
cardLink() {
|
||||
const data = this.currentData();
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
if (data.mode !== 'board') {
|
||||
// data.mode: card, linkedcard, linkedboard
|
||||
return createCardLink(data.activity.card(), null);
|
||||
if (this.mode !== 'board') {
|
||||
return createCardLink(this.activity.card(), null);
|
||||
}
|
||||
else if (currentBoardId != data.activity.boardId) {
|
||||
// data.mode: board
|
||||
// current activitie is linked
|
||||
return createCardLink(data.activity.card(), data.activity.board().title);
|
||||
else if (currentBoardId != this.activity.boardId) {
|
||||
return createCardLink(this.activity.card(), this.activity.board().title);
|
||||
}
|
||||
return createCardLink(this.currentData().activity.card(), null);
|
||||
return createCardLink(this.activity.card(), null);
|
||||
},
|
||||
|
||||
receivedDate() {
|
||||
const receivedDate = this.currentData().activity.card();
|
||||
if (!receivedDate) return null;
|
||||
return receivedDate.receivedAt;
|
||||
const card = this.activity.card();
|
||||
if (!card) return null;
|
||||
return card.receivedAt;
|
||||
},
|
||||
|
||||
startDate() {
|
||||
const startDate = this.currentData().activity.card();
|
||||
if (!startDate) return null;
|
||||
return startDate.startAt;
|
||||
const card = this.activity.card();
|
||||
if (!card) return null;
|
||||
return card.startAt;
|
||||
},
|
||||
|
||||
dueDate() {
|
||||
const dueDate = this.currentData().activity.card();
|
||||
if (!dueDate) return null;
|
||||
return dueDate.dueAt;
|
||||
const card = this.activity.card();
|
||||
if (!card) return null;
|
||||
return card.dueAt;
|
||||
},
|
||||
|
||||
endDate() {
|
||||
const endDate = this.currentData().activity.card();
|
||||
if (!endDate) return null;
|
||||
return endDate.endAt;
|
||||
const card = this.activity.card();
|
||||
if (!card) return null;
|
||||
return card.endAt;
|
||||
},
|
||||
|
||||
lastLabel() {
|
||||
const lastLabelId = this.currentData().activity.labelId;
|
||||
const lastLabelId = this.activity.labelId;
|
||||
if (!lastLabelId) return null;
|
||||
const lastLabel = ReactiveCache.getBoard(
|
||||
this.currentData().activity.boardId,
|
||||
this.activity.boardId,
|
||||
).getLabelById(lastLabelId);
|
||||
if (lastLabel && (lastLabel.name === undefined || lastLabel.name === '')) {
|
||||
return lastLabel.color;
|
||||
@@ -175,7 +199,7 @@ BlazeComponent.extendComponent({
|
||||
|
||||
lastCustomField() {
|
||||
const lastCustomField = ReactiveCache.getCustomField(
|
||||
this.currentData().activity.customFieldId,
|
||||
this.activity.customFieldId,
|
||||
);
|
||||
if (!lastCustomField) return null;
|
||||
return lastCustomField.name;
|
||||
@@ -183,16 +207,15 @@ BlazeComponent.extendComponent({
|
||||
|
||||
lastCustomFieldValue() {
|
||||
const lastCustomField = ReactiveCache.getCustomField(
|
||||
this.currentData().activity.customFieldId,
|
||||
this.activity.customFieldId,
|
||||
);
|
||||
if (!lastCustomField) return null;
|
||||
const value = this.currentData().activity.value;
|
||||
const value = this.activity.value;
|
||||
if (
|
||||
lastCustomField.settings.dropdownItems &&
|
||||
lastCustomField.settings.dropdownItems.length > 0
|
||||
) {
|
||||
const dropDownValue = _.find(
|
||||
lastCustomField.settings.dropdownItems,
|
||||
const dropDownValue = lastCustomField.settings.dropdownItems.find(
|
||||
item => {
|
||||
return item._id === value;
|
||||
},
|
||||
@@ -203,13 +226,13 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
listLabel() {
|
||||
const activity = this.currentData().activity;
|
||||
const activity = this.activity;
|
||||
const list = activity.list();
|
||||
return (list && list.title) || activity.title;
|
||||
},
|
||||
|
||||
sourceLink() {
|
||||
const source = this.currentData().activity.source;
|
||||
const source = this.activity.source;
|
||||
if (source) {
|
||||
if (source.url) {
|
||||
return Blaze.toHTML(
|
||||
@@ -229,36 +252,39 @@ BlazeComponent.extendComponent({
|
||||
|
||||
memberLink() {
|
||||
return Blaze.toHTMLWithData(Template.memberName, {
|
||||
user: this.currentData().activity.member(),
|
||||
user: this.activity.member(),
|
||||
});
|
||||
},
|
||||
|
||||
attachmentLink() {
|
||||
const attachment = this.currentData().activity.attachment();
|
||||
const attachment = this.activity.attachment();
|
||||
const attachmentUrl = attachment && typeof attachment.link === 'function'
|
||||
? attachment.link()
|
||||
: '';
|
||||
// trying to display url before file is stored generates js errors
|
||||
return (
|
||||
(attachment &&
|
||||
attachment.path &&
|
||||
attachmentUrl &&
|
||||
Blaze.toHTML(
|
||||
HTML.A(
|
||||
{
|
||||
href: `${attachment.link()}?download=true`,
|
||||
href: `${attachmentUrl}?download=true`,
|
||||
target: '_blank',
|
||||
},
|
||||
sanitizeText(attachment.name),
|
||||
),
|
||||
)) ||
|
||||
sanitizeText(this.currentData().activity.attachmentName)
|
||||
sanitizeText(this.activity.attachmentName)
|
||||
);
|
||||
},
|
||||
|
||||
customField() {
|
||||
const customField = this.currentData().activity.customField();
|
||||
const customField = this.activity.customField();
|
||||
if (!customField) return null;
|
||||
return customField.name;
|
||||
},
|
||||
|
||||
}).register('activity');
|
||||
});
|
||||
|
||||
Template.activity.helpers({
|
||||
sanitize(value) {
|
||||
@@ -268,7 +294,8 @@ Template.activity.helpers({
|
||||
|
||||
Template.commentReactions.events({
|
||||
'click .reaction'(event) {
|
||||
if (ReactiveCache.getCurrentUser().isBoardMember()) {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && user.isBoardMember()) {
|
||||
const codepoint = event.currentTarget.dataset['codepoint'];
|
||||
const commentId = Template.instance().data.commentId;
|
||||
const cardComment = ReactiveCache.getCardComment(commentId);
|
||||
@@ -280,7 +307,8 @@ Template.commentReactions.events({
|
||||
|
||||
Template.addReactionPopup.events({
|
||||
'click .add-comment-reaction'(event) {
|
||||
if (ReactiveCache.getCurrentUser().isBoardMember()) {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && user.isBoardMember()) {
|
||||
const codepoint = event.currentTarget.dataset['codepoint'];
|
||||
const commentId = Template.instance().data.commentId;
|
||||
const cardComment = ReactiveCache.getCardComment(commentId);
|
||||
|
||||
@@ -108,15 +108,12 @@
|
||||
text-decoration: none;
|
||||
height: 24px;
|
||||
}
|
||||
.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-smile-o {
|
||||
font-size: 17px;
|
||||
.comments .comment .comment-desc .reactions .open-comment-reaction-popup span {
|
||||
display: inline-block;
|
||||
font-size: clamp(14px, 2vw, 18px);
|
||||
font-weight: 500;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-plus {
|
||||
font-size: 8px;
|
||||
margin-top: -7px;
|
||||
margin-left: 1px;
|
||||
line-height: 1;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.comments .comment .comment-desc .reactions .reaction {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -25,13 +25,14 @@ template(name="comment")
|
||||
= text
|
||||
.edit-controls
|
||||
button.primary(type="submit") {{_ 'edit'}}
|
||||
.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form(title="{{_ 'close' }}")
|
||||
i.fa.fa-times-thin
|
||||
else
|
||||
.comment-text
|
||||
+viewer
|
||||
= text
|
||||
+commentReactions(reactions=reactions commentId=_id)
|
||||
span(title=createdAt).comment-meta {{ moment createdAt }}
|
||||
span(title=createdAt).comment-meta {{ displayDate createdAt }}
|
||||
if($eq currentUser._id userId)
|
||||
+editOrDeleteComment
|
||||
else if currentUser.isBoardAdmin
|
||||
@@ -54,9 +55,11 @@ template(name="commentReactions")
|
||||
span.reaction-codepoint !{reaction.reactionCodepoint}
|
||||
span.reaction-count #{reaction.userIds.length}
|
||||
if (currentUser.isBoardMember)
|
||||
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
|
||||
i.fa.fa-smile-o
|
||||
i.fa.fa-plus
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
|
||||
span(title="{{_ 'reaction' }}") 😀
|
||||
span(title="{{_ 'add' }}") ➕
|
||||
|
||||
template(name="addReactionPopup")
|
||||
.reactions-popup
|
||||
|
||||
@@ -1,94 +1,85 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import CardComments from '/models/cardComments';
|
||||
import { UnsavedEdits } from '/client/lib/unsavedEdits';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import autosize from 'autosize';
|
||||
|
||||
const commentFormIsOpen = new ReactiveVar(false);
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onDestroyed() {
|
||||
commentFormIsOpen.set(false);
|
||||
$('.note-popover').hide();
|
||||
},
|
||||
Template.commentForm.onDestroyed(function () {
|
||||
commentFormIsOpen.set(false);
|
||||
$('.note-popover').hide();
|
||||
});
|
||||
|
||||
Template.commentForm.helpers({
|
||||
commentFormIsOpen() {
|
||||
return commentFormIsOpen.get();
|
||||
},
|
||||
});
|
||||
|
||||
getInput() {
|
||||
return this.$('.js-new-comment-input');
|
||||
Template.commentForm.events({
|
||||
'submit .js-new-comment-form'(evt, tpl) {
|
||||
const input = tpl.$('.js-new-comment-input');
|
||||
const text = input.val().trim();
|
||||
const card = Template.currentData();
|
||||
let boardId = card.boardId;
|
||||
let cardId = card._id;
|
||||
if (card.isLinkedCard()) {
|
||||
boardId = ReactiveCache.getCard(card.linkedId).boardId;
|
||||
cardId = card.linkedId;
|
||||
} else if (card.isLinkedBoard()) {
|
||||
boardId = card.linkedId;
|
||||
}
|
||||
if (text) {
|
||||
CardComments.insert({
|
||||
text,
|
||||
boardId,
|
||||
cardId,
|
||||
});
|
||||
resetCommentInput(input);
|
||||
Tracker.flush();
|
||||
autosize.update(input);
|
||||
input.trigger('submitted');
|
||||
}
|
||||
evt.preventDefault();
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-new-comment-form'(evt) {
|
||||
const input = this.getInput();
|
||||
const text = input.val().trim();
|
||||
const card = this.currentData();
|
||||
let boardId = card.boardId;
|
||||
let cardId = card._id;
|
||||
if (card.isLinkedCard()) {
|
||||
boardId = ReactiveCache.getCard(card.linkedId).boardId;
|
||||
cardId = card.linkedId;
|
||||
} else if (card.isLinkedBoard()) {
|
||||
boardId = card.linkedId;
|
||||
}
|
||||
if (text) {
|
||||
CardComments.insert({
|
||||
text,
|
||||
boardId,
|
||||
cardId,
|
||||
});
|
||||
resetCommentInput(input);
|
||||
Tracker.flush();
|
||||
autosize.update(input);
|
||||
input.trigger('submitted');
|
||||
}
|
||||
evt.preventDefault();
|
||||
},
|
||||
// Pressing Ctrl+Enter should submit the form
|
||||
'keydown form textarea'(evt) {
|
||||
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
|
||||
this.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
// Pressing Ctrl+Enter should submit the form
|
||||
'keydown form textarea'(evt, tpl) {
|
||||
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
|
||||
tpl.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
}).register('commentForm');
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.comments.helpers({
|
||||
getComments() {
|
||||
const ret = this.data().comments();
|
||||
return ret;
|
||||
const data = Template.currentData();
|
||||
if (!data || typeof data.comments !== 'function') return [];
|
||||
return data.comments();
|
||||
},
|
||||
}).register("comments");
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-delete-comment': Popup.afterConfirm('deleteComment', () => {
|
||||
const commentId = this.data()._id;
|
||||
CardComments.remove(commentId);
|
||||
Popup.back();
|
||||
}),
|
||||
'submit .js-edit-comment'(evt) {
|
||||
evt.preventDefault();
|
||||
const commentText = this.currentComponent()
|
||||
.getValue()
|
||||
.trim();
|
||||
const commentId = this.data()._id;
|
||||
if (commentText) {
|
||||
CardComments.update(commentId, {
|
||||
$set: {
|
||||
text: commentText,
|
||||
},
|
||||
});
|
||||
}
|
||||
Template.comment.events({
|
||||
'click .js-delete-comment': Popup.afterConfirm('deleteComment', function () {
|
||||
const commentId = this._id;
|
||||
CardComments.remove(commentId);
|
||||
Popup.back();
|
||||
}),
|
||||
'submit .js-edit-comment'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const textarea = tpl.find('.js-edit-comment textarea,input[type=text]');
|
||||
const commentText = textarea && textarea.value ? textarea.value.trim() : '';
|
||||
const commentId = tpl.data._id;
|
||||
if (commentText) {
|
||||
CardComments.update(commentId, {
|
||||
$set: {
|
||||
text: commentText,
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
},
|
||||
}).register("comment");
|
||||
});
|
||||
|
||||
// XXX This should be a static method of the `commentForm` component
|
||||
function resetCommentInput(input) {
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
/* Board Conversion Progress Styles */
|
||||
.board-conversion-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.board-conversion-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.board-conversion-modal {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.board-conversion-header {
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.board-conversion-header h3 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.board-conversion-header h3 i {
|
||||
margin-right: 8px;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.board-conversion-header p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.board-conversion-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.conversion-progress {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #2196F3, #21CBF3);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.3),
|
||||
transparent
|
||||
);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #2196F3;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.conversion-status {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.conversion-status i {
|
||||
margin-right: 8px;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.conversion-time {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
background-color: #f5f5f5;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.conversion-time i {
|
||||
margin-right: 6px;
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
.board-conversion-footer {
|
||||
padding: 16px 24px 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.conversion-info {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.conversion-info i {
|
||||
margin-right: 6px;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 600px) {
|
||||
.board-conversion-modal {
|
||||
width: 95%;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.board-conversion-header,
|
||||
.board-conversion-content,
|
||||
.board-conversion-footer {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.board-conversion-header h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
template(name="boardConversionProgress")
|
||||
.board-conversion-overlay(class="{{#if isConverting}}active{{/if}}")
|
||||
.board-conversion-modal
|
||||
.board-conversion-header
|
||||
h3
|
||||
i.fa.fa-cog
|
||||
| {{_ 'converting-board'}}
|
||||
p {{_ 'converting-board-description'}}
|
||||
|
||||
.board-conversion-content
|
||||
.conversion-progress
|
||||
.progress-bar
|
||||
.progress-fill(style="width: {{conversionProgress}}%")
|
||||
.progress-text {{conversionProgress}}%
|
||||
|
||||
.conversion-status
|
||||
i.fa.fa-cog
|
||||
| {{conversionStatus}}
|
||||
|
||||
.conversion-time(style="{{#unless conversionEstimatedTime}}display: none;{{/unless}}")
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'estimated-time-remaining'}}: {{conversionEstimatedTime}}
|
||||
|
||||
.board-conversion-footer
|
||||
.conversion-info
|
||||
i.fa.fa-info-circle
|
||||
| {{_ 'conversion-info-text'}}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import {
|
||||
boardConverter,
|
||||
isConverting,
|
||||
conversionProgress,
|
||||
conversionStatus,
|
||||
conversionEstimatedTime
|
||||
} from '/client/lib/boardConverter';
|
||||
|
||||
Template.boardConversionProgress.helpers({
|
||||
isConverting() {
|
||||
return isConverting.get();
|
||||
},
|
||||
|
||||
conversionProgress() {
|
||||
return conversionProgress.get();
|
||||
},
|
||||
|
||||
conversionStatus() {
|
||||
return conversionStatus.get();
|
||||
},
|
||||
|
||||
conversionEstimatedTime() {
|
||||
return conversionEstimatedTime.get();
|
||||
}
|
||||
});
|
||||
|
||||
Template.boardConversionProgress.onCreated(function() {
|
||||
// Subscribe to conversion state changes
|
||||
this.autorun(() => {
|
||||
isConverting.get();
|
||||
conversionProgress.get();
|
||||
conversionStatus.get();
|
||||
conversionEstimatedTime.get();
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,21 @@
|
||||
template(name="archivedBoards")
|
||||
h2
|
||||
i.fa.fa-archive
|
||||
span(title="{{_ 'archived-boards'}}")
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archived-boards'}}
|
||||
|
||||
ul.archived-lists
|
||||
each archivedBoards
|
||||
li.archived-lists-item
|
||||
div.board-header-btns
|
||||
button.board-header-btn.js-delete-board
|
||||
i.fa.fa-trash-o
|
||||
button.primary.board-header-btn.js-delete-board
|
||||
i.fa.fa-trash
|
||||
| {{_ 'delete-board'}}
|
||||
button.board-header-btn.js-restore-board
|
||||
button.primary.board-header-btn.js-restore-board
|
||||
i.fa.fa-undo
|
||||
| {{_ 'restore-board'}}
|
||||
= title
|
||||
span {{ moment archivedAt 'LLL' }}
|
||||
span {{ displayDate archivedAt 'LLL' }}
|
||||
else
|
||||
li.no-items-message {{_ 'no-archived-boards'}}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import Boards from '/models/boards';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.subscribe('archivedBoards');
|
||||
},
|
||||
Template.archivedBoards.onCreated(function () {
|
||||
this.subscribe('archivedBoards');
|
||||
});
|
||||
|
||||
Template.archivedBoards.helpers({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
@@ -18,38 +21,34 @@ BlazeComponent.extendComponent({
|
||||
);
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-restore-board'() {
|
||||
// TODO : Make isSandstorm variable global
|
||||
const isSandstorm =
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.sandstorm;
|
||||
if (isSandstorm && Utils.getCurrentBoardId()) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
currentBoard.archive();
|
||||
}
|
||||
const board = this.currentData();
|
||||
board.restore();
|
||||
Utils.goBoardId(board._id);
|
||||
},
|
||||
'click .js-delete-board': Popup.afterConfirm('boardDelete', function() {
|
||||
Popup.back();
|
||||
const isSandstorm =
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.sandstorm;
|
||||
if (isSandstorm && Utils.getCurrentBoardId()) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
Boards.remove(currentBoard._id);
|
||||
}
|
||||
Boards.remove(this._id);
|
||||
FlowRouter.go('home');
|
||||
}),
|
||||
},
|
||||
];
|
||||
Template.archivedBoards.events({
|
||||
async 'click .js-restore-board'() {
|
||||
// TODO : Make isSandstorm variable global
|
||||
const isSandstorm =
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.sandstorm;
|
||||
if (isSandstorm && Utils.getCurrentBoardId()) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
await currentBoard.archive();
|
||||
}
|
||||
const board = this;
|
||||
await board.restore();
|
||||
Utils.goBoardId(board._id);
|
||||
},
|
||||
}).register('archivedBoards');
|
||||
'click .js-delete-board': Popup.afterConfirm('boardDelete', async function() {
|
||||
Popup.back();
|
||||
const isSandstorm =
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.sandstorm;
|
||||
if (isSandstorm && Utils.getCurrentBoardId()) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
await Boards.removeAsync(currentBoard._id);
|
||||
}
|
||||
await Boards.removeAsync(this._id);
|
||||
FlowRouter.go('home');
|
||||
}),
|
||||
});
|
||||
@@ -231,6 +231,30 @@
|
||||
font-size: 1em !important; /* Keep original icon size */
|
||||
}
|
||||
|
||||
/* Mobile iPhone: scale card details text and icons to 2x */
|
||||
body.mobile-mode.iphone-device .card-details {
|
||||
font-size: 2em !important;
|
||||
}
|
||||
body.mobile-mode.iphone-device .card-details .fa,
|
||||
body.mobile-mode.iphone-device .card-details .icon,
|
||||
body.mobile-mode.iphone-device .card-details i,
|
||||
body.mobile-mode.iphone-device .card-details .emoji-icon,
|
||||
body.mobile-mode.iphone-device .card-details a,
|
||||
body.mobile-mode.iphone-device .card-details p,
|
||||
body.mobile-mode.iphone-device .card-details span,
|
||||
body.mobile-mode.iphone-device .card-details div,
|
||||
body.mobile-mode.iphone-device .card-details button,
|
||||
body.mobile-mode.iphone-device .card-details input,
|
||||
body.mobile-mode.iphone-device .card-details select,
|
||||
body.mobile-mode.iphone-device .card-details textarea {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
/* Section titles slightly larger than content but not as big as card title */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-item-title {
|
||||
font-size: 1.1em !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Ensure scrollbars are positioned correctly */
|
||||
#content[style*="overflow-x: auto"]::-webkit-scrollbar:vertical {
|
||||
width: 12px;
|
||||
@@ -263,62 +287,106 @@
|
||||
animation: fadeIn 0.2s;
|
||||
z-index: 16;
|
||||
}
|
||||
|
||||
/* Fix for mobile Safari: ensure overlay stays behind card details */
|
||||
@media screen and (max-width: 800px) {
|
||||
.board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
|
||||
/* In desktop mode on small screens, still keep overlay behind card */
|
||||
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* In mobile mode, lower the overlay z-index to stay behind card details */
|
||||
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
|
||||
/* iPhone in desktop mode: remove overlay to avoid blocking card */
|
||||
body.desktop-mode.iphone-device .board-wrapper .board-canvas .board-overlay {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* Desktop mode: hide overlay to allow multiple cards and board interaction */
|
||||
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
.board-wrapper .board-canvas.is-dragging-active .open-minicard-composer,
|
||||
.board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked {
|
||||
display: none;
|
||||
}
|
||||
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
|
||||
.board-wrapper.mobile-view {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.board-wrapper.mobile-view .board-canvas {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.board-wrapper.mobile-view .board-canvas.mobile-view .swimlane {
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: flex;
|
||||
display: block !important;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
@media screen and (max-width: 800px),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||
.board-wrapper {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.board-wrapper .board-canvas {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.board-wrapper .board-canvas .swimlane {
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: flex;
|
||||
display: block !important;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
}
|
||||
}
|
||||
.calendar-event-green {
|
||||
@@ -496,3 +564,10 @@
|
||||
font-size: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Global file drag over state for board canvas */
|
||||
.board-canvas.file-drag-over {
|
||||
background-color: rgba(0, 123, 255, 0.05) !important;
|
||||
border: 2px dashed #007bff !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
template(name="board")
|
||||
if isBoardReady.get
|
||||
|
||||
if isConverting
|
||||
+boardConversionProgress
|
||||
else if isBoardReady
|
||||
if currentBoard
|
||||
if onlyShowCurrentCard
|
||||
+cardDetails(currentCard)
|
||||
@@ -16,15 +19,19 @@ template(name="boardBody")
|
||||
if notDisplayThisBoard
|
||||
| {{_ 'tableVisibilityMode-allowPrivateOnly'}}
|
||||
else
|
||||
// Debug information (remove in production)
|
||||
if debugBoardState
|
||||
.debug-info(style="position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.8); color: white; padding: 10px; z-index: 9999; font-size: 12px;")
|
||||
| {{_ 'board'}}: {{currentBoard.title}} | {{_ 'view'}}: {{boardView}} | {{_ 'has-swimlanes'}}: {{hasSwimlanes}} | {{_ 'swimlanes'}}: {{currentBoard.swimlanes.length}}
|
||||
.board-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
.board-canvas.js-swimlanes(
|
||||
class="{{#if hasSwimlanes}}dragscroll{{/if}}"
|
||||
class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
|
||||
class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
|
||||
class="{{#if draggingActive.get}}is-dragging-active{{/if}}"
|
||||
class="{{#if draggingActive}}is-dragging-active{{/if}}"
|
||||
class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}"
|
||||
class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
if showOverlay.get
|
||||
if showOverlay
|
||||
.board-overlay
|
||||
if currentBoard.isTemplatesBoard
|
||||
each currentBoard.swimlanes
|
||||
@@ -34,20 +41,30 @@ template(name="boardBody")
|
||||
each currentBoard.swimlanes
|
||||
+swimlane(this)
|
||||
else
|
||||
a.js-empty-board-add-swimlane(title="{{_ 'add-swimlane'}}")
|
||||
h1.big-message.quiet
|
||||
| {{_ 'add-swimlane'}} +
|
||||
// Fallback: If no swimlanes exist, show lists instead of empty message
|
||||
+listsGroup(currentBoard)
|
||||
else if isViewLists
|
||||
+listsGroup(currentBoard)
|
||||
else if isViewCalendar
|
||||
+calendarView
|
||||
else if isViewGantt
|
||||
+ganttView
|
||||
else
|
||||
+listsGroup(currentBoard)
|
||||
// Default view - show swimlanes if they exist, otherwise show lists
|
||||
if hasSwimlanes
|
||||
each currentBoard.swimlanes
|
||||
+swimlane(this)
|
||||
else
|
||||
+listsGroup(currentBoard)
|
||||
//- Render multiple open cards in desktop mode
|
||||
unless isMiniScreen
|
||||
each openCards
|
||||
+cardDetails(this cardIndex=@index)
|
||||
+sidebar
|
||||
|
||||
template(name="calendarView")
|
||||
if isViewCalendar
|
||||
.calendar-view.swimlane
|
||||
.calendar-view.swimlane.dragscroll
|
||||
if currentCard
|
||||
+cardDetails(currentCard)
|
||||
+fullcalendar(calendarOptions)
|
||||
+fullcalendar(calendarOptions)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -49,6 +49,12 @@ THEME - NEPHRITIS
|
||||
border-bottom: 2px solid #27ae60;
|
||||
border-right: 2px solid #27ae60;
|
||||
}
|
||||
.board-color-nephritis .checklist-progress-bar {
|
||||
background-color: #d4f1dd !important;
|
||||
}
|
||||
.board-color-nephritis .checklist-progress-bar .checklist-progress {
|
||||
background-color: #27ae60 !important;
|
||||
}
|
||||
.board-color-nephritis .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e7faef;
|
||||
}
|
||||
@@ -150,6 +156,12 @@ THEME - Pomegranate
|
||||
border-bottom: 2px solid #c0392b;
|
||||
border-right: 2px solid #c0392b;
|
||||
}
|
||||
.board-color-pomegranate .checklist-progress-bar {
|
||||
background-color: #f5d5d2 !important;
|
||||
}
|
||||
.board-color-pomegranate .checklist-progress-bar .checklist-progress {
|
||||
background-color: #c0392b !important;
|
||||
}
|
||||
.board-color-pomegranate .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #faeae9;
|
||||
}
|
||||
@@ -251,6 +263,12 @@ THEME - Belize
|
||||
border-bottom: 2px solid #2980b9;
|
||||
border-right: 2px solid #2980b9;
|
||||
}
|
||||
.board-color-belize .checklist-progress-bar {
|
||||
background-color: #d1e7f5 !important;
|
||||
}
|
||||
.board-color-belize .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2980b9 !important;
|
||||
}
|
||||
.board-color-belize .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e8f3fa;
|
||||
}
|
||||
@@ -352,6 +370,12 @@ THEME - Wisteria
|
||||
border-bottom: 2px solid #8e44ad;
|
||||
border-right: 2px solid #8e44ad;
|
||||
}
|
||||
.board-color-wisteria .checklist-progress-bar {
|
||||
background-color: #e8d9f0 !important;
|
||||
}
|
||||
.board-color-wisteria .checklist-progress-bar .checklist-progress {
|
||||
background-color: #8e44ad !important;
|
||||
}
|
||||
.board-color-wisteria .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #f4ecf7;
|
||||
}
|
||||
@@ -453,6 +477,12 @@ THEME - Midnight
|
||||
border-bottom: 2px solid #2c3e50;
|
||||
border-right: 2px solid #2c3e50;
|
||||
}
|
||||
.board-color-midnight .checklist-progress-bar {
|
||||
background-color: #d2dae2 !important;
|
||||
}
|
||||
.board-color-midnight .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2c3e50 !important;
|
||||
}
|
||||
.board-color-midnight .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e6ecf1;
|
||||
}
|
||||
@@ -554,6 +584,12 @@ THEME - Pumpkin
|
||||
border-bottom: 2px solid #e67e22;
|
||||
border-right: 2px solid #e67e22;
|
||||
}
|
||||
.board-color-pumpkin .checklist-progress-bar {
|
||||
background-color: #f9e5d1 !important;
|
||||
}
|
||||
.board-color-pumpkin .checklist-progress-bar .checklist-progress {
|
||||
background-color: #e67e22 !important;
|
||||
}
|
||||
.board-color-pumpkin .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #fdf2e9;
|
||||
}
|
||||
@@ -655,6 +691,12 @@ THEME - Moderate Pink
|
||||
border-bottom: 2px solid #cd5a91;
|
||||
border-right: 2px solid #cd5a91;
|
||||
}
|
||||
.board-color-moderatepink .checklist-progress-bar {
|
||||
background-color: #f4dde8 !important;
|
||||
}
|
||||
.board-color-moderatepink .checklist-progress-bar .checklist-progress {
|
||||
background-color: #cd5a91 !important;
|
||||
}
|
||||
.board-color-moderatepink .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #faeef4;
|
||||
}
|
||||
@@ -756,6 +798,12 @@ THEME - Strong Cyan
|
||||
border-bottom: 2px solid #00aecc;
|
||||
border-right: 2px solid #00aecc;
|
||||
}
|
||||
.board-color-strongcyan .checklist-progress-bar {
|
||||
background-color: #ccf2f9 !important;
|
||||
}
|
||||
.board-color-strongcyan .checklist-progress-bar .checklist-progress {
|
||||
background-color: #00aecc !important;
|
||||
}
|
||||
.board-color-strongcyan .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e0fbff;
|
||||
}
|
||||
@@ -857,6 +905,12 @@ THEME - Lime Green
|
||||
border-bottom: 2px solid #4bbf6b;
|
||||
border-right: 2px solid #4bbf6b;
|
||||
}
|
||||
.board-color-limegreen .checklist-progress-bar {
|
||||
background-color: #daf4de !important;
|
||||
}
|
||||
.board-color-limegreen .checklist-progress-bar .checklist-progress {
|
||||
background-color: #4bbf6b !important;
|
||||
}
|
||||
.board-color-limegreen .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #edf9f0;
|
||||
}
|
||||
@@ -959,6 +1013,12 @@ THEME - Dark
|
||||
border-bottom: 2px solid #2c3e51;
|
||||
border-right: 2px solid #2c3e51;
|
||||
}
|
||||
.board-color-dark .checklist-progress-bar {
|
||||
background-color: #d2dae2 !important;
|
||||
}
|
||||
.board-color-dark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2c3e51 !important;
|
||||
}
|
||||
.board-color-dark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e6ecf1;
|
||||
}
|
||||
@@ -1162,6 +1222,12 @@ THEME - Relax
|
||||
border-bottom: 2px solid #27ae61;
|
||||
border-right: 2px solid #27ae61;
|
||||
}
|
||||
.board-color-relax .checklist-progress-bar {
|
||||
background-color: #d4f1dd !important;
|
||||
}
|
||||
.board-color-relax .checklist-progress-bar .checklist-progress {
|
||||
background-color: #27ae61 !important;
|
||||
}
|
||||
.board-color-relax .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e7faef;
|
||||
}
|
||||
@@ -1292,6 +1358,12 @@ THEME - Corteza
|
||||
border-bottom: 2px solid #568ba2;
|
||||
border-right: 2px solid #568ba2;
|
||||
}
|
||||
.board-color-corteza .checklist-progress-bar {
|
||||
background-color: #dce6ec !important;
|
||||
}
|
||||
.board-color-corteza .checklist-progress-bar .checklist-progress {
|
||||
background-color: #568ba2 !important;
|
||||
}
|
||||
.board-color-corteza .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #eef3f6;
|
||||
}
|
||||
@@ -1397,6 +1469,12 @@ THEME - Clear Blue
|
||||
border-bottom: 2px solid #499bea;
|
||||
border-right: 2px solid #499bea;
|
||||
}
|
||||
.board-color-clearblue .checklist-progress-bar {
|
||||
background-color: #daeefb !important;
|
||||
}
|
||||
.board-color-clearblue .checklist-progress-bar .checklist-progress {
|
||||
background-color: #499bea !important;
|
||||
}
|
||||
.board-color-clearblue .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e0fbff;
|
||||
}
|
||||
@@ -1425,7 +1503,7 @@ THEME - Clear Blue
|
||||
}
|
||||
.board-color-clearblue .list {
|
||||
background: rgba(255,255,255,0.35);
|
||||
margin: 10px;
|
||||
margin: 10px 0;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
}
|
||||
@@ -1660,6 +1738,12 @@ THEME - Natural
|
||||
border-bottom: 2px solid #596557;
|
||||
border-right: 2px solid #596557;
|
||||
}
|
||||
.board-color-natural .checklist-progress-bar {
|
||||
background-color: #dee0dd !important;
|
||||
}
|
||||
.board-color-natural .checklist-progress-bar .checklist-progress {
|
||||
background-color: #596557 !important;
|
||||
}
|
||||
.board-color-natural .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #eef0ee;
|
||||
}
|
||||
@@ -1770,6 +1854,12 @@ THEME - Modern
|
||||
border-bottom: 2px solid #2a80b8;
|
||||
border-right: 2px solid #2a80b8;
|
||||
}
|
||||
.board-color-modern .checklist-progress-bar {
|
||||
background-color: #d1e7f5 !important;
|
||||
}
|
||||
.board-color-modern .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2a80b8 !important;
|
||||
}
|
||||
.board-color-modern .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e8f3fa;
|
||||
}
|
||||
@@ -2062,6 +2152,12 @@ THEME - Modern Dark
|
||||
border-bottom: 2px solid #2a2a2a;
|
||||
border-right: 2px solid #2a2a2a;
|
||||
}
|
||||
.board-color-moderndark .checklist-progress-bar {
|
||||
background-color: #d1d1d1 !important;
|
||||
}
|
||||
.board-color-moderndark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2a2a2a !important;
|
||||
}
|
||||
.board-color-moderndark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #eaeaea;
|
||||
}
|
||||
@@ -2547,6 +2643,12 @@ THEME - Exodark
|
||||
border-bottom: 2px solid #dbdbdb!important;/*Fix contrast of checkbox*/
|
||||
border-right: 2px solid #dbdbdb!important;
|
||||
}
|
||||
.board-color-exodark .checklist-progress-bar {
|
||||
background-color: #cccccc !important;
|
||||
}
|
||||
.board-color-exodark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
.board-color-exodark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
@@ -2588,7 +2690,7 @@ THEME - Exodark
|
||||
background: #222;
|
||||
}
|
||||
.board-color-exodark .list {
|
||||
margin: 10px;
|
||||
margin: 10px 0;
|
||||
color: #fff;
|
||||
border-radius: 15px;
|
||||
background-color: #1c1c1c;
|
||||
@@ -3140,6 +3242,12 @@ THEME - Clean Dark
|
||||
margin-left: 3px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.board-color-cleandark .checklist-progress-bar {
|
||||
background-color: #6b6b78 !important;
|
||||
}
|
||||
.board-color-cleandark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #23232B !important;
|
||||
}
|
||||
|
||||
.board-color-cleandark .allBoards {
|
||||
white-space: nowrap;
|
||||
@@ -3262,18 +3370,19 @@ THEME - Clean Light
|
||||
|
||||
.board-color-cleanlight .list,
|
||||
.board-color-cleandark .list {
|
||||
background: none;
|
||||
border-left: none;
|
||||
background: none !important;
|
||||
border-left: none !important;
|
||||
}
|
||||
|
||||
.board-color-cleanlight .list .list-header,
|
||||
.board-color-cleandark .list .list-header {
|
||||
border-bottom: none;
|
||||
display: flex;
|
||||
border-bottom: none !important;
|
||||
display: flex !important;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
background: none;
|
||||
background: none !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.board-color-cleanlight .list .list-header div:has(.list-header-name),
|
||||
@@ -3892,6 +4001,13 @@ THEME - Clean Light
|
||||
margin-left: 3px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.board-color-cleanlight .checklist-progress-bar {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
.board-color-cleanlight .checklist-progress-bar .checklist-progress {
|
||||
background-color: #c0c0c0 !important;
|
||||
color: #010101 !important;
|
||||
}
|
||||
|
||||
.board-color-cleanlight .allBoards {
|
||||
white-space: nowrap;
|
||||
@@ -4164,6 +4280,14 @@ THEME - Clean Light
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.board-color-cleandark .pop-over-list li > a {
|
||||
color: rgba(255, 255, 255, 1) !important;
|
||||
}
|
||||
|
||||
.board-color-cleanlight .pop-over-list li > a {
|
||||
color: rgba(10, 10, 20, 0.85) !important;
|
||||
}
|
||||
|
||||
.board-color-cleanlight .pop-over-list li > a i,
|
||||
.board-color-cleandark .pop-over-list li > a i {
|
||||
margin-right: 6px !important;
|
||||
|
||||
@@ -505,73 +505,73 @@
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
}
|
||||
.mobile-mode .swimlane {
|
||||
display: block !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane .swimlane-header {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 0 1rem 0 !important;
|
||||
padding: 1rem !important;
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
font-weight: bold !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
}
|
||||
.mobile-mode .swimlane .swimlane-header {
|
||||
display: block !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
margin: 0 0 1rem 0 !important;
|
||||
padding: 1rem !important;
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
font-weight: bold !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane .lists {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
flex-direction: column !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
.mobile-mode .swimlane .lists {
|
||||
display: block !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
flex-direction: column !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-top: none !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
position: static !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
bottom: auto !important;
|
||||
transform: none !important;
|
||||
}
|
||||
.mobile-mode .list {
|
||||
display: block !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-top: none !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
position: static !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
bottom: auto !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list:first-child {
|
||||
margin-left: 0 !important;
|
||||
@@ -667,9 +667,9 @@
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
@@ -1,145 +1,159 @@
|
||||
template(name="boardHeaderBar")
|
||||
h1.header-board-menu
|
||||
with currentBoard
|
||||
if $eq title 'Templates'
|
||||
| {{_ 'templates'}}
|
||||
else
|
||||
+viewer
|
||||
= title
|
||||
unless notDisplayThisBoard
|
||||
h1.header-board-menu
|
||||
with currentBoard
|
||||
if $eq title 'Templates'
|
||||
| {{_ 'templates'}}
|
||||
else
|
||||
+viewer
|
||||
= title
|
||||
|
||||
.board-header-btns.left
|
||||
unless isMiniScreen
|
||||
.board-header-btns.left
|
||||
unless isMiniScreen
|
||||
if currentBoard
|
||||
if currentUser
|
||||
with currentBoard
|
||||
if currentUser.isBoardAdmin
|
||||
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
|
||||
i.fa.fa-pencil-square-o
|
||||
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
span {{_ currentBoard.permission}}
|
||||
|
||||
a.board-header-btn.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
if $eq watchLevel "watching"
|
||||
i.fa.fa-eye
|
||||
if $eq watchLevel "tracking"
|
||||
i.fa.fa-bell
|
||||
if $eq watchLevel "muted"
|
||||
i.fa.fa-bell-slash
|
||||
span {{_ watchLevel}}
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
if showStarCounter
|
||||
span.board-star-counter {{currentBoard.stars}}
|
||||
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
||||
i.fa.fa-sort
|
||||
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
|
||||
if isSortActive
|
||||
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
|
||||
i.fa.fa-times-thin
|
||||
|
||||
else
|
||||
a.board-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
span {{_ 'log-in'}}
|
||||
|
||||
.board-header-btns.center
|
||||
|
||||
.board-header-btns.right
|
||||
if currentBoard
|
||||
if currentUser
|
||||
with currentBoard
|
||||
if currentUser.isBoardAdmin
|
||||
if isMiniScreen
|
||||
if currentUser
|
||||
with currentBoard
|
||||
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
|
||||
i.fa.fa-pencil-square-o
|
||||
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}" aria-label="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
if showStarCounter
|
||||
span
|
||||
= currentBoard.stars
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
span {{_ currentBoard.permission}}
|
||||
a.board-header-btn.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
if $eq watchLevel "watching"
|
||||
i.fa.fa-eye
|
||||
if $eq watchLevel "tracking"
|
||||
i.fa.fa-bell
|
||||
if $eq watchLevel "muted"
|
||||
i.fa.fa-bell-slash
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
||||
i.fa.fa-sort
|
||||
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
|
||||
if isSortActive
|
||||
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
|
||||
i.fa.fa-times-thin
|
||||
|
||||
a.board-header-btn.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
if $eq watchLevel "watching"
|
||||
i.fa.fa-eye
|
||||
if $eq watchLevel "tracking"
|
||||
i.fa.fa-bell
|
||||
if $eq watchLevel "muted"
|
||||
i.fa.fa-bell-slash
|
||||
span {{_ watchLevel}}
|
||||
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
||||
i.fa.fa-sort
|
||||
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
|
||||
if isSortActive
|
||||
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
|
||||
i.fa.fa-times-thin
|
||||
else
|
||||
a.board-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
|
||||
else
|
||||
a.board-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
span {{_ 'log-in'}}
|
||||
if isSandstorm
|
||||
if currentUser
|
||||
a.board-header-btn.js-open-archived-board
|
||||
i.fa.fa-archive
|
||||
|
||||
.board-header-btns.center
|
||||
//if showSort
|
||||
//
|
||||
a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
|
||||
//
|
||||
i.fa(class="{{directionClass}}")
|
||||
//
|
||||
span {{_ 'sort'}}{{_ listSortShortDesc}}
|
||||
|
||||
.board-header-btns.right
|
||||
if currentBoard
|
||||
if isMiniScreen
|
||||
if currentUser
|
||||
with currentBoard
|
||||
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
|
||||
i.fa.fa-pencil-square-o
|
||||
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
|
||||
a.board-header-btn.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
if $eq watchLevel "watching"
|
||||
i.fa.fa-eye
|
||||
if $eq watchLevel "tracking"
|
||||
i.fa.fa-bell
|
||||
if $eq watchLevel "muted"
|
||||
i.fa.fa-bell-slash
|
||||
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
||||
i.fa.fa-sort
|
||||
if isSortActive
|
||||
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
|
||||
i.fa.fa-times-thin
|
||||
|
||||
else
|
||||
a.board-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
|
||||
if isSandstorm
|
||||
if currentUser
|
||||
a.board-header-btn.js-open-archived-board
|
||||
i.fa.fa-archive
|
||||
|
||||
//if showSort
|
||||
// a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
|
||||
// i.fa(class="{{directionClass}}")
|
||||
// span {{_ 'sort'}}{{_ listSortShortDesc}}
|
||||
|
||||
a.board-header-btn.js-open-filter-view(
|
||||
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
|
||||
class="{{#if Filter.isActive}}emphasis{{/if}}")
|
||||
i.fa.fa-filter
|
||||
if Filter.isActive
|
||||
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
|
||||
i.fa.fa-times-thin
|
||||
|
||||
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
|
||||
i.fa.fa-search
|
||||
|
||||
unless currentBoard.isTemplatesBoard
|
||||
a.board-header-btn.js-toggle-board-view(
|
||||
title="{{_ 'board-view'}}")
|
||||
i.fa.fa-caret-down
|
||||
if $eq boardView 'board-view-swimlanes'
|
||||
i.fa.fa-th-large
|
||||
if $eq boardView 'board-view-lists'
|
||||
i.fa.fa-trello
|
||||
if $eq boardView 'board-view-cal'
|
||||
i.fa.fa-calendar
|
||||
|
||||
if canModifyBoard
|
||||
a.board-header-btn.js-multiselection-activate(
|
||||
title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
|
||||
class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
|
||||
i.fa.fa-check-square-o
|
||||
if MultiSelection.isActive
|
||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||
a.board-header-btn.js-open-filter-view(
|
||||
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
|
||||
class="{{#if Filter.isActive}}js-filter-active{{/if}}")
|
||||
i.fa.fa-filter
|
||||
span {{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}
|
||||
if Filter.isActive
|
||||
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
|
||||
i.fa.fa-times-thin
|
||||
|
||||
.separator
|
||||
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
|
||||
i.fa.fa-navicon
|
||||
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
|
||||
i.fa.fa-search
|
||||
span {{_ 'search'}}
|
||||
|
||||
unless currentBoard.isTemplatesBoard
|
||||
a.board-header-btn.js-toggle-board-view
|
||||
i.fa.fa-caret-down
|
||||
if $eq boardView 'board-view-swimlanes'
|
||||
i.fa.fa-th-large
|
||||
if $eq boardView 'board-view-lists'
|
||||
i.fa.fa-trello
|
||||
if $eq boardView 'board-view-cal'
|
||||
i.fa.fa-calendar
|
||||
if $eq boardView 'board-view-gantt'
|
||||
i.fa.fa-bar-chart
|
||||
span
|
||||
if $eq boardView 'board-view-swimlanes'
|
||||
| {{_ 'swimlanes'}}
|
||||
if $eq boardView 'board-view-lists'
|
||||
| {{_ 'lists'}}
|
||||
if $eq boardView 'board-view-cal'
|
||||
| {{_ 'calendar'}}
|
||||
if $eq boardView 'board-view-gantt'
|
||||
| {{_ 'gantt'}}
|
||||
if canModifyBoard
|
||||
a.board-header-btn.js-multiselection-activate(
|
||||
title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
|
||||
class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
|
||||
i.fa.fa-check-square-o
|
||||
span {{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}
|
||||
if MultiSelection.isActive
|
||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||
i.fa.fa-times-thin
|
||||
|
||||
.separator
|
||||
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
|
||||
i.fa.fa-bars
|
||||
|
||||
template(name="boardVisibilityList")
|
||||
ul.pop-over-list
|
||||
li
|
||||
with "private"
|
||||
a.js-select-visibility
|
||||
i.fa.fa-lock.colorful
|
||||
i.fa.fa-lock
|
||||
| {{_ 'private'}}
|
||||
if visibilityCheck
|
||||
i.fa.fa-check
|
||||
@@ -148,7 +162,7 @@ template(name="boardVisibilityList")
|
||||
li
|
||||
with "public"
|
||||
a.js-select-visibility
|
||||
i.fa.fa-globe.colorful
|
||||
i.fa.fa-globe
|
||||
| {{_ 'public'}}
|
||||
if visibilityCheck
|
||||
i.fa.fa-check
|
||||
@@ -162,7 +176,7 @@ template(name="boardChangeWatchPopup")
|
||||
li
|
||||
with "watching"
|
||||
a.js-select-watch
|
||||
i.fa.fa-eye.colorful
|
||||
i.fa.fa-eye
|
||||
| {{_ 'watching'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
@@ -170,7 +184,7 @@ template(name="boardChangeWatchPopup")
|
||||
li
|
||||
with "tracking"
|
||||
a.js-select-watch
|
||||
i.fa.fa-bell.colorful
|
||||
i.fa.fa-bell
|
||||
| {{_ 'tracking'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
@@ -178,7 +192,7 @@ template(name="boardChangeWatchPopup")
|
||||
li
|
||||
with "muted"
|
||||
a.js-select-watch
|
||||
i.fa.fa-bell-slash.colorful
|
||||
i.fa.fa-bell-slash
|
||||
| {{_ 'muted'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
@@ -189,35 +203,130 @@ template(name="boardChangeViewPopup")
|
||||
li
|
||||
with "board-view-swimlanes"
|
||||
a.js-open-swimlanes-view
|
||||
i.fa.fa-th-large.colorful
|
||||
| {{_ 'board-view-swimlanes'}}
|
||||
i.fa.fa-th-large
|
||||
| {{_ 'swimlanes'}}
|
||||
if $eq Utils.boardView "board-view-swimlanes"
|
||||
i.fa.fa-check
|
||||
li
|
||||
with "board-view-lists"
|
||||
a.js-open-lists-view
|
||||
i.fa.fa-trello.colorful
|
||||
i.fa.fa-trello
|
||||
| {{_ 'board-view-lists'}}
|
||||
if $eq Utils.boardView "board-view-lists"
|
||||
i.fa.fa-check
|
||||
li
|
||||
with "board-view-cal"
|
||||
a.js-open-cal-view
|
||||
i.fa.fa-calendar.colorful
|
||||
i.fa.fa-calendar
|
||||
| {{_ 'board-view-cal'}}
|
||||
if $eq Utils.boardView "board-view-cal"
|
||||
i.fa.fa-check
|
||||
li
|
||||
with "board-view-gantt"
|
||||
a.js-open-gantt-view
|
||||
i.fa.fa-bar-chart
|
||||
| {{_ 'board-view-gantt'}}
|
||||
if $eq Utils.boardView "board-view-gantt"
|
||||
i.fa.fa-check
|
||||
|
||||
template(name="createBoard")
|
||||
form
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
|
||||
if visibilityMenuIsOpen.get
|
||||
if visibilityMenuIsOpen
|
||||
+boardVisibilityList
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility.get 'public'
|
||||
if $eq visibility 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span.fa.fa-lock.colorful
|
||||
= " "
|
||||
| {{{_ 'board-private-info'}}}
|
||||
a.js-change-visibility {{_ 'change'}}.
|
||||
a.flex.js-toggle-add-template-container
|
||||
.materialCheckBox#add-template-container
|
||||
span {{_ 'add-template-container'}}
|
||||
input.primary.wide(type="submit" value="{{_ 'create'}}")
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-import-board {{_ 'import'}}
|
||||
span.quiet
|
||||
| /
|
||||
a.js-board-template {{_ 'template'}}
|
||||
|
||||
template(name="createBoardPopup")
|
||||
form
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
|
||||
if visibilityMenuIsOpen
|
||||
+boardVisibilityList
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span.fa.fa-lock.colorful
|
||||
= " "
|
||||
| {{{_ 'board-private-info'}}}
|
||||
a.js-change-visibility {{_ 'change'}}.
|
||||
a.flex.js-toggle-add-template-container
|
||||
.materialCheckBox#add-template-container
|
||||
span {{_ 'add-template-container'}}
|
||||
input.primary.wide(type="submit" value="{{_ 'create'}}")
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-import-board {{_ 'import'}}
|
||||
span.quiet
|
||||
| /
|
||||
a.js-board-template {{_ 'template'}}
|
||||
|
||||
template(name="headerBarCreateBoardPopup")
|
||||
form
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
|
||||
if visibilityMenuIsOpen
|
||||
+boardVisibilityList
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span.fa.fa-lock.colorful
|
||||
= " "
|
||||
| {{{_ 'board-private-info'}}}
|
||||
a.js-change-visibility {{_ 'change'}}.
|
||||
a.flex.js-toggle-add-template-container
|
||||
.materialCheckBox#add-template-container
|
||||
span {{_ 'add-template-container'}}
|
||||
input.primary.wide(type="submit" value="{{_ 'create'}}")
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-import-board {{_ 'import'}}
|
||||
span.quiet
|
||||
| /
|
||||
a.js-board-template {{_ 'template'}}
|
||||
|
||||
// New popup for Template Container creation; shares the same form content
|
||||
template(name="createTemplateContainerPopup")
|
||||
form
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
|
||||
if visibilityMenuIsOpen
|
||||
+boardVisibilityList
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
@@ -238,19 +347,30 @@ template(name="createBoard")
|
||||
a.js-board-template {{_ 'template'}}
|
||||
|
||||
//template(name="listsortPopup")
|
||||
// h2
|
||||
// | {{_ 'list-sort-by'}}
|
||||
// hr
|
||||
// ul.pop-over-list
|
||||
// each value in allowedSortValues
|
||||
// li
|
||||
// a.js-sort-by(name="{{value.name}}")
|
||||
// if $eq sortby value.name
|
||||
// i(class="fa {{Direction}}")
|
||||
// | {{_ value.label }}{{_ value.shortLabel}}
|
||||
// if $eq sortby value.name
|
||||
// i(class="fa fa-check")
|
||||
|
||||
//
|
||||
h2
|
||||
//
|
||||
| {{_ 'list-sort-by'}}
|
||||
//
|
||||
hr
|
||||
//
|
||||
ul.pop-over-list
|
||||
//
|
||||
each value in allowedSortValues
|
||||
//
|
||||
li
|
||||
//
|
||||
a.js-sort-by(name="{{value.name}}")
|
||||
//
|
||||
if $eq sortby value.name
|
||||
//
|
||||
| {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
|
||||
//
|
||||
| {{_ value.label }}{{_ value.shortLabel}}
|
||||
//
|
||||
if $eq sortby value.name
|
||||
//
|
||||
i.fa.fa-check
|
||||
template(name="boardChangeTitlePopup")
|
||||
form
|
||||
label
|
||||
@@ -269,14 +389,22 @@ template(name="boardCreateRulePopup")
|
||||
template(name="cardsSortPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-sort-due {{_ 'due-date'}}
|
||||
a.js-sort-due
|
||||
i.fa.fa-calendar
|
||||
| {{_ 'due-date'}}
|
||||
hr
|
||||
li
|
||||
a.js-sort-title {{_ 'title-alphabetically'}}
|
||||
a.js-sort-title
|
||||
i.fa.fa-sort-alpha-asc
|
||||
| {{_ 'title-alphabetically'}}
|
||||
hr
|
||||
li
|
||||
a.js-sort-created-desc {{_ 'created-at-newest-first'}}
|
||||
a.js-sort-created-desc
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'created-at-newest-first'}}
|
||||
hr
|
||||
li
|
||||
a.js-sort-created-asc {{_ 'created-at-oldest-first'}}
|
||||
a.js-sort-created-asc
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'created-at-oldest-first'}}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import dragscroll from '@wekanteam/dragscroll';
|
||||
import getSlug from 'limax';
|
||||
import Boards from '/models/boards';
|
||||
import Swimlanes from '/models/swimlanes';
|
||||
import TableVisibilityModeSettings from '/models/tableVisibilityModeSettings';
|
||||
import { Filter } from '/client/lib/filter';
|
||||
import { MultiSelection } from '/client/lib/multiSelection';
|
||||
import { getSidebarInstance } from '/client/features/sidebar/service';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
/*
|
||||
const DOWNCLS = 'fa-sort-down';
|
||||
@@ -9,7 +18,8 @@ const UPCLS = 'fa-sort-up';
|
||||
const sortCardsBy = new ReactiveVar('');
|
||||
|
||||
Template.boardChangeTitlePopup.events({
|
||||
submit(event, templateInstance) {
|
||||
async submit(event, templateInstance) {
|
||||
event.preventDefault();
|
||||
const newTitle = templateInstance
|
||||
.$('.js-board-name')
|
||||
.val()
|
||||
@@ -19,22 +29,37 @@ Template.boardChangeTitlePopup.events({
|
||||
.val()
|
||||
.trim();
|
||||
if (newTitle) {
|
||||
this.rename(newTitle);
|
||||
this.setDescription(newDesc);
|
||||
const board = Utils.getCurrentBoard();
|
||||
if (board) {
|
||||
await board.rename(newTitle);
|
||||
await board.setDescription(newDesc);
|
||||
}
|
||||
Popup.back();
|
||||
}
|
||||
event.preventDefault();
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.boardHeaderBar.onCreated(function () {
|
||||
Meteor.subscribe('tableVisibilityModeSettings');
|
||||
});
|
||||
|
||||
Template.boardHeaderBar.helpers({
|
||||
notDisplayThisBoard() {
|
||||
const allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return (
|
||||
allowPrivateVisibilityOnly !== undefined &&
|
||||
allowPrivateVisibilityOnly.booleanValue &&
|
||||
currentBoard &&
|
||||
currentBoard.permission === 'public'
|
||||
);
|
||||
},
|
||||
|
||||
watchLevel() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return currentBoard && currentBoard.getWatchLevel(Meteor.userId());
|
||||
},
|
||||
|
||||
|
||||
|
||||
isStarred() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
@@ -46,97 +71,130 @@ BlazeComponent.extendComponent({
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return currentBoard && currentBoard.stars >= 2;
|
||||
},
|
||||
/*
|
||||
showSort() {
|
||||
return ReactiveCache.getCurrentUser().hasSortBy();
|
||||
},
|
||||
directionClass() {
|
||||
return this.currentDirection() === -1 ? DOWNCLS : UPCLS;
|
||||
},
|
||||
changeDirection() {
|
||||
const direction = 0 - this.currentDirection() === -1 ? '-' : '';
|
||||
Meteor.call('setListSortBy', direction + this.currentListSortBy());
|
||||
},
|
||||
currentDirection() {
|
||||
return ReactiveCache.getCurrentUser().getListSortByDirection();
|
||||
},
|
||||
currentListSortBy() {
|
||||
return ReactiveCache.getCurrentUser().getListSortBy();
|
||||
},
|
||||
listSortShortDesc() {
|
||||
return `list-label-short-${this.currentListSortBy()}`;
|
||||
},
|
||||
*/
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
|
||||
'click .js-star-board'() {
|
||||
ReactiveCache.getCurrentUser().toggleBoardStar(Session.get('currentBoard'));
|
||||
},
|
||||
'click .js-open-board-menu': Popup.open('boardMenu'),
|
||||
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
|
||||
'click .js-watch-board': Popup.open('boardChangeWatch'),
|
||||
'click .js-open-archived-board'() {
|
||||
Modal.open('archivedBoards');
|
||||
},
|
||||
'click .js-toggle-board-view': Popup.open('boardChangeView'),
|
||||
'click .js-toggle-sidebar'() {
|
||||
Sidebar.toggle();
|
||||
},
|
||||
'click .js-open-filter-view'() {
|
||||
Sidebar.setView('filter');
|
||||
},
|
||||
'click .js-sort-cards': Popup.open('cardsSort'),
|
||||
/*
|
||||
'click .js-open-sort-view'(evt) {
|
||||
const target = evt.target;
|
||||
if (target.tagName === 'I') {
|
||||
// click on the text, popup choices
|
||||
this.changeDirection();
|
||||
} else {
|
||||
// change the sort order
|
||||
Popup.open('listsort')(evt);
|
||||
}
|
||||
},
|
||||
*/
|
||||
'click .js-filter-reset'(event) {
|
||||
event.stopPropagation();
|
||||
Sidebar.setView();
|
||||
Filter.reset();
|
||||
},
|
||||
'click .js-sort-reset'() {
|
||||
Session.set('sortBy', '');
|
||||
},
|
||||
'click .js-open-search-view'() {
|
||||
Sidebar.setView('search');
|
||||
},
|
||||
'click .js-multiselection-activate'() {
|
||||
const currentCard = Utils.getCurrentCardId();
|
||||
MultiSelection.activate();
|
||||
if (currentCard) {
|
||||
MultiSelection.add(currentCard);
|
||||
}
|
||||
},
|
||||
'click .js-multiselection-reset'(event) {
|
||||
event.stopPropagation();
|
||||
MultiSelection.disable();
|
||||
},
|
||||
'click .js-log-in'() {
|
||||
FlowRouter.go('atSignIn');
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('boardHeaderBar');
|
||||
|
||||
Template.boardHeaderBar.helpers({
|
||||
boardView() {
|
||||
return Utils.boardView();
|
||||
},
|
||||
isSortActive() {
|
||||
return Session.get('sortBy') ? true : false;
|
||||
},
|
||||
sortCardsIcon() {
|
||||
const sortBy = Session.get('sortBy');
|
||||
if (!sortBy) {
|
||||
return '🃏'; // Card icon when nothing is selected
|
||||
}
|
||||
|
||||
// Determine which sort option is active based on sortBy object
|
||||
if (sortBy.dueAt) {
|
||||
return '📅'; // Due date icon
|
||||
} else if (sortBy.title) {
|
||||
return '🔤'; // Alphabet icon
|
||||
} else if (sortBy.createdAt) {
|
||||
return sortBy.createdAt === 1 ? '⬆️' : '⬇️'; // Up/down arrow based on direction
|
||||
}
|
||||
|
||||
return '🃏'; // Default card icon
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardHeaderBar.events({
|
||||
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
|
||||
'click .js-star-board'() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
if (boardId) {
|
||||
Meteor.call('toggleBoardStar', boardId);
|
||||
}
|
||||
},
|
||||
'click .js-open-board-menu': Popup.open('boardMenu'),
|
||||
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
|
||||
'click .js-watch-board': Popup.open('boardChangeWatch'),
|
||||
'click .js-open-archived-board'() {
|
||||
Modal.open('archivedBoards');
|
||||
},
|
||||
'click .js-toggle-board-view': Popup.open('boardChangeView'),
|
||||
'click .js-toggle-sidebar'() {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Hamburger menu clicked');
|
||||
}
|
||||
// Use the same approach as keyboard shortcuts
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar && typeof sidebar.toggle === 'function') {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Using Sidebar.toggle()');
|
||||
}
|
||||
sidebar.toggle();
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('Sidebar not available, trying alternative approach');
|
||||
}
|
||||
// Try to trigger the sidebar through the global Blaze helper
|
||||
if (typeof Blaze !== 'undefined' && Blaze._globalHelpers && Blaze._globalHelpers.Sidebar) {
|
||||
const blazeSidebar = Blaze._globalHelpers.Sidebar();
|
||||
if (blazeSidebar && typeof blazeSidebar.toggle === 'function') {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Using Blaze helper Sidebar.toggle()');
|
||||
}
|
||||
blazeSidebar.toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-open-filter-view'() {
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar) {
|
||||
sidebar.setView('filter');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
},
|
||||
'click .js-sort-cards': Popup.open('cardsSort'),
|
||||
/*
|
||||
'click .js-open-sort-view'(evt) {
|
||||
const target = evt.target;
|
||||
if (target.tagName === 'I') {
|
||||
// click on the text, popup choices
|
||||
this.changeDirection();
|
||||
} else {
|
||||
// change the sort order
|
||||
Popup.open('listsort')(evt);
|
||||
}
|
||||
},
|
||||
*/
|
||||
'click .js-filter-reset'(event) {
|
||||
event.stopPropagation();
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar) {
|
||||
sidebar.setView();
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
Filter.reset();
|
||||
},
|
||||
'click .js-sort-reset'() {
|
||||
Session.set('sortBy', '');
|
||||
},
|
||||
'click .js-open-search-view'() {
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar) {
|
||||
sidebar.setView('search');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
},
|
||||
'click .js-multiselection-activate'() {
|
||||
const currentCard = Utils.getCurrentCardId();
|
||||
MultiSelection.activate();
|
||||
if (currentCard) {
|
||||
MultiSelection.add(currentCard);
|
||||
}
|
||||
},
|
||||
'click .js-multiselection-reset'(event) {
|
||||
event.stopPropagation();
|
||||
MultiSelection.disable();
|
||||
},
|
||||
'click .js-log-in'() {
|
||||
FlowRouter.go('atSignIn');
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardChangeViewPopup.events({
|
||||
@@ -152,194 +210,241 @@ Template.boardChangeViewPopup.events({
|
||||
Utils.setBoardView('board-view-cal');
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
const CreateBoard = BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'createBoard';
|
||||
},
|
||||
|
||||
onCreated() {
|
||||
this.visibilityMenuIsOpen = new ReactiveVar(false);
|
||||
this.visibility = new ReactiveVar('private');
|
||||
this.boardId = new ReactiveVar('');
|
||||
Meteor.subscribe('tableVisibilityModeSettings');
|
||||
},
|
||||
|
||||
notAllowPrivateVisibilityOnly(){
|
||||
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
|
||||
},
|
||||
|
||||
visibilityCheck() {
|
||||
return this.currentData() === this.visibility.get();
|
||||
},
|
||||
|
||||
setVisibility(visibility) {
|
||||
this.visibility.set(visibility);
|
||||
this.visibilityMenuIsOpen.set(false);
|
||||
},
|
||||
|
||||
toggleVisibilityMenu() {
|
||||
this.visibilityMenuIsOpen.set(!this.visibilityMenuIsOpen.get());
|
||||
},
|
||||
|
||||
toggleAddTemplateContainer() {
|
||||
$('#add-template-container').toggleClass('is-checked');
|
||||
},
|
||||
|
||||
onSubmit(event) {
|
||||
event.preventDefault();
|
||||
const title = this.find('.js-new-board-title').value;
|
||||
|
||||
const addTemplateContainer = $('#add-template-container.is-checked').length > 0;
|
||||
if (addTemplateContainer) {
|
||||
//const templateContainerId = Meteor.call('setCreateTemplateContainer');
|
||||
//Utils.goBoardId(templateContainerId);
|
||||
//alert('niinku template ' + Meteor.call('setCreateTemplateContainer'));
|
||||
|
||||
this.boardId.set(
|
||||
Boards.insert({
|
||||
// title: TAPi18n.__('templates'),
|
||||
title: title,
|
||||
permission: 'private',
|
||||
type: 'template-container',
|
||||
}),
|
||||
);
|
||||
|
||||
// Insert the card templates swimlane
|
||||
Swimlanes.insert({
|
||||
// title: TAPi18n.__('card-templates-swimlane'),
|
||||
title: 'Card Templates',
|
||||
boardId: this.boardId.get(),
|
||||
sort: 1,
|
||||
type: 'template-container',
|
||||
}),
|
||||
|
||||
// Insert the list templates swimlane
|
||||
Swimlanes.insert(
|
||||
{
|
||||
// title: TAPi18n.__('list-templates-swimlane'),
|
||||
title: 'List Templates',
|
||||
boardId: this.boardId.get(),
|
||||
sort: 2,
|
||||
type: 'template-container',
|
||||
},
|
||||
);
|
||||
|
||||
// Insert the board templates swimlane
|
||||
Swimlanes.insert(
|
||||
{
|
||||
//title: TAPi18n.__('board-templates-swimlane'),
|
||||
title: 'Board Templates',
|
||||
boardId: this.boardId.get(),
|
||||
sort: 3,
|
||||
type: 'template-container',
|
||||
},
|
||||
);
|
||||
|
||||
Utils.goBoardId(this.boardId.get());
|
||||
|
||||
} else {
|
||||
const visibility = this.visibility.get();
|
||||
|
||||
this.boardId.set(
|
||||
Boards.insert({
|
||||
title,
|
||||
permission: visibility,
|
||||
}),
|
||||
);
|
||||
|
||||
Swimlanes.insert({
|
||||
title: 'Default',
|
||||
boardId: this.boardId.get(),
|
||||
});
|
||||
|
||||
Utils.goBoardId(this.boardId.get());
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-select-visibility'() {
|
||||
this.setVisibility(this.currentData());
|
||||
},
|
||||
'click .js-change-visibility': this.toggleVisibilityMenu,
|
||||
'click .js-import': Popup.open('boardImportBoard'),
|
||||
submit: this.onSubmit,
|
||||
'click .js-import-board': Popup.open('chooseBoardSource'),
|
||||
'click .js-board-template': Popup.open('searchElement'),
|
||||
'click .js-toggle-add-template-container': this.toggleAddTemplateContainer,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('createBoardPopup');
|
||||
|
||||
(class HeaderBarCreateBoard extends CreateBoard {
|
||||
onSubmit(event) {
|
||||
super.onSubmit(event);
|
||||
// Immediately star boards crated with the headerbar popup.
|
||||
ReactiveCache.getCurrentUser().toggleBoardStar(this.boardId.get());
|
||||
}
|
||||
}.register('headerBarCreateBoardPopup'));
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
notAllowPrivateVisibilityOnly(){
|
||||
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
|
||||
},
|
||||
visibilityCheck() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return this.currentData() === currentBoard.permission;
|
||||
},
|
||||
|
||||
selectBoardVisibility() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const visibility = this.currentData();
|
||||
currentBoard.setVisibility(visibility);
|
||||
'click .js-open-gantt-view'() {
|
||||
Utils.setBoardView('board-view-gantt');
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-select-visibility': this.selectBoardVisibility,
|
||||
},
|
||||
];
|
||||
// Shared setup for all create board popups
|
||||
function setupCreateBoardState(tpl) {
|
||||
tpl.visibilityMenuIsOpen = new ReactiveVar(false);
|
||||
tpl.visibility = new ReactiveVar('private');
|
||||
tpl.boardId = new ReactiveVar('');
|
||||
Meteor.subscribe('tableVisibilityModeSettings');
|
||||
}
|
||||
|
||||
function createBoardHelpers() {
|
||||
return {
|
||||
visibilityMenuIsOpen() {
|
||||
return Template.instance().visibilityMenuIsOpen.get();
|
||||
},
|
||||
visibility() {
|
||||
return Template.instance().visibility.get();
|
||||
},
|
||||
notAllowPrivateVisibilityOnly() {
|
||||
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly')?.booleanValue;
|
||||
},
|
||||
visibilityCheck() {
|
||||
return Template.currentData() === Template.instance().visibility.get();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function createBoardSubmit(tpl, event) {
|
||||
event.preventDefault();
|
||||
const title = tpl.find('.js-new-board-title').value;
|
||||
const slug = getSlug(title) || 'board';
|
||||
|
||||
const addTemplateContainer = $('#add-template-container.is-checked').length > 0;
|
||||
if (addTemplateContainer) {
|
||||
tpl.boardId.set(
|
||||
await Meteor.callAsync('createBoardWithInitialSwimlanes', {
|
||||
title,
|
||||
slug,
|
||||
permission: 'private',
|
||||
type: 'template-container',
|
||||
migrationVersion: 1,
|
||||
swimlanes: [
|
||||
{ title: 'Card Templates', sort: 1, type: 'template-container' },
|
||||
{ title: 'List Templates', sort: 2, type: 'template-container' },
|
||||
{ title: 'Board Templates', sort: 3, type: 'template-container' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
// Assign to space if one was selected
|
||||
const spaceId = Session.get('createBoardInWorkspace');
|
||||
if (spaceId) {
|
||||
Meteor.call('assignBoardToWorkspace', tpl.boardId.get(), spaceId, (err) => {
|
||||
if (err) console.error('Error assigning board to space:', err);
|
||||
});
|
||||
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||
}
|
||||
|
||||
FlowRouter.go('board', { id: tpl.boardId.get(), slug });
|
||||
|
||||
} else {
|
||||
const visibility = tpl.visibility.get();
|
||||
|
||||
tpl.boardId.set(
|
||||
await Meteor.callAsync('createBoardWithInitialSwimlanes', {
|
||||
title,
|
||||
slug,
|
||||
permission: visibility,
|
||||
migrationVersion: 1,
|
||||
swimlanes: [{ title: 'Default' }],
|
||||
}),
|
||||
);
|
||||
|
||||
// Assign to space if one was selected
|
||||
const spaceId = Session.get('createBoardInWorkspace');
|
||||
if (spaceId) {
|
||||
Meteor.call('assignBoardToWorkspace', tpl.boardId.get(), spaceId, (err) => {
|
||||
if (err) console.error('Error assigning board to space:', err);
|
||||
});
|
||||
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||
}
|
||||
|
||||
FlowRouter.go('board', { id: tpl.boardId.get(), slug });
|
||||
}
|
||||
}
|
||||
|
||||
function createBoardEvents() {
|
||||
return {
|
||||
'click .js-select-visibility'(event, tpl) {
|
||||
tpl.visibility.set(this);
|
||||
tpl.visibilityMenuIsOpen.set(false);
|
||||
},
|
||||
'click .js-change-visibility'(event, tpl) {
|
||||
tpl.visibilityMenuIsOpen.set(!tpl.visibilityMenuIsOpen.get());
|
||||
},
|
||||
'click .js-import': Popup.open('boardImportBoard'),
|
||||
async 'submit'(event, tpl) {
|
||||
await createBoardSubmit(tpl, event);
|
||||
},
|
||||
'click .js-import-board': Popup.open('chooseBoardSource'),
|
||||
'click .js-board-template': Popup.open('searchElement'),
|
||||
'click .js-toggle-add-template-container'() {
|
||||
$('#add-template-container').toggleClass('is-checked');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// createBoard (non-popup version)
|
||||
Template.createBoard.onCreated(function () {
|
||||
setupCreateBoardState(this);
|
||||
});
|
||||
|
||||
Template.createBoard.helpers(createBoardHelpers());
|
||||
|
||||
Template.createBoard.events(createBoardEvents());
|
||||
|
||||
// createBoardPopup
|
||||
Template.createBoardPopup.onCreated(function () {
|
||||
setupCreateBoardState(this);
|
||||
});
|
||||
|
||||
Template.createBoardPopup.helpers(createBoardHelpers());
|
||||
|
||||
Template.createBoardPopup.events(createBoardEvents());
|
||||
|
||||
// createTemplateContainerPopup
|
||||
Template.createTemplateContainerPopup.onCreated(function () {
|
||||
setupCreateBoardState(this);
|
||||
});
|
||||
|
||||
Template.createTemplateContainerPopup.onRendered(function () {
|
||||
// Always pre-check the template container checkbox for this popup
|
||||
$('#add-template-container').addClass('is-checked');
|
||||
});
|
||||
|
||||
Template.createTemplateContainerPopup.helpers(createBoardHelpers());
|
||||
|
||||
Template.createTemplateContainerPopup.events(createBoardEvents());
|
||||
|
||||
// headerBarCreateBoardPopup
|
||||
Template.headerBarCreateBoardPopup.onCreated(function () {
|
||||
setupCreateBoardState(this);
|
||||
});
|
||||
|
||||
Template.headerBarCreateBoardPopup.helpers(createBoardHelpers());
|
||||
|
||||
Template.headerBarCreateBoardPopup.events({
|
||||
'click .js-select-visibility'(event, tpl) {
|
||||
tpl.visibility.set(this);
|
||||
tpl.visibilityMenuIsOpen.set(false);
|
||||
},
|
||||
}).register('boardChangeVisibilityPopup');
|
||||
'click .js-change-visibility'(event, tpl) {
|
||||
tpl.visibilityMenuIsOpen.set(!tpl.visibilityMenuIsOpen.get());
|
||||
},
|
||||
'click .js-import': Popup.open('boardImportBoard'),
|
||||
async submit(event, tpl) {
|
||||
await createBoardSubmit(tpl, event);
|
||||
// Immediately star boards created with the headerbar popup.
|
||||
await ReactiveCache.getCurrentUser().toggleBoardStar(tpl.boardId.get());
|
||||
},
|
||||
'click .js-import-board': Popup.open('chooseBoardSource'),
|
||||
'click .js-board-template': Popup.open('searchElement'),
|
||||
'click .js-toggle-add-template-container'() {
|
||||
$('#add-template-container').toggleClass('is-checked');
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.boardVisibilityList.helpers({
|
||||
notAllowPrivateVisibilityOnly() {
|
||||
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly')?.booleanValue;
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardChangeVisibilityPopup.onCreated(function () {
|
||||
Meteor.subscribe('tableVisibilityModeSettings');
|
||||
});
|
||||
|
||||
Template.boardChangeVisibilityPopup.helpers({
|
||||
notAllowPrivateVisibilityOnly(){
|
||||
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly')?.booleanValue;
|
||||
},
|
||||
visibilityCheck() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return this === currentBoard.permission;
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardChangeVisibilityPopup.events({
|
||||
'click .js-select-visibility'() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const visibility = this;
|
||||
if (typeof visibility === 'string') {
|
||||
currentBoard.setVisibility(visibility);
|
||||
Popup.back();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardChangeWatchPopup.helpers({
|
||||
watchLevel() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return currentBoard.getWatchLevel(Meteor.userId());
|
||||
},
|
||||
|
||||
watchCheck() {
|
||||
return this.currentData() === this.watchLevel();
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return this === currentBoard.getWatchLevel(Meteor.userId());
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-select-watch'() {
|
||||
const level = this.currentData();
|
||||
Meteor.call(
|
||||
'watch',
|
||||
'board',
|
||||
Session.get('currentBoard'),
|
||||
level,
|
||||
(err, ret) => {
|
||||
if (!err && ret) Popup.back();
|
||||
},
|
||||
);
|
||||
Template.boardChangeWatchPopup.events({
|
||||
'click .js-select-watch'() {
|
||||
const level = this;
|
||||
if (typeof level === 'string') {
|
||||
Meteor.call(
|
||||
'watch',
|
||||
'board',
|
||||
Session.get('currentBoard'),
|
||||
level,
|
||||
(err, ret) => {
|
||||
if (!err && ret) Popup.back();
|
||||
},
|
||||
},
|
||||
];
|
||||
);
|
||||
}
|
||||
},
|
||||
}).register('boardChangeWatchPopup');
|
||||
});
|
||||
|
||||
/*
|
||||
BlazeComponent.extendComponent({
|
||||
// BlazeComponent.extendComponent was removed - this code is unused.
|
||||
// Original listsortPopup component:
|
||||
// {
|
||||
onCreated() {
|
||||
//this.sortBy = new ReactiveVar();
|
||||
////this.sortDirection = new ReactiveVar();
|
||||
@@ -407,46 +512,40 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('listsortPopup');
|
||||
// }.register('listsortPopup');
|
||||
*/
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-sort-due'() {
|
||||
const sortBy = {
|
||||
dueAt: 1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('due-date'));
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-sort-title'() {
|
||||
const sortBy = {
|
||||
title: 1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('title'));
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-sort-created-asc'() {
|
||||
const sortBy = {
|
||||
createdAt: 1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('date-created-newest-first'));
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-sort-created-desc'() {
|
||||
const sortBy = {
|
||||
createdAt: -1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('date-created-oldest-first'));
|
||||
Popup.back();
|
||||
},
|
||||
},
|
||||
];
|
||||
Template.cardsSortPopup.events({
|
||||
'click .js-sort-due'() {
|
||||
const sortBy = {
|
||||
dueAt: 1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('due-date'));
|
||||
Popup.back();
|
||||
},
|
||||
}).register('cardsSortPopup');
|
||||
'click .js-sort-title'() {
|
||||
const sortBy = {
|
||||
title: 1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('title'));
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-sort-created-asc'() {
|
||||
const sortBy = {
|
||||
createdAt: 1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('date-created-newest-first'));
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-sort-created-desc'() {
|
||||
const sortBy = {
|
||||
createdAt: -1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('date-created-oldest-first'));
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
@import url("../../../css/reset.css") print, screen;
|
||||
@import url("../../../public/css/reset.css") print, screen;
|
||||
|
||||
/* Board List Header with Zoom Controls */
|
||||
.board-list-header {
|
||||
@@ -8,6 +8,350 @@
|
||||
padding: 1vh 0;
|
||||
}
|
||||
|
||||
/* Two-column layout for All Boards */
|
||||
.boards-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.boards-left-menu {
|
||||
border-right: 1px solid #e0e0e0;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.boards-left-menu ul.menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.boards-left-menu .menu-item {
|
||||
margin: 4px 0;
|
||||
}
|
||||
.boards-left-menu .menu-item a {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.boards-left-menu .menu-item .menu-label {
|
||||
flex: 1;
|
||||
}
|
||||
.boards-left-menu .menu-item .menu-count {
|
||||
background: #ddd;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.boards-left-menu .menu-item.active a,
|
||||
.boards-left-menu .menu-item a:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.boards-left-menu .menu-item.active .menu-count {
|
||||
background: #bbb;
|
||||
}
|
||||
|
||||
/* Drag-over state for menu items (for dropping boards on Remaining) */
|
||||
.boards-left-menu .menu-item a.drag-over {
|
||||
background: #d0e8ff;
|
||||
border: 2px dashed #2196F3;
|
||||
}
|
||||
|
||||
/* Highlight valid drop targets while a board is being dragged */
|
||||
.workspace-node.board-drag-hint > .workspace-node-content {
|
||||
background: #e8f5e9;
|
||||
border: 1px dashed #4CAF50;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.boards-left-menu .menu-item a.board-drag-hint {
|
||||
background: #e8f5e9;
|
||||
border: 1px dashed #4CAF50;
|
||||
}
|
||||
|
||||
.workspaces-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: bold;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.workspaces-header .js-add-space,
|
||||
.workspaces-header .js-add-workspace {
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.workspace-tree {
|
||||
list-style: none;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.workspace-node {
|
||||
margin: 2px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.workspace-node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.workspace-node.dragging > .workspace-node-content {
|
||||
opacity: 0.5;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.workspace-node.drag-over > .workspace-node-content {
|
||||
background: #d0e8ff;
|
||||
border: 2px dashed #2196F3;
|
||||
}
|
||||
|
||||
.workspace-drag-handle {
|
||||
cursor: grab;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
padding: 0 4px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.workspace-drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.workspace-node .js-select-space {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-icon .workspace-icon-value,
|
||||
.workspace-node .workspace-icon .workspace-icon-value p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-icon .emoji-icon {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-count {
|
||||
background: #ddd;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.workspace-node .js-edit-space,
|
||||
.workspace-node .js-add-subspace {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.workspace-node .js-edit-space:hover,
|
||||
.workspace-node .js-add-subspace:hover {
|
||||
opacity: 1;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.workspace-node .js-open-workspace-menu {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.workspace-node .js-open-workspace-menu:hover {
|
||||
opacity: 1;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.workspace-actions-form label,
|
||||
.workspace-subspace-form label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.workspace-actions-form input[type='text'],
|
||||
.workspace-subspace-form input[type='text'] {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.workspace-actions-form .workspace-color-label {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.workspace-actions-form .workspace-color-list {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.workspace-delete-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.workspace-delete-confirm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workspace-node.active > .workspace-node-content .js-select-space,
|
||||
.workspace-node > .workspace-node-content:hover .js-select-space {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.workspace-node.active .workspace-count {
|
||||
background: #bbb;
|
||||
}
|
||||
|
||||
.boards-right-grid {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.boards-path-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.boards-path-header .path-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.boards-path-header .multiselection-hint {
|
||||
background: #FFF3CD;
|
||||
color: #856404;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
border: 1px solid #FFE69C;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.boards-path-header .path-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.boards-path-header .path-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.boards-path-header .path-text {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn {
|
||||
padding: 6px 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn:hover {
|
||||
background: #f0f0f0;
|
||||
border-color: #bbb;
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn.emphasis {
|
||||
background: #2196F3;
|
||||
color: #fff;
|
||||
border-color: #2196F3;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.5);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn.emphasis:hover {
|
||||
background: #1976D2;
|
||||
box-shadow: 0 3px 12px rgba(33, 150, 243, 0.7);
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn-close {
|
||||
padding: 4px 10px;
|
||||
background: #f44336;
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-left: 10px; /* Extra space between MultiSelection toggle and Remove Filter */
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn-close:hover {
|
||||
background: #d32f2f;
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -103,26 +447,38 @@
|
||||
transform: rotate(4deg);
|
||||
display: block !important;
|
||||
}
|
||||
.board-list li.starred .fa-star,
|
||||
.board-list li.starred .fa-star-o {
|
||||
.board-list li.starred .is-star-active,
|
||||
.board-list li.starred .is-not-star-active {
|
||||
opacity: 1;
|
||||
color: #ffd700;
|
||||
}
|
||||
/* Show star icon on hover even for non-starred boards */
|
||||
.board-list li:hover .is-star-active,
|
||||
.board-list li:hover .is-not-star-active {
|
||||
opacity: 1;
|
||||
}
|
||||
.board-list .board-list-item {
|
||||
overflow: hidden;
|
||||
background-color: #999;
|
||||
background-color: inherit; /* Inherit board color from parent li.js-board */
|
||||
color: #f6f6f6;
|
||||
min-height: 100px;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
border-radius: 3px;
|
||||
border-radius: 0; /* No border-radius - parent .js-board has it */
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
padding: 8px;
|
||||
margin: 8px;
|
||||
padding: 36px 8px 32px 8px; /* Top padding for drag handle, bottom for checkbox */
|
||||
margin: 0; /* No margin - moved to parent .js-board */
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.board-list .board-list-item > .js-open-board {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
}
|
||||
.board-list .board-list-item.template-container {
|
||||
border: 4px solid #fff;
|
||||
}
|
||||
@@ -150,13 +506,27 @@
|
||||
.board-list .js-add-board .label {
|
||||
font-weight: normal;
|
||||
line-height: 56px;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #999; /* Darker background for better text contrast */
|
||||
border-radius: 3px;
|
||||
padding: 36px 8px 32px 8px;
|
||||
color: #fff; /* White text */
|
||||
}
|
||||
.board-list .js-add-board :hover {
|
||||
background-color: #939393;
|
||||
.board-list .js-add-board .label i {
|
||||
color: #fff; /* White icon */
|
||||
}
|
||||
.board-list .fa-star,
|
||||
.board-list .fa-star-o {
|
||||
bottom: 0;
|
||||
.board-list .js-add-board .label:hover {
|
||||
background-color: #808080; /* Even darker on hover */
|
||||
}
|
||||
.board-list .js-add-board .label:hover i {
|
||||
color: #fff; /* Keep icon white on hover */
|
||||
}
|
||||
.board-list .is-star-active,
|
||||
.board-list .is-not-star-active {
|
||||
top: 0;
|
||||
font-size: 14px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
@@ -164,7 +534,6 @@
|
||||
padding: 9px 9px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition-duration: 0.15s;
|
||||
transition-property: color, font-size, background;
|
||||
}
|
||||
@@ -212,32 +581,133 @@
|
||||
transition-duration: 0.15s;
|
||||
transition-property: color, font-size, background;
|
||||
}
|
||||
.board-list li:hover a:hover .fa-star,
|
||||
.board-list li:hover a:hover .is-star-active,
|
||||
.board-list li:hover a:hover .fa-clone,
|
||||
.board-list li:hover a:hover .fa-archive,
|
||||
.board-list li:hover a:hover .fa-star-o {
|
||||
.board-list li:hover a:hover .is-not-star-active {
|
||||
color: #fff;
|
||||
}
|
||||
.board-list li:hover a .fa-star,
|
||||
.board-list li:hover a .is-star-active,
|
||||
.board-list li:hover a .fa-clone,
|
||||
.board-list li:hover a .fa-archive,
|
||||
.board-list li:hover a .fa-star-o {
|
||||
.board-list li:hover a .is-not-star-active {
|
||||
color: #fff;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.board-list li:hover a .fa-star:hover,
|
||||
.board-list li:hover a .is-star-active:hover,
|
||||
.board-list li:hover a .fa-clone:hover,
|
||||
.board-list li:hover a .fa-archive:hover,
|
||||
.board-list li:hover a .fa-star-o:hover {
|
||||
.board-list li:hover a .is-not-star-active:hover {
|
||||
font-size: 18px;
|
||||
opacity: 1;
|
||||
}
|
||||
.board-list li:hover a .fa-star.is-star-active,
|
||||
.board-list li:hover a .fa-clone.is-star-active,
|
||||
.board-list li:hover a .fa-archive.is-star-active,
|
||||
.board-list li:hover a .fa-star-o.is-star-active {
|
||||
.board-list li:hover a .is-star-active,
|
||||
.board-list li:hover a .fa-clone,
|
||||
.board-list li:hover a .fa-archive,
|
||||
.board-list li:hover a .is-not-star-active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Board drag handle - always visible and positioned at top */
|
||||
.board-list .board-handle {
|
||||
position: absolute;
|
||||
padding: 4px 6px;
|
||||
top: 4px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
background: rgba(0,0,0,0.4);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
transition: background-color 0.2s ease;
|
||||
cursor: grab;
|
||||
opacity: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.board-list .board-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.board-list .board-handle:hover {
|
||||
background: rgba(255, 255, 0, 0.8) !important;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Multiselection checkbox on board items */
|
||||
.board-list .board-list-item .multi-selection-checkbox {
|
||||
position: absolute !important;
|
||||
bottom: 4px !important;
|
||||
left: 4px !important;
|
||||
top: auto !important;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid #fff;
|
||||
background: rgba(0,0,0,0.5);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
z-index: 11;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
transform: none !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.board-list .board-list-item .multi-selection-checkbox:hover {
|
||||
background: rgba(0,0,0,0.7);
|
||||
transform: scale(1.15) !important;
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.board-list .board-list-item .multi-selection-checkbox.is-checked {
|
||||
background: #3cb500;
|
||||
border-color: #3cb500;
|
||||
box-shadow: 0 2px 8px rgba(60, 181, 0, 0.6);
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
top: auto !important;
|
||||
left: 4px !important;
|
||||
transform: none !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.board-list .board-list-item .multi-selection-checkbox.is-checked::after {
|
||||
content: '✓';
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Grey checkboxes when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .board-list .board-list-item .multi-selection-checkbox.is-checked {
|
||||
background: #7a7a7a;
|
||||
border-color: #7a7a7a;
|
||||
box-shadow: 0 2px 8px rgba(122, 122, 122, 0.6);
|
||||
}
|
||||
|
||||
body.grey-icons-enabled .board-list.is-multiselection-active .js-board.is-checked {
|
||||
outline: 4px solid #7a7a7a;
|
||||
box-shadow: 0 4px 12px rgba(122, 122, 122, 0.4);
|
||||
}
|
||||
|
||||
.board-list.is-multiselection-active .js-board.is-checked {
|
||||
outline: 4px solid #3cb500;
|
||||
outline-offset: -4px;
|
||||
box-shadow: 0 4px 12px rgba(60, 181, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Visual hint when multiselection is active */
|
||||
.board-list.is-multiselection-active .board-list-item {
|
||||
border: 2px dashed rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
|
||||
.board-backgrounds-list .board-background-select {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
@@ -271,8 +741,19 @@
|
||||
}
|
||||
.board-backgrounds-list .board-background-select .background-box i.fa-check {
|
||||
font-size: 25px;
|
||||
color: #fff;
|
||||
color: #3cb500;
|
||||
}
|
||||
/* Grey check icons when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .board-backgrounds-list .board-background-select .background-box i.fa-check {
|
||||
color: #7a7a7a;
|
||||
}
|
||||
|
||||
/* Prevent Grey Icons from affecting checkmarks in background color list */
|
||||
body.grey-icons-enabled .checkmark-no-grey {
|
||||
filter: none !important;
|
||||
-webkit-filter: none !important;
|
||||
}
|
||||
|
||||
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
|
||||
.board-list.mobile-view {
|
||||
height: calc(100vh - 120px);
|
||||
@@ -361,6 +842,18 @@
|
||||
min-height: 100vh; /* Force content to be tall enough to scroll */
|
||||
}
|
||||
|
||||
/* Hide archive and clone board buttons in mobile view */
|
||||
.board-list.mobile-view .js-archive-board,
|
||||
.board-list.mobile-view .js-clone-board {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Change board drag handle to up-down arrow in mobile view */
|
||||
.board-list.mobile-view .board-handle.fa-arrows::before {
|
||||
content: "↕️" !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
.board-list.mobile-view::after {
|
||||
content: '';
|
||||
display: block;
|
||||
@@ -371,7 +864,8 @@
|
||||
screen and (max-device-width: 800px),
|
||||
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
|
||||
screen and (max-width: 800px) and (orientation: portrait),
|
||||
screen and (max-width: 800px) and (orientation: landscape) {
|
||||
screen and (max-width: 800px) and (orientation: landscape),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||
.board-list {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
@@ -457,7 +951,8 @@
|
||||
screen and (max-device-width: 800px),
|
||||
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
|
||||
screen and (max-width: 800px) and (orientation: portrait),
|
||||
screen and (max-width: 800px) and (orientation: landscape) {
|
||||
screen and (max-width: 800px) and (orientation: landscape),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||
.wrapper {
|
||||
font-size: 2em !important; /* 2x bigger base font size for All Boards page */
|
||||
}
|
||||
@@ -466,6 +961,22 @@
|
||||
font-size: inherit !important; /* Inherit the 2x scaling */
|
||||
}
|
||||
|
||||
/* Stack the two-column All Boards layout on small screens. The fixed
|
||||
"260px 1fr" grid had no mobile collapse, so on narrow non-iPhone browsers
|
||||
(Fairphone Firefox ESR / Ubuntu Touch Morph, narrow desktop) it overflowed
|
||||
and the left menu (Starred / Templates / Remaining / Workspaces / + Add
|
||||
board) was clipped. Width-based, so it applies to every browser. */
|
||||
.boards-layout {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
|
||||
.boards-left-menu {
|
||||
border-right: none !important;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding-right: 0 !important;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.wrapper .fa, .wrapper .icon {
|
||||
font-size: 2em !important; /* 2x bigger icons */
|
||||
}
|
||||
@@ -725,9 +1236,62 @@
|
||||
#resetBtn {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#resetBtn.filter-reset-btn {
|
||||
background: #f44336;
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
#resetBtn.filter-reset-btn:hover {
|
||||
background: #d32f2f;
|
||||
}
|
||||
|
||||
#resetBtn.filter-reset-btn .reset-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.js-board {
|
||||
display: block;
|
||||
background-color: #999; /* Default gray background if no color class is applied */
|
||||
border-radius: 3px; /* Rounded corners for board items */
|
||||
overflow: hidden; /* Ensure children respect rounded corners */
|
||||
margin: 8px; /* Space between board items */
|
||||
}
|
||||
|
||||
/* Reset background for add-board button */
|
||||
.js-add-board {
|
||||
background-color: transparent !important;
|
||||
margin: 8px !important; /* Keep margin for add-board */
|
||||
}
|
||||
|
||||
/* Apply board colors to li.js-board parent instead of just the link */
|
||||
.board-list .board-color-nephritis { background-color: #27ae60; }
|
||||
.board-list .board-color-pomegranate { background-color: #c0392b; }
|
||||
.board-list .board-color-belize { background-color: #2980b9; }
|
||||
.board-list .board-color-wisteria { background-color: #8e44ad; }
|
||||
.board-list .board-color-midnight { background-color: #2c3e50; }
|
||||
.board-list .board-color-pumpkin { background-color: #e67e22; }
|
||||
.board-list .board-color-moderatepink { background-color: #cd5a91; }
|
||||
.board-list .board-color-strongcyan { background-color: #00aecc; }
|
||||
.board-list .board-color-limegreen { background-color: #4bbf6b; }
|
||||
.board-list .board-color-dark { background-color: #2c3e51; }
|
||||
.board-list .board-color-relax { background-color: #27ae61; }
|
||||
.board-list .board-color-corteza { background-color: #568ba2; }
|
||||
.board-list .board-color-clearblue { background-color: #3498db; }
|
||||
.board-list .board-color-natural { background-color: #596557; }
|
||||
.board-list .board-color-modern { background-color: #2a80b8; }
|
||||
.board-list .board-color-moderndark { background-color: #2a2a2a; }
|
||||
.board-list .board-color-exodark { background-color: #222; }
|
||||
|
||||
.minicard-members {
|
||||
padding: 6px 0 6px 8px;
|
||||
width: 100%;
|
||||
@@ -757,7 +1321,8 @@
|
||||
screen and (max-device-width: 800px),
|
||||
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
|
||||
screen and (max-width: 800px) and (orientation: portrait),
|
||||
screen and (max-width: 800px) and (orientation: landscape) {
|
||||
screen and (max-width: 800px) and (orientation: landscape),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||
.wrapper {
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
@@ -824,5 +1389,17 @@
|
||||
#content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Hide archive and clone board buttons in mobile view */
|
||||
.board-list .js-archive-board,
|
||||
.board-list .js-clone-board {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Change board drag handle to up-down arrow in mobile view */
|
||||
.board-list .board-handle.fa-arrows::before {
|
||||
content: "↕️" !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,155 +2,258 @@ template(name="boardList")
|
||||
.wrapper
|
||||
.board-list-header
|
||||
|
||||
ul.AllBoardTeamsOrgs
|
||||
li.AllBoardTeams
|
||||
if userHasTeams
|
||||
select.js-AllBoardTeams#jsAllBoardTeams("multiple")
|
||||
option(value="-1") {{_ 'teams'}} :
|
||||
each teamsDatas
|
||||
option(value="{{teamId}}") {{_ teamDisplayName}}
|
||||
.boards-layout
|
||||
// Left menu
|
||||
.boards-left-menu
|
||||
ul.menu
|
||||
li(class="menu-item {{#if isSelectedMenu 'starred'}}active{{/if}}")
|
||||
a.js-select-menu(data-type="starred")
|
||||
span.menu-label
|
||||
span.emoji-icon
|
||||
i.fa.fa-star
|
||||
| {{_ 'allboards.starred'}}
|
||||
span.menu-count {{menuItemCount 'starred'}}
|
||||
li(class="menu-item {{#if isSelectedMenu 'templates'}}active{{/if}}")
|
||||
a.js-select-menu(data-type="templates")
|
||||
span.menu-label
|
||||
span.emoji-icon
|
||||
i.fa.fa-clipboard
|
||||
| {{_ 'allboards.templates'}}
|
||||
span.menu-count {{menuItemCount 'templates'}}
|
||||
li(class="menu-item {{#if isSelectedMenu 'remaining'}}active{{/if}}")
|
||||
a.js-select-menu(data-type="remaining")
|
||||
span.menu-label
|
||||
span.emoji-icon
|
||||
i.fa.fa-folder
|
||||
| {{_ 'allboards.remaining'}}
|
||||
span.menu-count {{menuItemCount 'remaining'}}
|
||||
.workspaces-header
|
||||
span
|
||||
span.emoji-icon
|
||||
i.fa.fa-folder-open
|
||||
| {{_ 'allboards.workspaces'}}
|
||||
a.js-add-workspace(title="{{_ 'allboards.add-workspace'}}") +
|
||||
// Workspaces tree
|
||||
+workspaceTree(nodes=workspacesTree selectedWorkspaceId=selectedWorkspaceId)
|
||||
|
||||
li.AllBoardOrgs
|
||||
if userHasOrgs
|
||||
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
|
||||
option(value="-1") {{_ 'organizations'}} :
|
||||
each orgsDatas
|
||||
option(value="{{orgId}}") {{orgDisplayName}}
|
||||
// Existing filter by orgs/teams (kept)
|
||||
ul.AllBoardTeamsOrgs
|
||||
li.AllBoardTeams
|
||||
if userHasTeams
|
||||
select.js-AllBoardTeams#jsAllBoardTeams("multiple")
|
||||
option(value="-1") {{_ 'teams'}} :
|
||||
each teamsDatas
|
||||
option(value="{{teamId}}") {{_ teamDisplayName}}
|
||||
|
||||
//li.AllBoardTemplates
|
||||
// if userHasTemplates
|
||||
// select.js-AllBoardTemplates#jsAllBoardTemplates("multiple")
|
||||
// option(value="-1") {{_ 'templates'}} :
|
||||
// each templatesDatas
|
||||
// option(value="{{templateId}}") {{_ templateDisplayName}}
|
||||
li.AllBoardOrgs
|
||||
if userHasOrgs
|
||||
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
|
||||
option(value="-1") {{_ 'organizations'}} :
|
||||
each orgsDatas
|
||||
option(value="{{orgId}}") {{orgDisplayName}}
|
||||
|
||||
li.AllBoardBtns
|
||||
div.AllBoardButtonsContainer
|
||||
if userHasOrgsOrTeams
|
||||
i.fa.fa-filter
|
||||
input#filterBtn(type="button" value="{{_ 'filter'}}")
|
||||
input#resetBtn(type="button" value="{{_ 'filter-clear'}}")
|
||||
li.AllBoardBtns
|
||||
div.AllBoardButtonsContainer
|
||||
if userHasOrgsOrTeams
|
||||
span.emoji-icon
|
||||
i.fa.fa-search
|
||||
input#filterBtn(type="button" value="{{_ 'filter'}}")
|
||||
button#resetBtn.filter-reset-btn
|
||||
span.reset-icon
|
||||
span.emoji-icon
|
||||
i.fa.fa-times-thin
|
||||
span {{_ 'filter-clear'}}
|
||||
|
||||
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
li.js-add-board
|
||||
a.board-list-item.label(title="{{_ 'add-board'}}")
|
||||
| {{_ 'add-board'}}
|
||||
each boards
|
||||
li(class="{{_id}}" class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
|
||||
if isInvited
|
||||
.board-list-item
|
||||
span.details
|
||||
span.board-list-item-name= title
|
||||
i.fa.js-star-board(
|
||||
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
p.board-list-item-desc {{_ 'just-invited'}}
|
||||
button.js-accept-invite.primary {{_ 'accept'}}
|
||||
button.js-decline-invite {{_ 'decline'}}
|
||||
else
|
||||
if $eq type "template-container"
|
||||
a.js-open-board.template-container.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
span.details
|
||||
span.board-list-item-name(title="{{_ 'template-container'}}")
|
||||
+viewer
|
||||
= title
|
||||
i.fa.js-star-board(
|
||||
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
p.board-list-item-desc
|
||||
+viewer
|
||||
= description
|
||||
if hasSpentTimeCards
|
||||
i.fa.js-has-spenttime-cards(
|
||||
class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
i.fa.board-handle(
|
||||
class="fa-arrows"
|
||||
title="{{_ 'drag-board'}}")
|
||||
else
|
||||
if isSandstorm
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
else if isAdministrable
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
else if currentUser.isAdmin
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
// Right boards grid
|
||||
.boards-right-grid
|
||||
.boards-path-header
|
||||
.path-left
|
||||
span.path-icon.emoji-icon {{currentMenuPath.icon}}
|
||||
span.path-text {{currentMenuPath.text}}
|
||||
if BoardMultiSelection.isActive
|
||||
span.multiselection-hint
|
||||
span.emoji-icon
|
||||
i.fa.fa-thumb-tack
|
||||
| {{_ 'multi-selection-active'}}
|
||||
.path-right
|
||||
if canModifyBoards
|
||||
if hasBoardsSelected
|
||||
button.js-archive-selected-boards.board-header-btn
|
||||
span.emoji-icon
|
||||
i.fa.fa-archive
|
||||
span {{_ 'archive-board'}}
|
||||
button.js-duplicate-selected-boards.board-header-btn
|
||||
span.emoji-icon
|
||||
i.fa.fa-clipboard
|
||||
span {{_ 'duplicate-board'}}
|
||||
a.board-header-btn.js-multiselection-activate(
|
||||
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
|
||||
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-check-square-o
|
||||
if BoardMultiSelection.isActive
|
||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-times
|
||||
ul.board-list.clearfix.js-boards.dragscroll(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
|
||||
li.js-add-board
|
||||
if isSelectedMenu 'templates'
|
||||
a.board-list-item.label(title="{{_ 'add-template-container'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-template-container'}}
|
||||
else
|
||||
a.js-open-board.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
span.details
|
||||
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
|
||||
+viewer
|
||||
= title
|
||||
unless currentSetting.hideBoardMemberList
|
||||
if allowsBoardMemberList
|
||||
.minicard-members
|
||||
each member in boardMembers _id
|
||||
a.name
|
||||
+userAvatar(userId=member noRemove=true)
|
||||
unless currentSetting.hideCardCounterList
|
||||
if allowsCardCounterList
|
||||
.minicard-lists.flex.flex-wrap
|
||||
each list in boardLists _id
|
||||
.item
|
||||
| {{ list }}
|
||||
i.fa.js-star-board(
|
||||
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
p.board-list-item-desc
|
||||
+viewer
|
||||
= description
|
||||
if hasSpentTimeCards
|
||||
i.fa.js-has-spenttime-cards(
|
||||
class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
i.fa.board-handle(
|
||||
class="fa-arrows"
|
||||
title="{{_ 'drag-board'}}")
|
||||
else
|
||||
if isSandstorm
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
else if isAdministrable
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
else if currentUser.isAdmin
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
a.board-list-item.label(title="{{_ 'add-board'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-board'}}
|
||||
each boards
|
||||
li.js-board(class="{{_id}} {{#if isStarred}}starred{{/if}} {{colorClass}} {{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}", draggable="true")
|
||||
if isInvited
|
||||
.board-list-item
|
||||
if BoardMultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
span.details
|
||||
span.board-list-item-name= title
|
||||
span.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
span.emoji-icon
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
p.board-list-item-desc {{_ 'just-invited'}}
|
||||
button.js-accept-invite.primary {{_ 'accept'}}
|
||||
button.js-decline-invite {{_ 'decline'}}
|
||||
else
|
||||
if $eq type "template-container"
|
||||
.template-container.board-list-item
|
||||
if BoardMultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
span.board-handle.nodragscroll(title="{{boardWorkspaceDragHint}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-arrows
|
||||
|
||||
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
span.details
|
||||
span.board-list-item-name(title="{{_ 'template-container'}}")
|
||||
+viewer
|
||||
= title
|
||||
p.board-list-item-desc
|
||||
+viewer
|
||||
= description
|
||||
if hasSpentTimeCards
|
||||
span.js-has-spenttime-cards(
|
||||
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-clock-o
|
||||
span.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
span.emoji-icon
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
else
|
||||
.board-list-item
|
||||
if BoardMultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
span.board-handle.nodragscroll(title="{{boardWorkspaceDragHint}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-arrows
|
||||
|
||||
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
span.details
|
||||
span.board-list-item-name(title="{{boardOpenAndMoveHint}}")
|
||||
+viewer
|
||||
= title
|
||||
unless currentSetting.hideBoardMemberList
|
||||
if allowsBoardMemberList
|
||||
.minicard-members
|
||||
each member in boardMembers _id
|
||||
a.name
|
||||
+userAvatar(userId=member noRemove=true)
|
||||
unless currentSetting.hideCardCounterList
|
||||
if allowsCardCounterList
|
||||
.minicard-lists.flex.flex-wrap
|
||||
each list in boardLists _id
|
||||
.item
|
||||
| {{ list }}
|
||||
p.board-list-item-desc
|
||||
+viewer
|
||||
= description
|
||||
if hasSpentTimeCards
|
||||
span.js-has-spenttime-cards(
|
||||
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-clock-o
|
||||
a.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
span.emoji-icon
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
|
||||
template(name="boardListHeaderBar")
|
||||
h1 {{_ title }}
|
||||
//.board-header-btns.right
|
||||
// a.board-header-btn.js-open-archived-board
|
||||
// i.fa.fa-archive
|
||||
// span {{_ 'archives'}}
|
||||
// a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
|
||||
// i.fa.fa-clone
|
||||
// span {{_ 'templates'}}
|
||||
//
|
||||
a.board-header-btn.js-open-archived-board
|
||||
//
|
||||
i.fa.fa-archive
|
||||
//
|
||||
span {{_ 'archives'}}
|
||||
//
|
||||
a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
|
||||
//
|
||||
i.fa.fa-clone
|
||||
//
|
||||
span {{_ 'templates'}}
|
||||
|
||||
// Recursive template for workspaces tree
|
||||
template(name="workspaceTree")
|
||||
if nodes
|
||||
ul.workspace-tree.js-workspace-tree
|
||||
each nodes
|
||||
li.workspace-node(class="{{#if $eq id selectedWorkspaceId}}active{{/if}}" data-workspace-id="{{id}}" draggable="true")
|
||||
.workspace-node-content
|
||||
span.workspace-drag-handle
|
||||
span.emoji-icon
|
||||
i.fa.fa-arrows
|
||||
|
||||
a.js-select-workspace.js-select-space(data-id="{{id}}")
|
||||
span.workspace-icon
|
||||
if icon
|
||||
span.workspace-icon-value(style="{{#if iconColor}}color: {{iconColor}};{{/if}}")
|
||||
+viewer
|
||||
= icon
|
||||
else
|
||||
span.emoji-icon(style="{{#if iconColor}}color: {{iconColor}};{{/if}}")
|
||||
i.fa.fa-folder
|
||||
span.workspace-name= name
|
||||
a.js-open-workspace-menu(data-id="{{id}}" title="{{_ 'allboards.workspace-menu'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-bars
|
||||
span.workspace-count {{workspaceCount id}}
|
||||
if children
|
||||
+workspaceTree(nodes=children selectedWorkspaceId=selectedWorkspaceId)
|
||||
|
||||
template(name="workspaceActionsPopup")
|
||||
form.workspace-actions-form.js-workspace-actions-form(data-id="{{id}}")
|
||||
label
|
||||
| {{_ 'allboards.edit-workspace-name'}}
|
||||
input.js-workspace-name(type="text" value="{{workspaceName}}" required)
|
||||
label
|
||||
| {{_ 'allboards.edit-workspace-icon'}}
|
||||
input.js-workspace-icon(type="text" value="{{workspaceIcon}}")
|
||||
input.primary.wide(type="submit" value="{{_ 'save'}}")
|
||||
hr
|
||||
form.workspace-subspace-form.js-workspace-subspace-form(data-id="{{id}}")
|
||||
label
|
||||
| {{_ 'allboards.add-subworkspace-prompt'}}
|
||||
input.js-subworkspace-name(type="text")
|
||||
button.primary.wide(type="submit") {{_ 'allboards.add-subworkspace'}}
|
||||
hr
|
||||
.workspace-delete-block
|
||||
span.js-workspace-delete-error(style="color: #c00; display: block; margin: 4px 0;")
|
||||
button.js-delete-workspace.negate.wide(type="button" data-id="{{id}}") {{_ 'delete'}}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,43 @@
|
||||
.calendar-view .fc {
|
||||
--fc-button-text-color: #333;
|
||||
--fc-button-bg-color: #f5f5f5;
|
||||
--fc-button-border-color: rgba(0, 0, 0, 0.2);
|
||||
--fc-button-hover-bg-color: #e6e6e6;
|
||||
--fc-button-hover-border-color: rgba(0, 0, 0, 0.25);
|
||||
--fc-button-active-bg-color: #d9d9d9;
|
||||
--fc-button-active-border-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.calendar-view .fc .fc-button-primary {
|
||||
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||
0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
background-image: linear-gradient(to bottom, #fff 0%, #e6e6e6 100%);
|
||||
}
|
||||
|
||||
.calendar-view .fc .fc-button-primary:focus,
|
||||
.calendar-view .fc .fc-button-primary:not(:disabled).fc-button-active,
|
||||
.calendar-view .fc .fc-button-primary:not(:disabled):active {
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),
|
||||
0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.calendar-create-close {
|
||||
min-height: auto !important;
|
||||
min-width: auto !important;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
color: #666 !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.calendar-create-close:hover,
|
||||
.calendar-create-close:focus {
|
||||
background: transparent !important;
|
||||
color: #111 !important;
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ template(name="miniboard")
|
||||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
.fa.fa-arrows
|
||||
span.drag-handle(title="{{_ 'dragBoard'}}")
|
||||
i.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
||||
@@ -0,0 +1,263 @@
|
||||
/* Original Positions View Styles */
|
||||
.original-positions-view {
|
||||
margin: 10px 0;
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.original-positions-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.original-positions-header .btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.original-positions-content {
|
||||
background-color: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.original-positions-loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.original-positions-loading i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.original-positions-filters {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.original-positions-filters .btn-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.original-positions-filters .btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.original-positions-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.original-position-item {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
padding: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.original-position-item:hover {
|
||||
background-color: #e9ecef;
|
||||
border-color: #ced4da;
|
||||
}
|
||||
|
||||
.original-position-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.original-position-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.original-position-item-header i {
|
||||
color: #6c757d;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.entity-type {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.entity-name {
|
||||
color: #212529;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.entity-id {
|
||||
color: #6c757d;
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.original-position-item-details {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.original-position-description {
|
||||
color: #495057;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.original-title {
|
||||
color: #6c757d;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
padding: 4px 6px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.original-title strong {
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.original-position-date {
|
||||
color: #6c757d;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.no-original-positions {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.no-original-positions i {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.original-positions-view {
|
||||
margin: 5px 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.original-positions-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.original-positions-header .btn {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.original-positions-filters .btn-group {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.original-position-item-header {
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.entity-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.original-position-item-details {
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.original-positions-view {
|
||||
background-color: #2d3748;
|
||||
border-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.original-positions-content {
|
||||
background-color: #1a202c;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
.original-position-item {
|
||||
background-color: #2d3748;
|
||||
border-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.original-position-item:hover {
|
||||
background-color: #4a5568;
|
||||
border-color: #718096;
|
||||
}
|
||||
|
||||
.original-position-item-header {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.original-position-item-header i {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.entity-name {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.entity-id {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.original-position-description {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.original-title {
|
||||
background-color: #4a5568;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.original-title strong {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.original-position-date {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.no-original-positions {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.no-original-positions i {
|
||||
color: #718096;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
template(name="originalPositionsView")
|
||||
.original-positions-view
|
||||
.original-positions-header
|
||||
button.btn.btn-sm.btn-outline-secondary.js-toggle-original-positions
|
||||
i.fa.fa-history
|
||||
if isShowingOriginalPositions
|
||||
| Hide Original Positions
|
||||
else
|
||||
| Show Original Positions
|
||||
|
||||
if isShowingOriginalPositions
|
||||
button.btn.btn-sm.btn-outline-primary.js-refresh-history
|
||||
i.fa.fa-refresh
|
||||
| Refresh
|
||||
|
||||
if isShowingOriginalPositions
|
||||
.original-positions-content
|
||||
if isLoading
|
||||
.original-positions-loading
|
||||
i.fa.fa-spinner.fa-spin
|
||||
| Loading original positions...
|
||||
else
|
||||
.original-positions-filters
|
||||
.btn-group.btn-group-sm(role="group")
|
||||
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'all'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="all")
|
||||
| All
|
||||
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'swimlane'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="swimlane")
|
||||
i.fa.fa-bars
|
||||
| Swimlanes
|
||||
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'list'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="list")
|
||||
i.fa.fa-columns
|
||||
| Lists
|
||||
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'card'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="card")
|
||||
i.fa.fa-sticky-note
|
||||
| Cards
|
||||
|
||||
.original-positions-list
|
||||
each getFilteredHistory
|
||||
.original-position-item
|
||||
.original-position-item-header
|
||||
i.fa(class="{{getEntityTypeIcon entityType}}")
|
||||
span.entity-type {{getEntityTypeLabel entityType}}
|
||||
span.entity-name {{getEntityDisplayName this}}
|
||||
span.entity-id ({{entityId}})
|
||||
.original-position-item-details
|
||||
.original-position-description
|
||||
| {{getEntityOriginalPositionDescription this}}
|
||||
if originalTitle
|
||||
.original-title
|
||||
strong Original title:
|
||||
| {{originalTitle}}
|
||||
.original-position-date
|
||||
small.text-muted Created: {{formatDate createdAt}}
|
||||
else
|
||||
.no-original-positions
|
||||
i.fa.fa-info-circle
|
||||
| No original position data available for this board.
|
||||
@@ -0,0 +1,134 @@
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
/**
|
||||
* Component to display original positions for all entities on a board
|
||||
*/
|
||||
|
||||
Template.originalPositionsView.onCreated(function () {
|
||||
this.showOriginalPositions = new ReactiveVar(false);
|
||||
this.boardHistory = new ReactiveVar([]);
|
||||
this.isLoading = new ReactiveVar(false);
|
||||
this.filterType = new ReactiveVar('all'); // 'all', 'swimlane', 'list', 'card'
|
||||
|
||||
const tpl = this;
|
||||
|
||||
this.loadBoardHistory = function () {
|
||||
const boardId = Session.get('currentBoard');
|
||||
if (!boardId) return;
|
||||
|
||||
tpl.isLoading.set(true);
|
||||
|
||||
Meteor.call('positionHistory.getBoardHistory', boardId, (error, result) => {
|
||||
tpl.isLoading.set(false);
|
||||
if (error) {
|
||||
console.error('Error loading board history:', error);
|
||||
tpl.boardHistory.set([]);
|
||||
} else {
|
||||
tpl.boardHistory.set(result);
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
Template.originalPositionsView.onRendered(function () {
|
||||
this.loadBoardHistory();
|
||||
});
|
||||
|
||||
Template.originalPositionsView.helpers({
|
||||
isShowingOriginalPositions() {
|
||||
return Template.instance().showOriginalPositions.get();
|
||||
},
|
||||
|
||||
isLoading() {
|
||||
return Template.instance().isLoading.get();
|
||||
},
|
||||
|
||||
getBoardHistory() {
|
||||
return Template.instance().boardHistory.get();
|
||||
},
|
||||
|
||||
getFilteredHistory() {
|
||||
const tpl = Template.instance();
|
||||
const history = tpl.boardHistory.get();
|
||||
const filterType = tpl.filterType.get();
|
||||
|
||||
if (filterType === 'all') {
|
||||
return history;
|
||||
}
|
||||
|
||||
return history.filter(item => item.entityType === filterType);
|
||||
},
|
||||
|
||||
isFilterType(type) {
|
||||
return Template.instance().filterType.get() === type;
|
||||
},
|
||||
|
||||
getEntityDisplayName(entity) {
|
||||
const position = entity.originalPosition || {};
|
||||
return position.title || `Entity ${entity.entityId}`;
|
||||
},
|
||||
|
||||
getEntityOriginalPositionDescription(entity) {
|
||||
const position = entity.originalPosition || {};
|
||||
let description = `Position: ${position.sort || 0}`;
|
||||
|
||||
if (entity.entityType === 'list' && entity.originalSwimlaneId) {
|
||||
description += ` in swimlane ${entity.originalSwimlaneId}`;
|
||||
} else if (entity.entityType === 'card') {
|
||||
if (entity.originalSwimlaneId) {
|
||||
description += ` in swimlane ${entity.originalSwimlaneId}`;
|
||||
}
|
||||
if (entity.originalListId) {
|
||||
description += ` in list ${entity.originalListId}`;
|
||||
}
|
||||
}
|
||||
|
||||
return description;
|
||||
},
|
||||
|
||||
getEntityTypeIcon(entityType) {
|
||||
switch (entityType) {
|
||||
case 'swimlane':
|
||||
return 'fa-bars';
|
||||
case 'list':
|
||||
return 'fa-columns';
|
||||
case 'card':
|
||||
return 'fa-sticky-note';
|
||||
default:
|
||||
return 'fa-question';
|
||||
}
|
||||
},
|
||||
|
||||
getEntityTypeLabel(entityType) {
|
||||
switch (entityType) {
|
||||
case 'swimlane':
|
||||
return 'Swimlane';
|
||||
case 'list':
|
||||
return 'List';
|
||||
case 'card':
|
||||
return 'Card';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
return new Date(date).toLocaleString();
|
||||
},
|
||||
});
|
||||
|
||||
Template.originalPositionsView.events({
|
||||
'click .js-toggle-original-positions'(evt, tpl) {
|
||||
tpl.showOriginalPositions.set(!tpl.showOriginalPositions.get());
|
||||
},
|
||||
|
||||
'click .js-refresh-history'(evt, tpl) {
|
||||
tpl.loadBoardHistory();
|
||||
},
|
||||
|
||||
'click .js-filter-type'(evt, tpl) {
|
||||
const type = evt.currentTarget.dataset.filterType;
|
||||
tpl.filterType.set(type);
|
||||
},
|
||||
});
|
||||
@@ -55,6 +55,12 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.attachment-actions a {
|
||||
margin-left: 16px;
|
||||
}
|
||||
.attachment-actions a:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
.add-attachment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -106,6 +112,9 @@
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 4em;
|
||||
position: absolute;
|
||||
right: 50px;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
/* Upload progress indicators for drag-and-drop uploads */
|
||||
@@ -241,10 +250,6 @@
|
||||
.js-card-details.is-dragging-over {
|
||||
border: 2px dashed #007bff !important;
|
||||
background: rgba(0, 123, 255, 0.05) !important;
|
||||
}
|
||||
top: 0;
|
||||
right: 8px;
|
||||
position: absolute;
|
||||
}
|
||||
.attachment-arrow {
|
||||
font-size: 4em;
|
||||
@@ -253,6 +258,20 @@
|
||||
align-self: center;
|
||||
margin: 0 20px;
|
||||
}
|
||||
#prev-attachment {
|
||||
font-size: 4em;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
margin-left: 70px;
|
||||
}
|
||||
#next-attachment {
|
||||
font-size: 4em;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
margin-right: 70px;
|
||||
}
|
||||
#viewer-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -266,6 +285,13 @@
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
#video-viewer {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
#audio-viewer {
|
||||
max-width: 100%;
|
||||
}
|
||||
#pdf-viewer {
|
||||
width: 40vw;
|
||||
height: 100%;
|
||||
@@ -300,9 +326,19 @@
|
||||
}
|
||||
#prev-attachment {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
bottom: 2.2em;
|
||||
font-size: 1.6em;
|
||||
padding: 16px;
|
||||
margin-left: 0;
|
||||
}
|
||||
#next-attachment {
|
||||
right: 0;
|
||||
position: absolute;
|
||||
bottom: 2.2em;
|
||||
font-size: 1.6em;
|
||||
padding: 16px;
|
||||
margin-right: 0;
|
||||
}
|
||||
#pdf-viewer {
|
||||
width: 100%;
|
||||
|
||||
@@ -34,10 +34,11 @@ template(name="attachmentViewer")
|
||||
#viewer-overlay.hidden
|
||||
#viewer-top-bar
|
||||
span#attachment-name
|
||||
a#viewer-close.fa.fa-times-thin
|
||||
a#viewer-close
|
||||
i.fa.fa-times-thin
|
||||
|
||||
#viewer-container
|
||||
i.fa.fa-chevron-left.attachment-arrow#prev-attachment
|
||||
i.fa.fa-caret-left#prev-attachment
|
||||
#viewer-content
|
||||
img#image-viewer.hidden
|
||||
video#video-viewer.hidden(controls="true")
|
||||
@@ -45,7 +46,7 @@ template(name="attachmentViewer")
|
||||
object#pdf-viewer.hidden(type="application/pdf")
|
||||
span.pdf-preview-error {{_ 'preview-pdf-not-supported' }}
|
||||
object#txt-viewer.hidden(type="text/plain")
|
||||
i.fa.fa-chevron-right.attachment-arrow#next-attachment
|
||||
i.fa.fa-caret-right#next-attachment
|
||||
|
||||
template(name="attachmentGallery")
|
||||
|
||||
@@ -53,11 +54,11 @@ template(name="attachmentGallery")
|
||||
|
||||
if canModifyCard
|
||||
a.attachment-item.add-attachment.js-add-attachment
|
||||
i.fa.fa-plus.icon
|
||||
i.fa.fa-plus
|
||||
|
||||
each attachments
|
||||
|
||||
.attachment-item
|
||||
.attachment-item(class="{{#if isAttachmentMigrating _id}}migrating{{/if}}")
|
||||
.attachment-thumbnail-container.open-preview(data-attachment-id="{{_id}}" data-card-id="{{ meta.cardId }}")
|
||||
if link
|
||||
if(isImage)
|
||||
@@ -85,30 +86,36 @@ template(name="attachmentGallery")
|
||||
b
|
||||
= name
|
||||
span.file-size ({{fileSize size}})
|
||||
if uploaderName
|
||||
div.attachment-uploader {{uploaderName}}
|
||||
if uploadedAt
|
||||
div.attachment-uploaded-at {{uploadedAt}}
|
||||
.attachment-actions
|
||||
a.js-download(href="{{link}}?download=true", download="{{name}}")
|
||||
i.fa.fa-download.icon(title="{{_ 'download'}}")
|
||||
a.js-download(href="{{link}}?download=true", download="{{name}}", title="{{_ 'download'}}")
|
||||
i.fa.fa-arrow-down
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isWorker
|
||||
a.js-rename
|
||||
i.fa.fa-pencil-square-o.icon(title="{{_ 'rename'}}")
|
||||
a.js-confirm-delete
|
||||
i.fa.fa-trash.icon(title="{{_ 'delete'}}")
|
||||
a.fa.fa-navicon.icon.js-open-attachment-menu(data-attachment-link="{{link}}" title="{{_ 'attachmentActionsPopup-title'}}")
|
||||
|
||||
a.js-rename(title="{{_ 'rename'}}")
|
||||
i.fa.fa-pencil-square-o
|
||||
a.js-confirm-delete(title="{{_ 'delete'}}")
|
||||
i.fa.fa-trash
|
||||
a.js-open-attachment-menu(data-attachment-link="{{link}}", title="{{_ 'attachmentActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
// Migration spinner overlay
|
||||
if isAttachmentMigrating _id
|
||||
.attachment-migration-overlay
|
||||
.migration-spinner
|
||||
i.fa.fa-cog.fa-spin
|
||||
.migration-text {{_ 'migrating-attachment'}}
|
||||
|
||||
template(name="attachmentActionsPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
if isImage
|
||||
a(class="{{#if isCover}}js-remove-cover{{else}}js-add-cover{{/if}}")
|
||||
i.fa.fa-book
|
||||
i.fa.fa-picture-o
|
||||
if isCover
|
||||
| {{_ 'remove-cover'}}
|
||||
else
|
||||
| {{_ 'add-cover'}}
|
||||
| {{#if isCover}}{{_ 'remove-cover'}}{{else}}{{_ 'add-cover'}}{{/if}}
|
||||
if currentUser.isBoardAdmin
|
||||
if isImage
|
||||
a(class="{{#if isBackgroundImage}}js-remove-background-image{{else}}js-add-background-image{{/if}}")
|
||||
@@ -118,23 +125,6 @@ template(name="attachmentActionsPopup")
|
||||
else
|
||||
| {{_ 'add-background-image'}}
|
||||
|
||||
if $neq versions.original.storage "fs"
|
||||
a.js-move-storage-fs
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'attachment-move-storage-fs'}}
|
||||
|
||||
if $neq versions.original.storage "gridfs"
|
||||
if versions.original.storage
|
||||
a.js-move-storage-gridfs
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'attachment-move-storage-gridfs'}}
|
||||
|
||||
if $neq versions.original.storage "s3"
|
||||
if versions.original.storage
|
||||
a.js-move-storage-s3
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'attachment-move-storage-s3'}}
|
||||
|
||||
template(name="attachmentRenamePopup")
|
||||
input.js-edit-attachment-name(type='text' autofocus value="{{getNameWithoutExtension}}" dir="auto")
|
||||
.edit-controls.clearfix
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { ObjectID } from 'bson';
|
||||
import { ObjectId } from 'bson';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
|
||||
import uploadProgressManager from '../../lib/uploadProgressManager';
|
||||
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
|
||||
import Attachments from '/models/attachments';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import { formatDateTime } from '/imports/lib/dateUtils';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
|
||||
const filesize = require('filesize');
|
||||
const prettyMilliseconds = require('pretty-ms');
|
||||
const { filesize } = require('filesize');
|
||||
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
|
||||
@@ -18,7 +23,7 @@ let touchStartCoords = null;
|
||||
let touchEndCoords = null;
|
||||
|
||||
// Stores link to the attachment for which attachment actions popup was opened
|
||||
attachmentActionsLink = null;
|
||||
let attachmentActionsLink = null;
|
||||
|
||||
Template.attachmentGallery.events({
|
||||
'click .open-preview'(event) {
|
||||
@@ -39,8 +44,12 @@ Template.attachmentGallery.events({
|
||||
attachmentActionsLink = event.currentTarget.getAttribute("data-attachment-link");
|
||||
},
|
||||
'click .js-rename': Popup.open('attachmentRename'),
|
||||
'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete', function() {
|
||||
Attachments.remove(this._id);
|
||||
'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete', async function() {
|
||||
const card = this.meta && this.meta.cardId ? ReactiveCache.getCard(this.meta.cardId) : null;
|
||||
if (card && card.coverId === this._id) {
|
||||
await card.unsetCover();
|
||||
}
|
||||
await Attachments.removeAsync(this._id);
|
||||
Popup.back();
|
||||
}),
|
||||
});
|
||||
@@ -80,6 +89,10 @@ function attachmentCanBeOpened(attachment) {
|
||||
);
|
||||
}
|
||||
|
||||
function getAttachmentUrl(attachment) {
|
||||
return attachment && typeof attachment.link === 'function' ? attachment.link() : '';
|
||||
}
|
||||
|
||||
function openAttachmentViewer(attachmentId) {
|
||||
const attachment = ReactiveCache.getAttachment(attachmentId);
|
||||
|
||||
@@ -96,18 +109,18 @@ function openAttachmentViewer(attachmentId) {
|
||||
*/
|
||||
switch(true){
|
||||
case (attachment.isImage):
|
||||
$("#image-viewer").attr("src", attachment.link());
|
||||
$("#image-viewer").attr("src", getAttachmentUrl(attachment));
|
||||
$("#image-viewer").removeClass("hidden");
|
||||
break;
|
||||
case (attachment.isPDF):
|
||||
$("#pdf-viewer").attr("data", attachment.link());
|
||||
$("#pdf-viewer").attr("data", getAttachmentUrl(attachment));
|
||||
$("#pdf-viewer").removeClass("hidden");
|
||||
break;
|
||||
case (attachment.isVideo):
|
||||
// We have to create a new <source> DOM element and append it to the video
|
||||
// element, otherwise the video won't load
|
||||
let videoSource = document.createElement('source');
|
||||
videoSource.setAttribute('src', attachment.link());
|
||||
videoSource.setAttribute('src', getAttachmentUrl(attachment));
|
||||
$("#video-viewer").append(videoSource);
|
||||
|
||||
$("#video-viewer").removeClass("hidden");
|
||||
@@ -116,14 +129,14 @@ function openAttachmentViewer(attachmentId) {
|
||||
// We have to create a new <source> DOM element and append it to the audio
|
||||
// element, otherwise the audio won't load
|
||||
let audioSource = document.createElement('source');
|
||||
audioSource.setAttribute('src', attachment.link());
|
||||
audioSource.setAttribute('src', getAttachmentUrl(attachment));
|
||||
$("#audio-viewer").append(audioSource);
|
||||
|
||||
$("#audio-viewer").removeClass("hidden");
|
||||
break;
|
||||
case (attachment.isText):
|
||||
case (attachment.isJSON):
|
||||
$("#txt-viewer").attr("data", attachment.link());
|
||||
$("#txt-viewer").attr("data", getAttachmentUrl(attachment));
|
||||
$("#txt-viewer").removeClass("hidden");
|
||||
break;
|
||||
}
|
||||
@@ -195,8 +208,6 @@ function processTouch(){
|
||||
xDist = touchEndCoords.x - touchStartCoords.x;
|
||||
yDist = touchEndCoords.y - touchStartCoords.y;
|
||||
|
||||
console.log("xDist: " + xDist);
|
||||
|
||||
// Left swipe
|
||||
if (Math.abs(xDist) > Math.abs(yDist) && xDist < 0) {
|
||||
openNextAttachment();
|
||||
@@ -216,14 +227,12 @@ function processTouch(){
|
||||
|
||||
Template.attachmentViewer.events({
|
||||
'touchstart #viewer-container'(event) {
|
||||
console.log("touchstart")
|
||||
touchStartCoords = {
|
||||
x: event.changedTouches[0].screenX,
|
||||
y: event.changedTouches[0].screenY
|
||||
}
|
||||
},
|
||||
'touchend #viewer-container'(event) {
|
||||
console.log("touchend")
|
||||
touchEndCoords = {
|
||||
x: event.changedTouches[0].screenX,
|
||||
y: event.changedTouches[0].screenY
|
||||
@@ -262,6 +271,24 @@ Template.attachmentViewer.events({
|
||||
});
|
||||
|
||||
Template.attachmentGallery.helpers({
|
||||
attachments() {
|
||||
const card = Template.currentData();
|
||||
if (!card) return [];
|
||||
const cardId = typeof card.getRealId === 'function' ? card.getRealId() : card._id;
|
||||
if (!cardId) return [];
|
||||
const filesCursor = Attachments.find(
|
||||
{ 'meta.cardId': cardId },
|
||||
{ sort: { uploadedAt: -1 } },
|
||||
);
|
||||
// Call fetch() on the underlying Mongo cursor to establish a reactive
|
||||
// dependency directly inside this Blaze computation. Without this,
|
||||
// .each() (which calls cursor.map()) would not register reactivity and
|
||||
// newly-uploaded attachments would not appear until a page reload.
|
||||
if (filesCursor && filesCursor.cursor) {
|
||||
filesCursor.cursor.fetch();
|
||||
}
|
||||
return filesCursor.each();
|
||||
},
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
@@ -272,6 +299,26 @@ Template.attachmentGallery.helpers({
|
||||
sanitize(value) {
|
||||
return sanitizeHTML(value);
|
||||
},
|
||||
uploaderName() {
|
||||
const uploaderId = this.userId;
|
||||
if (!uploaderId) return '';
|
||||
const uploader = ReactiveCache.getUser(uploaderId);
|
||||
if (!uploader) return '';
|
||||
return uploader.profile && uploader.profile.fullname
|
||||
? uploader.profile.fullname
|
||||
: uploader.username || '';
|
||||
},
|
||||
uploadedAt() {
|
||||
if (this.uploadedAtOstrio) {
|
||||
return formatDateTime(this.uploadedAtOstrio);
|
||||
}
|
||||
// Fall back to ObjectId timestamp (first 4 bytes = Unix seconds)
|
||||
try {
|
||||
const ts = parseInt(this._id.substring(0, 8), 16) * 1000;
|
||||
if (!isNaN(ts)) return formatDateTime(new Date(ts));
|
||||
} catch (_) {}
|
||||
return '';
|
||||
},
|
||||
});
|
||||
|
||||
Template.cardAttachmentsPopup.onCreated(function() {
|
||||
@@ -293,12 +340,13 @@ Template.cardAttachmentsPopup.helpers({
|
||||
});
|
||||
|
||||
Template.cardAttachmentsPopup.events({
|
||||
'change .js-attach-file'(event, templateInstance) {
|
||||
async 'change .js-attach-file'(event, templateInstance) {
|
||||
event.stopPropagation();
|
||||
const card = this;
|
||||
const files = event.currentTarget.files;
|
||||
if (files) {
|
||||
let uploads = [];
|
||||
const uploaders = handleFileUpload(card, files);
|
||||
const uploaders = await handleFileUpload(card, files);
|
||||
|
||||
uploaders.forEach(uploader => {
|
||||
uploader.on('start', function() {
|
||||
@@ -316,6 +364,10 @@ Template.cardAttachmentsPopup.events({
|
||||
}
|
||||
},
|
||||
'click .js-computer-upload'(event, templateInstance) {
|
||||
// Prevent the click fired when the OS file-picker dialog closes from
|
||||
// triggering EscapeActions and closing the popup before the upload starts.
|
||||
// Same pattern as swimlanes.js after drag operations.
|
||||
EscapeActions.preventNextClick();
|
||||
templateInstance.find('.js-attach-file').click();
|
||||
event.preventDefault();
|
||||
},
|
||||
@@ -327,7 +379,7 @@ const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
|
||||
let pastedResults = null;
|
||||
|
||||
// Shared upload logic for drag-and-drop functionality
|
||||
export function handleFileUpload(card, files) {
|
||||
export async function handleFileUpload(card, files) {
|
||||
if (!files || files.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -342,7 +394,7 @@ export function handleFileUpload(card, files) {
|
||||
}
|
||||
|
||||
// Check if user can modify the card
|
||||
if (!card.canModifyCard()) {
|
||||
if (!Utils.canModifyCard()) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('User does not have permission to modify this card');
|
||||
}
|
||||
@@ -360,7 +412,7 @@ export function handleFileUpload(card, files) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileId = new ObjectID().toString();
|
||||
const fileId = new ObjectId().toString();
|
||||
let fileName = sanitizeText(file.name);
|
||||
|
||||
// If sanitized filename is not same as original filename,
|
||||
@@ -380,11 +432,16 @@ export function handleFileUpload(card, files) {
|
||||
fileName: fileName,
|
||||
meta: Utils.getCommonAttachmentMetaFrom(card),
|
||||
chunkSize: 'dynamic',
|
||||
// Use HTTP transport instead of DDP so file chunks go over a dedicated
|
||||
// fetch POST rather than flooding the WebSocket/DDP channel. DDP
|
||||
// upload causes repeated DDP reconnects in Safari, showing the
|
||||
// "Loading, please wait" offline banner and stalling progress at ~95%.
|
||||
transport: 'http',
|
||||
};
|
||||
config.meta.fileId = fileId;
|
||||
|
||||
try {
|
||||
const uploader = Attachments.insert(
|
||||
const uploader = await Attachments.insertAsync(
|
||||
config,
|
||||
false,
|
||||
);
|
||||
@@ -459,21 +516,22 @@ Template.previewClipboardImagePopup.onRendered(() => {
|
||||
});
|
||||
|
||||
Template.previewClipboardImagePopup.events({
|
||||
'click .js-upload-pasted-image'() {
|
||||
async 'click .js-upload-pasted-image'() {
|
||||
const card = this;
|
||||
if (pastedResults && pastedResults.file) {
|
||||
const file = pastedResults.file;
|
||||
window.oPasted = pastedResults;
|
||||
const fileId = new ObjectID().toString();
|
||||
const fileId = new ObjectId().toString();
|
||||
const config = {
|
||||
file,
|
||||
fileId: fileId,
|
||||
meta: Utils.getCommonAttachmentMetaFrom(card),
|
||||
fileName: file.name || file.type.replace('image/', 'clipboard.'),
|
||||
chunkSize: 'dynamic',
|
||||
transport: 'http',
|
||||
};
|
||||
config.meta.fileId = fileId;
|
||||
const uploader = Attachments.insert(
|
||||
const uploader = await Attachments.insertAsync(
|
||||
config,
|
||||
false,
|
||||
);
|
||||
@@ -494,9 +552,9 @@ Template.previewClipboardImagePopup.events({
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.attachmentActionsPopup.helpers({
|
||||
isCover() {
|
||||
const ret = ReactiveCache.getCard(this.data().meta.cardId).coverId == this.data()._id;
|
||||
const ret = ReactiveCache.getCard(this.meta.cardId).coverId == this._id;
|
||||
return ret;
|
||||
},
|
||||
isBackgroundImage() {
|
||||
@@ -504,75 +562,74 @@ BlazeComponent.extendComponent({
|
||||
//return currentBoard.backgroundImageURL === $(".attachment-thumbnail-img").attr("src");
|
||||
return false;
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-add-cover'() {
|
||||
ReactiveCache.getCard(this.data().meta.cardId).setCover(this.data()._id);
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-remove-cover'() {
|
||||
ReactiveCache.getCard(this.data().meta.cardId).unsetCover();
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-add-background-image'() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
currentBoard.setBackgroundImageURL(attachmentActionsLink);
|
||||
Utils.setBackgroundImage(attachmentActionsLink);
|
||||
Popup.back();
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-remove-background-image'() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
currentBoard.setBackgroundImageURL("");
|
||||
Utils.setBackgroundImage("");
|
||||
Popup.back();
|
||||
Utils.reload();
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-move-storage-fs'() {
|
||||
Meteor.call('moveAttachmentToStorage', this.data()._id, "fs");
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-move-storage-gridfs'() {
|
||||
Meteor.call('moveAttachmentToStorage', this.data()._id, "gridfs");
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-move-storage-s3'() {
|
||||
Meteor.call('moveAttachmentToStorage', this.data()._id, "s3");
|
||||
Popup.back();
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}).register('attachmentActionsPopup');
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.attachmentActionsPopup.events({
|
||||
'click .js-add-cover'() {
|
||||
ReactiveCache.getCard(this.meta.cardId).setCover(this._id);
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-remove-cover'() {
|
||||
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();
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-remove-background-image'(event) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
currentBoard.setBackgroundImageURL("");
|
||||
Utils.setBackgroundImage("");
|
||||
Popup.back();
|
||||
Utils.reload();
|
||||
event.preventDefault();
|
||||
},
|
||||
});
|
||||
|
||||
Template.attachmentRenamePopup.helpers({
|
||||
getNameWithoutExtension() {
|
||||
const ret = this.data().name.replace(new RegExp("\." + this.data().extension + "$"), "");
|
||||
const ret = this.name.replace(new RegExp("\\." + this.extension + "$"), "");
|
||||
return ret;
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'keydown input.js-edit-attachment-name'(evt) {
|
||||
// enter = save
|
||||
if (evt.keyCode === 13) {
|
||||
this.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
'click button.js-submit-edit-attachment-name'(event) {
|
||||
// save button pressed
|
||||
event.preventDefault();
|
||||
const name = this.$('.js-edit-attachment-name')[0]
|
||||
.value
|
||||
.trim() + this.data().extensionWithDot;
|
||||
if (name === sanitizeText(name)) {
|
||||
Meteor.call('renameAttachment', this.data()._id, name);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}).register('attachmentRenamePopup');
|
||||
});
|
||||
|
||||
Template.attachmentRenamePopup.events({
|
||||
'keydown input.js-edit-attachment-name'(evt, tpl) {
|
||||
// enter = save
|
||||
if (evt.keyCode === 13) {
|
||||
tpl.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
'click button.js-submit-edit-attachment-name'(event, tpl) {
|
||||
// save button pressed
|
||||
event.preventDefault();
|
||||
const name = tpl.$('.js-edit-attachment-name')[0]
|
||||
.value
|
||||
.trim() + this.extensionWithDot;
|
||||
if (name === sanitizeText(name)) {
|
||||
Meteor.call('renameAttachment', this._id, name);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
// Template helpers for attachment migration status
|
||||
Template.registerHelper('attachmentMigrationStatus', function(attachmentId) {
|
||||
return attachmentMigrationManager.getAttachmentMigrationStatus(attachmentId);
|
||||
});
|
||||
|
||||
Template.registerHelper('isAttachmentMigrating', function(attachmentId) {
|
||||
return attachmentMigrationManager.isAttachmentBeingMigrated(attachmentId);
|
||||
});
|
||||
|
||||
Template.registerHelper('attachmentMigrationProgress', function() {
|
||||
return attachmentMigrationManager.attachmentMigrationProgress.get();
|
||||
});
|
||||
|
||||
Template.registerHelper('attachmentMigrationStatusText', function() {
|
||||
return attachmentMigrationManager.attachmentMigrationStatus.get();
|
||||
});
|
||||
@@ -22,7 +22,7 @@ template(name="cardCustomField-text")
|
||||
= value
|
||||
.edit-controls.clearfix
|
||||
button.primary(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
if value
|
||||
@@ -41,7 +41,7 @@ template(name="cardCustomField-number")
|
||||
input(type="number" value=data.value)
|
||||
.edit-controls.clearfix
|
||||
button.primary(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
if value
|
||||
@@ -56,9 +56,9 @@ template(name="cardCustomField-checkbox")
|
||||
.js-checklist-item.checklist-item(class="{{#if data.value }}is-checked{{/if}}")
|
||||
if canModifyCard
|
||||
.check-box-container
|
||||
.check-box.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
|
||||
.check-box.materialCheckBox(class="{{#if data.value}}is-checked{{/if}}")
|
||||
else
|
||||
.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if data.value}}is-checked{{/if}}")
|
||||
|
||||
template(name="cardCustomField-currency")
|
||||
if canModifyCard
|
||||
@@ -66,7 +66,7 @@ template(name="cardCustomField-currency")
|
||||
input(type="text" value=data.value autofocus)
|
||||
.edit-controls.clearfix
|
||||
button.primary(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
if value
|
||||
@@ -98,6 +98,21 @@ template(name="cardCustomField-date")
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardCustomField-datePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="cardCustomField-dropdown")
|
||||
if canModifyCard
|
||||
+inlinedForm(classNames="js-card-customfield-dropdown")
|
||||
@@ -113,7 +128,7 @@ template(name="cardCustomField-dropdown")
|
||||
= name
|
||||
.edit-controls.clearfix
|
||||
button.primary(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
if value
|
||||
@@ -134,7 +149,7 @@ template(name="cardCustomField-stringtemplate")
|
||||
input.js-card-customfield-stringtemplate-item.last(type="text" value="" placeholder="{{_ 'custom-field-stringtemplate-item-placeholder'}}" autofocus)
|
||||
.edit-controls.clearfix
|
||||
button.primary(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
if value
|
||||
|
||||
@@ -1,12 +1,41 @@
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { DatePicker } from '/client/lib/datepicker';
|
||||
import Cards from '/models/cards';
|
||||
import {
|
||||
setupDatePicker,
|
||||
datePickerRendered,
|
||||
datePickerHelpers,
|
||||
datePickerEvents,
|
||||
} from '/client/lib/datepicker';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatDateByUserPreference,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
import { CustomFieldStringTemplate } from '/client/lib/customFields'
|
||||
import { getCurrentCardFromContext } from '/client/lib/currentCard';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { getSidebarInstance } from '/client/features/sidebar/service';
|
||||
|
||||
Template.cardCustomFieldsPopup.helpers({
|
||||
hasCustomField() {
|
||||
const card = Utils.getCurrentCard();
|
||||
const card = getCurrentCardFromContext();
|
||||
if (!card) return false;
|
||||
const customFieldId = this._id;
|
||||
return card.customFieldIndex(customFieldId) > -1;
|
||||
},
|
||||
@@ -14,315 +43,294 @@ Template.cardCustomFieldsPopup.helpers({
|
||||
|
||||
Template.cardCustomFieldsPopup.events({
|
||||
'click .js-select-field'(event) {
|
||||
const card = Utils.getCurrentCard();
|
||||
const card = getCurrentCardFromContext();
|
||||
if (!card) return;
|
||||
const customFieldId = this._id;
|
||||
card.toggleCustomField(customFieldId);
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-settings'(event) {
|
||||
EscapeActions.executeUpTo('detailsPane');
|
||||
Sidebar.setView('customFields');
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar) {
|
||||
sidebar.setView('customFields');
|
||||
}
|
||||
event.preventDefault();
|
||||
},
|
||||
});
|
||||
|
||||
// cardCustomField
|
||||
const CardCustomField = BlazeComponent.extendComponent({
|
||||
Template.cardCustomField.helpers({
|
||||
getTemplate() {
|
||||
return `cardCustomField-${this.data().definition.type}`;
|
||||
},
|
||||
|
||||
onCreated() {
|
||||
const self = this;
|
||||
self.card = Utils.getCurrentCard();
|
||||
self.customFieldId = this.data()._id;
|
||||
return `cardCustomField-${this.definition.type}`;
|
||||
},
|
||||
});
|
||||
CardCustomField.register('cardCustomField');
|
||||
|
||||
Template.cardCustomField.onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
});
|
||||
|
||||
// cardCustomField-text
|
||||
(class extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
}
|
||||
Template['cardCustomField-text'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-customfield-text'(event) {
|
||||
event.preventDefault();
|
||||
const value = this.currentComponent().getValue();
|
||||
this.card.setCustomField(this.customFieldId, value);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}.register('cardCustomField-text'));
|
||||
Template['cardCustomField-text'].events({
|
||||
'submit .js-card-customfield-text'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const value = tpl.currentComponent ? tpl.currentComponent().getValue() : tpl.$('textarea').val();
|
||||
tpl.card.setCustomField(tpl.customFieldId, value);
|
||||
},
|
||||
});
|
||||
|
||||
// cardCustomField-number
|
||||
(class extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
}
|
||||
Template['cardCustomField-number'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-customfield-number'(event) {
|
||||
event.preventDefault();
|
||||
const value = parseInt(this.find('input').value, 10);
|
||||
this.card.setCustomField(this.customFieldId, value);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}.register('cardCustomField-number'));
|
||||
Template['cardCustomField-number'].events({
|
||||
'submit .js-card-customfield-number'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const value = parseInt(tpl.find('input').value, 10);
|
||||
tpl.card.setCustomField(tpl.customFieldId, value);
|
||||
},
|
||||
});
|
||||
|
||||
// cardCustomField-checkbox
|
||||
(class extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
}
|
||||
Template['cardCustomField-checkbox'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
});
|
||||
|
||||
toggleItem() {
|
||||
this.card.setCustomField(this.customFieldId, !this.data().value);
|
||||
}
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-checklist-item .check-box-container': this.toggleItem,
|
||||
},
|
||||
];
|
||||
}
|
||||
}.register('cardCustomField-checkbox'));
|
||||
Template['cardCustomField-checkbox'].events({
|
||||
'click .js-checklist-item .check-box-container'(event, tpl) {
|
||||
tpl.card.setCustomField(tpl.customFieldId, !Template.currentData().value);
|
||||
},
|
||||
});
|
||||
|
||||
// cardCustomField-currency
|
||||
(class extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
|
||||
this.currencyCode = this.data().definition.settings.currencyCode;
|
||||
}
|
||||
Template['cardCustomField-currency'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
this.currencyCode = Template.currentData().definition.settings.currencyCode;
|
||||
});
|
||||
|
||||
Template['cardCustomField-currency'].helpers({
|
||||
formattedValue() {
|
||||
const locale = TAPi18n.getLanguage();
|
||||
|
||||
const tpl = Template.instance();
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: this.currencyCode,
|
||||
}).format(this.data().value);
|
||||
}
|
||||
currency: tpl.currencyCode,
|
||||
}).format(this.value);
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-customfield-currency'(event) {
|
||||
event.preventDefault();
|
||||
// To allow input separated by comma, the comma is replaced by a period.
|
||||
const value = Number(this.find('input').value.replace(/,/i, '.'), 10);
|
||||
this.card.setCustomField(this.customFieldId, value);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}.register('cardCustomField-currency'));
|
||||
Template['cardCustomField-currency'].events({
|
||||
'submit .js-card-customfield-currency'(event, tpl) {
|
||||
event.preventDefault();
|
||||
// To allow input separated by comma, the comma is replaced by a period.
|
||||
const value = Number(tpl.find('input').value.replace(/,/i, '.'), 10);
|
||||
tpl.card.setCustomField(tpl.customFieldId, value);
|
||||
},
|
||||
});
|
||||
|
||||
// cardCustomField-date
|
||||
(class extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.date = ReactiveVar();
|
||||
self.now = ReactiveVar(moment());
|
||||
window.setInterval(() => {
|
||||
self.now.set(moment());
|
||||
}, 60000);
|
||||
Template['cardCustomField-date'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
const self = this;
|
||||
self.date = ReactiveVar();
|
||||
self.now = ReactiveVar(now());
|
||||
window.setInterval(() => {
|
||||
self.now.set(now());
|
||||
}, 60000);
|
||||
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().value));
|
||||
});
|
||||
}
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().value));
|
||||
});
|
||||
});
|
||||
|
||||
Template['cardCustomField-date'].helpers({
|
||||
showWeek() {
|
||||
return this.date.get().week().toString();
|
||||
}
|
||||
|
||||
return getISOWeek(Template.instance().date.get()).toString();
|
||||
},
|
||||
showWeekOfYear() {
|
||||
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
return user.isShowWeekOfYear();
|
||||
},
|
||||
showDate() {
|
||||
// this will start working once mquandalle:moment
|
||||
// is updated to at least moment.js 2.10.5
|
||||
// until then, the date is displayed in the "L" format
|
||||
return this.date.get().calendar(null, {
|
||||
sameElse: 'llll',
|
||||
});
|
||||
}
|
||||
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
showISODate() {
|
||||
return this.date.get().toISOString();
|
||||
}
|
||||
|
||||
return Template.instance().date.get().toISOString();
|
||||
},
|
||||
classes() {
|
||||
const tpl = Template.instance();
|
||||
if (
|
||||
this.date.get().isBefore(this.now.get(), 'minute') &&
|
||||
this.now.get().isBefore(this.data().value)
|
||||
isBefore(tpl.date.get(), tpl.now.get(), 'minute') &&
|
||||
isBefore(tpl.now.get(), this.value, 'minute')
|
||||
) {
|
||||
return 'current';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
},
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
|
||||
}
|
||||
return `${TAPi18n.__('card-start-on')} ${Template.instance().date.get().toLocaleString()}`;
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-edit-date': Popup.open('cardCustomField-date'),
|
||||
},
|
||||
];
|
||||
}
|
||||
}.register('cardCustomField-date'));
|
||||
Template['cardCustomField-date'].events({
|
||||
'click .js-edit-date': Popup.open('cardCustomField-date'),
|
||||
});
|
||||
|
||||
// cardCustomField-datePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.card = Utils.getCurrentCard();
|
||||
self.customFieldId = this.data()._id;
|
||||
this.data().value && this.date.set(moment(this.data().value));
|
||||
}
|
||||
Template['cardCustomField-datePopup'].onCreated(function () {
|
||||
const data = Template.currentData();
|
||||
setupDatePicker(this, {
|
||||
initialDate: data.value ? data.value : undefined,
|
||||
});
|
||||
// Override card and store customFieldId for store/delete callbacks
|
||||
this.datePicker.card = getCurrentCardFromContext();
|
||||
this.customFieldId = data._id;
|
||||
});
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setCustomField(this.customFieldId, date);
|
||||
}
|
||||
Template['cardCustomField-datePopup'].onRendered(function () {
|
||||
datePickerRendered(this);
|
||||
});
|
||||
|
||||
_deleteDate() {
|
||||
this.card.setCustomField(this.customFieldId, '');
|
||||
}
|
||||
}.register('cardCustomField-datePopup'));
|
||||
Template['cardCustomField-datePopup'].helpers(datePickerHelpers());
|
||||
|
||||
Template['cardCustomField-datePopup'].events(datePickerEvents({
|
||||
storeDate(date) {
|
||||
this.datePicker.card.setCustomField(this.customFieldId, date);
|
||||
},
|
||||
deleteDate() {
|
||||
this.datePicker.card.setCustomField(this.customFieldId, '');
|
||||
},
|
||||
}));
|
||||
|
||||
// cardCustomField-dropdown
|
||||
(class extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
this._items = this.data().definition.settings.dropdownItems;
|
||||
this.items = this._items.slice(0);
|
||||
this.items.unshift({
|
||||
_id: '',
|
||||
name: TAPi18n.__('custom-field-dropdown-none'),
|
||||
});
|
||||
}
|
||||
Template['cardCustomField-dropdown'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
this._items = Template.currentData().definition.settings.dropdownItems;
|
||||
this.items = this._items.slice(0);
|
||||
this.items.unshift({
|
||||
_id: '',
|
||||
name: TAPi18n.__('custom-field-dropdown-none'),
|
||||
});
|
||||
});
|
||||
|
||||
Template['cardCustomField-dropdown'].helpers({
|
||||
items() {
|
||||
return Template.instance().items;
|
||||
},
|
||||
selectedItem() {
|
||||
const selected = this._items.find(item => {
|
||||
return item._id === this.data().value;
|
||||
const tpl = Template.instance();
|
||||
const selected = tpl._items.find(item => {
|
||||
return item._id === this.value;
|
||||
});
|
||||
return selected
|
||||
? selected.name
|
||||
: TAPi18n.__('custom-field-dropdown-unknown');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-customfield-dropdown'(event) {
|
||||
event.preventDefault();
|
||||
const value = this.find('select').value;
|
||||
this.card.setCustomField(this.customFieldId, value);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}.register('cardCustomField-dropdown'));
|
||||
Template['cardCustomField-dropdown'].events({
|
||||
'submit .js-card-customfield-dropdown'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const value = tpl.find('select').value;
|
||||
tpl.card.setCustomField(tpl.customFieldId, value);
|
||||
},
|
||||
});
|
||||
|
||||
// cardCustomField-stringtemplate
|
||||
class CardCustomFieldStringTemplate extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
|
||||
this.customField = new CustomFieldStringTemplate(this.data().definition);
|
||||
|
||||
this.stringtemplateItems = new ReactiveVar(this.data().value ?? []);
|
||||
}
|
||||
Template['cardCustomField-stringtemplate'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
this.customField = new CustomFieldStringTemplate(Template.currentData().definition);
|
||||
this.stringtemplateItems = new ReactiveVar(Template.currentData().value ?? []);
|
||||
});
|
||||
|
||||
Template['cardCustomField-stringtemplate'].helpers({
|
||||
formattedValue() {
|
||||
const ret = this.customField.getFormattedValue(this.data().value);
|
||||
const tpl = Template.instance();
|
||||
const ret = tpl.customField.getFormattedValue(this.value);
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
stringtemplateItems() {
|
||||
return Template.instance().stringtemplateItems.get();
|
||||
},
|
||||
});
|
||||
|
||||
getItems() {
|
||||
return Array.from(this.findAll('input'))
|
||||
.map(input => input.value)
|
||||
.filter(value => !!value.trim());
|
||||
}
|
||||
Template['cardCustomField-stringtemplate'].events({
|
||||
'submit .js-card-customfield-stringtemplate'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const items = tpl.stringtemplateItems.get();
|
||||
tpl.card.setCustomField(tpl.customFieldId, items);
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-customfield-stringtemplate'(event) {
|
||||
event.preventDefault();
|
||||
const items = this.stringtemplateItems.get();
|
||||
this.card.setCustomField(this.customFieldId, items);
|
||||
},
|
||||
'keydown .js-card-customfield-stringtemplate-item'(event, tpl) {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
|
||||
'keydown .js-card-customfield-stringtemplate-item'(event) {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
if (event.target.value.trim() || event.metaKey || event.ctrlKey) {
|
||||
const inputLast = tpl.find('input.last');
|
||||
|
||||
if (event.target.value.trim() || event.metaKey || event.ctrlKey) {
|
||||
const inputLast = this.find('input.last');
|
||||
let items = Array.from(tpl.findAll('input'))
|
||||
.map(input => input.value)
|
||||
.filter(value => !!value.trim());
|
||||
|
||||
let items = this.getItems();
|
||||
if (event.target === inputLast) {
|
||||
inputLast.value = '';
|
||||
} else if (event.target.nextSibling === inputLast) {
|
||||
inputLast.focus();
|
||||
} else {
|
||||
event.target.blur();
|
||||
|
||||
if (event.target === inputLast) {
|
||||
inputLast.value = '';
|
||||
} else if (event.target.nextSibling === inputLast) {
|
||||
inputLast.focus();
|
||||
} else {
|
||||
event.target.blur();
|
||||
const idx = Array.from(tpl.findAll('input')).indexOf(
|
||||
event.target,
|
||||
);
|
||||
items.splice(idx + 1, 0, '');
|
||||
|
||||
const idx = Array.from(this.findAll('input')).indexOf(
|
||||
event.target,
|
||||
);
|
||||
items.splice(idx + 1, 0, '');
|
||||
Tracker.afterFlush(() => {
|
||||
const element = tpl.findAll('input')[idx + 1];
|
||||
element.focus();
|
||||
element.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
Tracker.afterFlush(() => {
|
||||
const element = this.findAll('input')[idx + 1];
|
||||
element.focus();
|
||||
element.value = '';
|
||||
});
|
||||
}
|
||||
tpl.stringtemplateItems.set(items);
|
||||
}
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
tpl.find('button[type=submit]').click();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
this.stringtemplateItems.set(items);
|
||||
}
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
this.find('button[type=submit]').click();
|
||||
}
|
||||
}
|
||||
},
|
||||
'blur .js-card-customfield-stringtemplate-item'(event, tpl) {
|
||||
if (
|
||||
!event.target.value.trim() ||
|
||||
event.target === tpl.find('input.last')
|
||||
) {
|
||||
const items = Array.from(tpl.findAll('input'))
|
||||
.map(input => input.value)
|
||||
.filter(value => !!value.trim());
|
||||
tpl.stringtemplateItems.set(items);
|
||||
tpl.find('input.last').value = '';
|
||||
}
|
||||
},
|
||||
|
||||
'blur .js-card-customfield-stringtemplate-item'(event) {
|
||||
if (
|
||||
!event.target.value.trim() ||
|
||||
event.target === this.find('input.last')
|
||||
) {
|
||||
const items = this.getItems();
|
||||
this.stringtemplateItems.set(items);
|
||||
this.find('input.last').value = '';
|
||||
}
|
||||
},
|
||||
|
||||
'click .js-close-inlined-form'(event) {
|
||||
this.stringtemplateItems.set(this.data().value ?? []);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
CardCustomFieldStringTemplate.register('cardCustomField-stringtemplate');
|
||||
'click .js-close-inlined-form'(event, tpl) {
|
||||
tpl.stringtemplateItems.set(Template.currentData().value ?? []);
|
||||
},
|
||||
});
|
||||
@@ -8,57 +8,134 @@
|
||||
.card-date.is-active {
|
||||
background-color: #b3b3b3;
|
||||
}
|
||||
.card-date.current,
|
||||
.card-date.almost-due,
|
||||
.card-date.due,
|
||||
.card-date.long-overdue {
|
||||
/* Date status colors - red = overdue, amber = due soon, no shade = not due */
|
||||
.card-date.overdue {
|
||||
background-color: #ff4444; /* Red for overdue */
|
||||
color: #fff;
|
||||
}
|
||||
.card-date.overdue:hover,
|
||||
.card-date.overdue.is-active {
|
||||
background-color: #cc3333;
|
||||
}
|
||||
|
||||
.card-date.due-soon {
|
||||
background-color: #ffaa00; /* Amber for due soon */
|
||||
color: #000;
|
||||
}
|
||||
.card-date.due-soon:hover,
|
||||
.card-date.due-soon.is-active {
|
||||
background-color: #e69900;
|
||||
}
|
||||
|
||||
.card-date.not-due {
|
||||
/* No special background - uses default date type colors */
|
||||
}
|
||||
|
||||
.card-date.current {
|
||||
background-color: #5ba639;
|
||||
background-color: #5ba639; /* Green for current/active */
|
||||
color: #fff;
|
||||
}
|
||||
.card-date.current:hover,
|
||||
.card-date.current.is-active {
|
||||
background-color: #46802c;
|
||||
}
|
||||
.card-date.almost-due {
|
||||
background-color: #edc909;
|
||||
|
||||
.card-date.completed {
|
||||
background-color: #90ee90; /* Light green for completed */
|
||||
color: #000;
|
||||
}
|
||||
.card-date.almost-due:hover,
|
||||
.card-date.almost-due.is-active {
|
||||
background-color: #bc9f07;
|
||||
.card-date.completed:hover,
|
||||
.card-date.completed.is-active {
|
||||
background-color: #7dd87d;
|
||||
}
|
||||
.card-date.due {
|
||||
background-color: #fa3f00;
|
||||
|
||||
.card-date.completed-early {
|
||||
background-color: #4caf50; /* Green for completed early */
|
||||
color: #fff;
|
||||
}
|
||||
.card-date.due:hover,
|
||||
.card-date.due.is-active {
|
||||
background-color: #c73200;
|
||||
.card-date.completed-early:hover,
|
||||
.card-date.completed-early.is-active {
|
||||
background-color: #45a049;
|
||||
}
|
||||
.card-date.long-overdue {
|
||||
background-color: #fd5d47;
|
||||
|
||||
.card-date.completed-late {
|
||||
background-color: #ff9800; /* Orange for completed late */
|
||||
color: #fff;
|
||||
}
|
||||
.card-date.long-overdue:hover,
|
||||
.card-date.long-overdue.is-active {
|
||||
background-color: #fd3e24;
|
||||
.card-date.completed-late:hover,
|
||||
.card-date.completed-late.is-active {
|
||||
background-color: #f57c00;
|
||||
}
|
||||
|
||||
.card-date.completed-on-time {
|
||||
background-color: #2196f3; /* Blue for completed on time */
|
||||
color: #fff;
|
||||
}
|
||||
.card-date.completed-on-time:hover,
|
||||
.card-date.completed-on-time.is-active {
|
||||
background-color: #1976d2;
|
||||
}
|
||||
|
||||
/* Date type specific colors */
|
||||
.card-date.received-date {
|
||||
background-color: #dbdbdb; /* Light grey for received */
|
||||
}
|
||||
|
||||
.card-date.received-date:hover,
|
||||
.card-date.received-date.is-active {
|
||||
background-color: #b3b3b3;
|
||||
}
|
||||
|
||||
.card-date.start-date {
|
||||
background-color: #90ee90; /* Light green for start */
|
||||
color: #000; /* Black text for start */
|
||||
}
|
||||
|
||||
.card-date.start-date:hover,
|
||||
.card-date.start-date.is-active {
|
||||
background-color: #7dd87d;
|
||||
}
|
||||
|
||||
.card-date.due-date {
|
||||
background-color: #ffd700; /* Yellow for due */
|
||||
color: #000; /* Black text for due */
|
||||
}
|
||||
|
||||
.card-date.due-date:hover,
|
||||
.card-date.due-date.is-active {
|
||||
background-color: #e6c200;
|
||||
}
|
||||
|
||||
.card-date.end-date {
|
||||
background-color: #ffb3b3; /* Light red for end */
|
||||
color: #000; /* Black text for end */
|
||||
}
|
||||
|
||||
.card-date.end-date:hover,
|
||||
.card-date.end-date.is-active {
|
||||
background-color: #ff9999;
|
||||
}
|
||||
.card-date.end-date time::before {
|
||||
content: "\f253";
|
||||
content: "🏁"; /* Finish flag - represents end/completion */
|
||||
}
|
||||
.card-date.due-date time::before {
|
||||
content: "\f090";
|
||||
content: "⏰"; /* Alarm clock - represents due/deadline */
|
||||
}
|
||||
.card-date.start-date time::before {
|
||||
content: "\f251";
|
||||
content: "🚀"; /* Rocket - represents start/launch */
|
||||
}
|
||||
.card-date.received-date time::before {
|
||||
content: "\f08b";
|
||||
content: "📥"; /* Inbox tray - represents received/incoming */
|
||||
}
|
||||
|
||||
/* Generic date badge and custom field date */
|
||||
.card-date:not(.received-date):not(.start-date):not(.due-date):not(.end-date) time::before {
|
||||
/*content: "📅"; // Calendar - represents generic date */
|
||||
}
|
||||
.card-date time::before {
|
||||
font: normal normal normal 14px/1 FontAwesome;
|
||||
font-size: inherit;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
margin-right: 0.3em;
|
||||
display: inline-block;
|
||||
}
|
||||
.customfield-date {
|
||||
display: block;
|
||||
|
||||
@@ -14,6 +14,70 @@ template(name="dateBadge")
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardReceivedDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardStartDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardDueDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardEndDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="dateCustomField")
|
||||
a(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
@@ -21,3 +85,205 @@ template(name="dateCustomField")
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardCustomFieldDate")
|
||||
a(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="voteEndDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="pokerEndDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="minicardReceivedDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date.received-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date.received-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="minicardStartDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date.start-date(title="{{_ 'card-start'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date.start-date(title="{{_ 'card-start'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="minicardDueDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date.due-date(title="{{_ 'card-due'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date.due-date(title="{{_ 'card-due'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="minicardEndDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date.end-date(title="{{_ 'card-end'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date.end-date(title="{{_ 'card-end'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="minicardCustomFieldDate")
|
||||
a(title="{{_ 'date'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="editCardReceivedDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="editCardStartDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="editCardDueDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="editCardEndDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="editVoteEndDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="editPokerEndDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
+591
-348
@@ -1,399 +1,642 @@
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { DatePicker } from '/client/lib/datepicker';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import {
|
||||
setupDatePicker,
|
||||
datePickerRendered,
|
||||
datePickerHelpers,
|
||||
datePickerEvents,
|
||||
} from '/client/lib/datepicker';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatDateByUserPreference,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar,
|
||||
diff
|
||||
} from '/imports/lib/dateUtils';
|
||||
|
||||
// --- DatePicker popups (edit date forms) ---
|
||||
|
||||
// editCardReceivedDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
this.data().getReceived() &&
|
||||
this.date.set(moment(this.data().getReceived()));
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setReceived(moment(date).format('YYYY-MM-DD HH:mm'));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
this.card.unsetReceived();
|
||||
}
|
||||
}.register('editCardReceivedDatePopup'));
|
||||
|
||||
// editCardStartDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
this.data().getStart() && this.date.set(moment(this.data().getStart()));
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
super.onRendered();
|
||||
if (moment.isDate(this.card.getReceived())) {
|
||||
this.$('.js-datepicker').datepicker(
|
||||
'setStartDate',
|
||||
this.card.getReceived(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setStart(moment(date).format('YYYY-MM-DD HH:mm'));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
this.card.unsetStart();
|
||||
}
|
||||
}.register('editCardStartDatePopup'));
|
||||
|
||||
// editCardDueDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated('1970-01-01 17:00:00');
|
||||
this.data().getDue() && this.date.set(moment(this.data().getDue()));
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
super.onRendered();
|
||||
if (moment.isDate(this.card.getStart())) {
|
||||
this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
|
||||
}
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setDue(moment(date).format('YYYY-MM-DD HH:mm'));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
this.card.unsetDue();
|
||||
}
|
||||
}.register('editCardDueDatePopup'));
|
||||
|
||||
// editCardEndDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
this.data().getEnd() && this.date.set(moment(this.data().getEnd()));
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
super.onRendered();
|
||||
if (moment.isDate(this.card.getStart())) {
|
||||
this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
|
||||
}
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setEnd(moment(date).format('YYYY-MM-DD HH:mm'));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
this.card.unsetEnd();
|
||||
}
|
||||
}.register('editCardEndDatePopup'));
|
||||
|
||||
// Display received, start, due & end dates
|
||||
const CardDate = BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'dateBadge';
|
||||
},
|
||||
|
||||
onCreated() {
|
||||
const self = this;
|
||||
self.date = ReactiveVar();
|
||||
self.now = ReactiveVar(moment());
|
||||
window.setInterval(() => {
|
||||
self.now.set(moment());
|
||||
}, 60000);
|
||||
},
|
||||
|
||||
showWeek() {
|
||||
return this.date.get().week().toString();
|
||||
},
|
||||
|
||||
showWeekOfYear() {
|
||||
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
|
||||
},
|
||||
|
||||
showDate() {
|
||||
// this will start working once mquandalle:moment
|
||||
// is updated to at least moment.js 2.10.5
|
||||
// until then, the date is displayed in the "L" format
|
||||
return this.date.get().calendar(null, {
|
||||
sameElse: 'llll',
|
||||
});
|
||||
},
|
||||
|
||||
showISODate() {
|
||||
return this.date.get().toISOString();
|
||||
},
|
||||
Template.editCardReceivedDatePopup.onCreated(function () {
|
||||
const card = Template.currentData();
|
||||
setupDatePicker(this, {
|
||||
defaultTime: formatDateTime(now()),
|
||||
initialDate: card.getReceived() ? card.getReceived() : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
class CardReceivedDate extends CardDate {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getReceived()));
|
||||
});
|
||||
}
|
||||
Template.editCardReceivedDatePopup.onRendered(function () {
|
||||
datePickerRendered(this);
|
||||
});
|
||||
|
||||
Template.editCardReceivedDatePopup.helpers(datePickerHelpers());
|
||||
|
||||
Template.editCardReceivedDatePopup.events(datePickerEvents({
|
||||
storeDate(date) {
|
||||
this.datePicker.card.setReceived(date);
|
||||
},
|
||||
deleteDate() {
|
||||
this.datePicker.card.unsetReceived();
|
||||
},
|
||||
}));
|
||||
|
||||
// editCardStartDatePopup
|
||||
Template.editCardStartDatePopup.onCreated(function () {
|
||||
const card = Template.currentData();
|
||||
setupDatePicker(this, {
|
||||
defaultTime: formatDateTime(now()),
|
||||
initialDate: card.getStart() ? card.getStart() : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
Template.editCardStartDatePopup.onRendered(function () {
|
||||
datePickerRendered(this);
|
||||
});
|
||||
|
||||
Template.editCardStartDatePopup.helpers(datePickerHelpers());
|
||||
|
||||
Template.editCardStartDatePopup.events(datePickerEvents({
|
||||
storeDate(date) {
|
||||
this.datePicker.card.setStart(date);
|
||||
},
|
||||
deleteDate() {
|
||||
this.datePicker.card.unsetStart();
|
||||
},
|
||||
}));
|
||||
|
||||
// editCardDueDatePopup
|
||||
Template.editCardDueDatePopup.onCreated(function () {
|
||||
const card = Template.currentData();
|
||||
setupDatePicker(this, {
|
||||
defaultTime: '1970-01-01 17:00:00',
|
||||
initialDate: card.getDue() ? card.getDue() : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
Template.editCardDueDatePopup.onRendered(function () {
|
||||
datePickerRendered(this);
|
||||
});
|
||||
|
||||
Template.editCardDueDatePopup.helpers(datePickerHelpers());
|
||||
|
||||
Template.editCardDueDatePopup.events(datePickerEvents({
|
||||
storeDate(date) {
|
||||
this.datePicker.card.setDue(date);
|
||||
},
|
||||
deleteDate() {
|
||||
this.datePicker.card.unsetDue();
|
||||
},
|
||||
}));
|
||||
|
||||
// editCardEndDatePopup
|
||||
Template.editCardEndDatePopup.onCreated(function () {
|
||||
const card = Template.currentData();
|
||||
setupDatePicker(this, {
|
||||
defaultTime: formatDateTime(now()),
|
||||
initialDate: card.getEnd() ? card.getEnd() : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
Template.editCardEndDatePopup.onRendered(function () {
|
||||
datePickerRendered(this);
|
||||
});
|
||||
|
||||
Template.editCardEndDatePopup.helpers(datePickerHelpers());
|
||||
|
||||
Template.editCardEndDatePopup.events(datePickerEvents({
|
||||
storeDate(date) {
|
||||
this.datePicker.card.setEnd(date);
|
||||
},
|
||||
deleteDate() {
|
||||
this.datePicker.card.unsetEnd();
|
||||
},
|
||||
}));
|
||||
|
||||
// --- Card date badge display helpers ---
|
||||
|
||||
// Shared onCreated logic for card date badge templates
|
||||
function cardDateOnCreated(tpl) {
|
||||
tpl.date = new ReactiveVar();
|
||||
tpl.now = new ReactiveVar(now());
|
||||
window.setInterval(() => {
|
||||
tpl.now.set(now());
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
// Shared helpers for card date badge templates
|
||||
function cardDateHelpers(extraHelpers) {
|
||||
const base = {
|
||||
showWeek() {
|
||||
return getISOWeek(Template.instance().date.get()).toString();
|
||||
},
|
||||
showWeekOfYear() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (!user) return window.localStorage.getItem('showWeekOfYear') === 'true';
|
||||
return user.isShowWeekOfYear();
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
showISODate() {
|
||||
return Template.instance().date.get().toISOString();
|
||||
},
|
||||
};
|
||||
return Object.assign(base, extraHelpers);
|
||||
}
|
||||
|
||||
// cardReceivedDate
|
||||
Template.cardReceivedDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getReceived()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.cardReceivedDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
const tpl = Template.instance();
|
||||
let classes = 'received-date ';
|
||||
const dueAt = this.data().getDue();
|
||||
const endAt = this.data().getEnd();
|
||||
const startAt = this.data().getStart();
|
||||
const theDate = this.date.get();
|
||||
// if dueAt, endAt and startAt exist & are > receivedAt, receivedAt doesn't need to be flagged
|
||||
const data = Template.currentData();
|
||||
const dueAt = data.getDue();
|
||||
const endAt = data.getEnd();
|
||||
const startAt = data.getStart();
|
||||
const theDate = tpl.date.get();
|
||||
|
||||
if (
|
||||
(startAt && theDate.isAfter(startAt)) ||
|
||||
(endAt && theDate.isAfter(endAt)) ||
|
||||
(dueAt && theDate.isAfter(dueAt))
|
||||
)
|
||||
classes += 'long-overdue';
|
||||
else classes += 'current';
|
||||
(startAt && isAfter(theDate, startAt)) ||
|
||||
(endAt && isAfter(theDate, endAt)) ||
|
||||
(dueAt && isAfter(theDate, dueAt))
|
||||
) {
|
||||
classes += 'overdue';
|
||||
} else {
|
||||
classes += 'not-due';
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
},
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-received-on')} ${this.date
|
||||
.get()
|
||||
.format('LLLL')}`;
|
||||
}
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
|
||||
},
|
||||
}));
|
||||
|
||||
events() {
|
||||
return super.events().concat({
|
||||
'click .js-edit-date': Popup.open('editCardReceivedDate'),
|
||||
});
|
||||
}
|
||||
}
|
||||
CardReceivedDate.register('cardReceivedDate');
|
||||
Template.cardReceivedDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardReceivedDate'),
|
||||
});
|
||||
|
||||
class CardStartDate extends CardDate {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getStart()));
|
||||
});
|
||||
}
|
||||
// cardStartDate
|
||||
Template.cardStartDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getStart()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.cardStartDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
let classes = 'start-date' + ' ';
|
||||
const dueAt = this.data().getDue();
|
||||
const endAt = this.data().getEnd();
|
||||
const theDate = this.date.get();
|
||||
const now = this.now.get();
|
||||
// if dueAt or endAt exist & are > startAt, startAt doesn't need to be flagged
|
||||
if ((endAt && theDate.isAfter(endAt)) || (dueAt && theDate.isAfter(dueAt)))
|
||||
classes += 'long-overdue';
|
||||
else if (theDate.isAfter(now)) classes += '';
|
||||
else classes += 'current';
|
||||
const tpl = Template.instance();
|
||||
let classes = 'start-date ';
|
||||
const data = Template.currentData();
|
||||
const dueAt = data.getDue();
|
||||
const endAt = data.getEnd();
|
||||
const theDate = tpl.date.get();
|
||||
const nowVal = tpl.now.get();
|
||||
|
||||
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
|
||||
classes += 'overdue';
|
||||
} else if (isAfter(theDate, nowVal)) {
|
||||
classes += 'not-due';
|
||||
} else {
|
||||
classes += 'current';
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
},
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
|
||||
}
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
|
||||
},
|
||||
}));
|
||||
|
||||
events() {
|
||||
return super.events().concat({
|
||||
'click .js-edit-date': Popup.open('editCardStartDate'),
|
||||
});
|
||||
}
|
||||
}
|
||||
CardStartDate.register('cardStartDate');
|
||||
Template.cardStartDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardStartDate'),
|
||||
});
|
||||
|
||||
class CardDueDate extends CardDate {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getDue()));
|
||||
});
|
||||
}
|
||||
// cardDueDate
|
||||
Template.cardDueDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getDue()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.cardDueDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
let classes = 'due-date' + ' ';
|
||||
const endAt = this.data().getEnd();
|
||||
const theDate = this.date.get();
|
||||
const now = this.now.get();
|
||||
// if the due date is after the end date, green - done early
|
||||
if (endAt && theDate.isAfter(endAt)) classes += 'current';
|
||||
// if there is an end date, don't need to flag the due date
|
||||
else if (endAt) classes += '';
|
||||
else if (now.diff(theDate, 'days') >= 2) classes += 'long-overdue';
|
||||
else if (now.diff(theDate, 'minute') >= 0) classes += 'due';
|
||||
else if (now.diff(theDate, 'days') >= -1) classes += 'almost-due';
|
||||
const tpl = Template.instance();
|
||||
let classes = 'due-date ';
|
||||
const data = Template.currentData();
|
||||
const endAt = data.getEnd();
|
||||
const theDate = tpl.date.get();
|
||||
const nowVal = tpl.now.get();
|
||||
|
||||
if (endAt && isBefore(endAt, theDate)) {
|
||||
classes += 'completed-early';
|
||||
} else if (endAt) {
|
||||
classes += 'completed';
|
||||
} else {
|
||||
const daysDiff = diff(theDate, nowVal, 'days');
|
||||
|
||||
if (daysDiff < 0) {
|
||||
classes += 'overdue';
|
||||
} else if (daysDiff <= 1) {
|
||||
classes += 'due-soon';
|
||||
} else {
|
||||
classes += 'not-due';
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
},
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-due-on')} ${this.date.get().format('LLLL')}`;
|
||||
}
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${TAPi18n.__('card-due-on')} ${formattedDate}`;
|
||||
},
|
||||
}));
|
||||
|
||||
events() {
|
||||
return super.events().concat({
|
||||
'click .js-edit-date': Popup.open('editCardDueDate'),
|
||||
});
|
||||
}
|
||||
}
|
||||
CardDueDate.register('cardDueDate');
|
||||
Template.cardDueDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardDueDate'),
|
||||
});
|
||||
|
||||
class CardEndDate extends CardDate {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getEnd()));
|
||||
});
|
||||
}
|
||||
// cardEndDate
|
||||
Template.cardEndDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getEnd()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.cardEndDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
let classes = 'end-date' + ' ';
|
||||
const dueAt = this.data().getDue();
|
||||
const theDate = this.date.get();
|
||||
if (!dueAt) classes += '';
|
||||
else if (theDate.isBefore(dueAt)) classes += 'current';
|
||||
else if (theDate.isAfter(dueAt)) classes += 'due';
|
||||
const tpl = Template.instance();
|
||||
let classes = 'end-date ';
|
||||
const data = Template.currentData();
|
||||
const dueAt = data.getDue();
|
||||
const theDate = tpl.date.get();
|
||||
|
||||
if (!dueAt) {
|
||||
classes += 'completed';
|
||||
} else if (isBefore(theDate, dueAt)) {
|
||||
classes += 'completed-early';
|
||||
} else if (isAfter(theDate, dueAt)) {
|
||||
classes += 'completed-late';
|
||||
} else {
|
||||
classes += 'completed-on-time';
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
},
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
|
||||
}
|
||||
const tpl = Template.instance();
|
||||
return `${TAPi18n.__('card-end-on')} ${format(tpl.date.get(), 'LLLL')}`;
|
||||
},
|
||||
}));
|
||||
|
||||
events() {
|
||||
return super.events().concat({
|
||||
'click .js-edit-date': Popup.open('editCardEndDate'),
|
||||
});
|
||||
}
|
||||
}
|
||||
CardEndDate.register('cardEndDate');
|
||||
Template.cardEndDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardEndDate'),
|
||||
});
|
||||
|
||||
class CardCustomFieldDate extends CardDate {
|
||||
template() {
|
||||
return 'dateCustomField';
|
||||
}
|
||||
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().value));
|
||||
});
|
||||
}
|
||||
|
||||
showWeek() {
|
||||
return this.date.get().week().toString();
|
||||
}
|
||||
|
||||
showWeekOfYear() {
|
||||
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
|
||||
}
|
||||
// cardCustomFieldDate
|
||||
Template.cardCustomFieldDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().value));
|
||||
});
|
||||
});
|
||||
|
||||
Template.cardCustomFieldDate.helpers(cardDateHelpers({
|
||||
showDate() {
|
||||
const tpl = Template.instance();
|
||||
// this will start working once mquandalle:moment
|
||||
// is updated to at least moment.js 2.10.5
|
||||
// until then, the date is displayed in the "L" format
|
||||
return this.date.get().calendar(null, {
|
||||
return tpl.date.get().calendar(null, {
|
||||
sameElse: 'llll',
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
showTitle() {
|
||||
return `${this.date.get().format('LLLL')}`;
|
||||
}
|
||||
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${formattedDate}`;
|
||||
},
|
||||
classes() {
|
||||
return 'customfield-date';
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
events() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
CardCustomFieldDate.register('cardCustomFieldDate');
|
||||
// --- Minicard date templates ---
|
||||
|
||||
(class extends CardReceivedDate {
|
||||
showDate() {
|
||||
return this.date.get().format('L');
|
||||
}
|
||||
}.register('minicardReceivedDate'));
|
||||
// minicardReceivedDate
|
||||
Template.minicardReceivedDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getReceived()));
|
||||
});
|
||||
});
|
||||
|
||||
(class extends CardStartDate {
|
||||
showDate() {
|
||||
return this.date.get().format('YYYY-MM-DD HH:mm');
|
||||
}
|
||||
}.register('minicardStartDate'));
|
||||
|
||||
(class extends CardDueDate {
|
||||
showDate() {
|
||||
return this.date.get().format('YYYY-MM-DD HH:mm');
|
||||
}
|
||||
}.register('minicardDueDate'));
|
||||
|
||||
(class extends CardEndDate {
|
||||
showDate() {
|
||||
return this.date.get().format('YYYY-MM-DD HH:mm');
|
||||
}
|
||||
}.register('minicardEndDate'));
|
||||
|
||||
(class extends CardCustomFieldDate {
|
||||
showDate() {
|
||||
return this.date.get().format('L');
|
||||
}
|
||||
}.register('minicardCustomFieldDate'));
|
||||
|
||||
class VoteEndDate extends CardDate {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getVoteEnd()));
|
||||
});
|
||||
}
|
||||
Template.minicardReceivedDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
const classes = 'end-date' + ' ';
|
||||
const tpl = Template.instance();
|
||||
let classes = 'received-date ';
|
||||
const data = Template.currentData();
|
||||
const dueAt = data.getDue();
|
||||
const endAt = data.getEnd();
|
||||
const startAt = data.getStart();
|
||||
const theDate = tpl.date.get();
|
||||
|
||||
if (
|
||||
(startAt && isAfter(theDate, startAt)) ||
|
||||
(endAt && isAfter(theDate, endAt)) ||
|
||||
(dueAt && isAfter(theDate, dueAt))
|
||||
) {
|
||||
classes += 'overdue';
|
||||
} else {
|
||||
classes += 'not-due';
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
showDate() {
|
||||
return this.date.get().format('L LT');
|
||||
}
|
||||
},
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
|
||||
}
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
}));
|
||||
|
||||
events() {
|
||||
return super.events().concat({
|
||||
'click .js-edit-date': Popup.open('editVoteEndDate'),
|
||||
});
|
||||
}
|
||||
}
|
||||
VoteEndDate.register('voteEndDate');
|
||||
Template.minicardReceivedDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardReceivedDate'),
|
||||
});
|
||||
|
||||
class PokerEndDate extends CardDate {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getPokerEnd()));
|
||||
});
|
||||
}
|
||||
// minicardStartDate
|
||||
Template.minicardStartDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getStart()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.minicardStartDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
const classes = 'end-date' + ' ';
|
||||
return classes;
|
||||
}
|
||||
showDate() {
|
||||
return this.date.get().format('l LT');
|
||||
}
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
|
||||
}
|
||||
const tpl = Template.instance();
|
||||
let classes = 'start-date ';
|
||||
const data = Template.currentData();
|
||||
const dueAt = data.getDue();
|
||||
const endAt = data.getEnd();
|
||||
const theDate = tpl.date.get();
|
||||
const nowVal = tpl.now.get();
|
||||
|
||||
events() {
|
||||
return super.events().concat({
|
||||
'click .js-edit-date': Popup.open('editPokerEndDate'),
|
||||
});
|
||||
}
|
||||
}
|
||||
PokerEndDate.register('pokerEndDate');
|
||||
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
|
||||
classes += 'overdue';
|
||||
} else if (isAfter(theDate, nowVal)) {
|
||||
classes += 'not-due';
|
||||
} else {
|
||||
classes += 'current';
|
||||
}
|
||||
return classes;
|
||||
},
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
}));
|
||||
|
||||
Template.minicardStartDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardStartDate'),
|
||||
});
|
||||
|
||||
// minicardDueDate
|
||||
Template.minicardDueDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getDue()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.minicardDueDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
const tpl = Template.instance();
|
||||
let classes = 'due-date ';
|
||||
const data = Template.currentData();
|
||||
const endAt = data.getEnd();
|
||||
const theDate = tpl.date.get();
|
||||
const nowVal = tpl.now.get();
|
||||
|
||||
if (endAt && isBefore(endAt, theDate)) {
|
||||
classes += 'completed-early';
|
||||
} else if (endAt) {
|
||||
classes += 'completed';
|
||||
} else {
|
||||
const daysDiff = diff(theDate, nowVal, 'days');
|
||||
|
||||
if (daysDiff < 0) {
|
||||
classes += 'overdue';
|
||||
} else if (daysDiff <= 1) {
|
||||
classes += 'due-soon';
|
||||
} else {
|
||||
classes += 'not-due';
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
},
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${TAPi18n.__('card-due-on')} ${formattedDate}`;
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
}));
|
||||
|
||||
Template.minicardDueDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardDueDate'),
|
||||
});
|
||||
|
||||
// minicardEndDate
|
||||
Template.minicardEndDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getEnd()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.minicardEndDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
const tpl = Template.instance();
|
||||
let classes = 'end-date ';
|
||||
const data = Template.currentData();
|
||||
const dueAt = data.getDue();
|
||||
const theDate = tpl.date.get();
|
||||
|
||||
if (!dueAt) {
|
||||
classes += 'completed';
|
||||
} else if (isBefore(theDate, dueAt)) {
|
||||
classes += 'completed-early';
|
||||
} else if (isAfter(theDate, dueAt)) {
|
||||
classes += 'completed-late';
|
||||
} else {
|
||||
classes += 'completed-on-time';
|
||||
}
|
||||
return classes;
|
||||
},
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
return `${TAPi18n.__('card-end-on')} ${format(tpl.date.get(), 'LLLL')}`;
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
}));
|
||||
|
||||
Template.minicardEndDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardEndDate'),
|
||||
});
|
||||
|
||||
// minicardCustomFieldDate
|
||||
Template.minicardCustomFieldDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().value));
|
||||
});
|
||||
});
|
||||
|
||||
Template.minicardCustomFieldDate.helpers(cardDateHelpers({
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${formattedDate}`;
|
||||
},
|
||||
classes() {
|
||||
return 'customfield-date';
|
||||
},
|
||||
}));
|
||||
|
||||
// --- Vote and Poker end date badge templates ---
|
||||
|
||||
// voteEndDate
|
||||
Template.voteEndDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getVoteEnd()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.voteEndDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
return 'end-date ';
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
return `${TAPi18n.__('card-end-on')} ${tpl.date.get().toLocaleString()}`;
|
||||
},
|
||||
}));
|
||||
|
||||
Template.voteEndDate.events({
|
||||
'click .js-edit-date': Popup.open('editVoteEndDate'),
|
||||
});
|
||||
|
||||
// pokerEndDate
|
||||
Template.pokerEndDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getPokerEnd()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.pokerEndDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
return 'end-date ';
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
return `${TAPi18n.__('card-end-on')} ${format(tpl.date.get(), 'LLLL')}`;
|
||||
},
|
||||
}));
|
||||
|
||||
Template.pokerEndDate.events({
|
||||
'click .js-edit-date': Popup.open('editPokerEndDate'),
|
||||
});
|
||||
@@ -1,37 +1,29 @@
|
||||
const descriptionFormIsOpen = new ReactiveVar(false);
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onDestroyed() {
|
||||
descriptionFormIsOpen.set(false);
|
||||
$('.note-popover').hide();
|
||||
},
|
||||
Template.descriptionForm.onDestroyed(function () {
|
||||
descriptionFormIsOpen.set(false);
|
||||
$('.note-popover').hide();
|
||||
});
|
||||
|
||||
Template.descriptionForm.helpers({
|
||||
descriptionFormIsOpen() {
|
||||
return descriptionFormIsOpen.get();
|
||||
},
|
||||
});
|
||||
|
||||
getInput() {
|
||||
return this.$('.js-new-description-input');
|
||||
Template.descriptionForm.events({
|
||||
async 'submit .js-card-description'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const description = tpl.currentComponent ? tpl.currentComponent().getValue() : tpl.$('textarea').val();
|
||||
await this.setDescription(description);
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-description'(event) {
|
||||
event.preventDefault();
|
||||
const description = this.currentComponent().getValue();
|
||||
this.data().setDescription(description);
|
||||
},
|
||||
// Pressing Ctrl+Enter should submit the form
|
||||
'keydown form textarea'(evt) {
|
||||
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
|
||||
const submitButton = this.find('button[type=submit]');
|
||||
if (submitButton) {
|
||||
submitButton.click();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
// Pressing Ctrl+Enter should submit the form
|
||||
'keydown form textarea'(evt, tpl) {
|
||||
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
|
||||
const submitButton = tpl.find('button[type=submit]');
|
||||
if (submitButton) {
|
||||
submitButton.click();
|
||||
}
|
||||
}
|
||||
},
|
||||
}).register('descriptionForm');
|
||||
});
|
||||
@@ -1,16 +1,46 @@
|
||||
/* Date Format Selector */
|
||||
.card-details-item-date-format {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-details-item-date-format .card-details-item-title {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 6px;
|
||||
color: #333;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.card-details-item-date-format .js-date-format-selector {
|
||||
width: 100%;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.card-details-item-date-format .js-date-format-selector:focus {
|
||||
outline: none;
|
||||
border-color: #007cba;
|
||||
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2);
|
||||
}
|
||||
|
||||
.assignee {
|
||||
border-radius: 0.4vw;
|
||||
display: block;
|
||||
position: relative;
|
||||
float: left;
|
||||
height: 4vw;
|
||||
width: 4vw;
|
||||
margin: 0.4vh;
|
||||
height: clamp(24px, 3.5vw, 36px);
|
||||
width: clamp(24px, 3.5vw, 36px);
|
||||
margin: 0.3vh;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
z-index: 1;
|
||||
text-decoration: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.04);
|
||||
}
|
||||
.assignee .avatar {
|
||||
overflow: hidden;
|
||||
@@ -23,23 +53,28 @@
|
||||
background-color: #dbdbdb;
|
||||
color: #444;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
.assignee .avatar.avatar-image {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.assignee .assignee-presence-status {
|
||||
background-color: #b3b3b3;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 50%;
|
||||
height: 1vw;
|
||||
width: 1vw;
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
position: absolute;
|
||||
right: -0.1vw;
|
||||
bottom: -0.1vw;
|
||||
border: 1px solid #fff;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
z-index: 15;
|
||||
}
|
||||
.assignee .assignee-presence-status.active {
|
||||
@@ -63,6 +98,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 0 2px #bfbfbf inset;
|
||||
transition: box-shadow 0.12s;
|
||||
}
|
||||
.assignee.add-assignee:hover,
|
||||
.assignee.add-assignee.is-active {
|
||||
@@ -74,22 +110,83 @@
|
||||
background-color: rgba(0,0,0,0.875);
|
||||
color: #fff;
|
||||
border-radius: 0.7vw;
|
||||
font-size: 0.98em;
|
||||
}
|
||||
|
||||
.card-details {
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: min(600px, 80vw);
|
||||
will-change: flex-basis;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: #f7f7f7;
|
||||
border-radius: bottom 0.4vw;
|
||||
border-radius: 0 0 0.4vw 0.4vw;
|
||||
z-index: 30;
|
||||
animation: flexGrowIn 0.1s;
|
||||
box-shadow: 0 0 0.9vh 0 #b3b3b3;
|
||||
transition: flex-basis 0.1s;
|
||||
transition: flex-basis 0.1s, box-shadow 0.15s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Desktop mode: position card below board header */
|
||||
body.desktop-mode .card-details:not(.card-details-popup) {
|
||||
position: fixed;
|
||||
width: auto;
|
||||
max-width: 800px;
|
||||
flex-basis: auto;
|
||||
border-radius: 8px;
|
||||
z-index: 2001;
|
||||
}
|
||||
|
||||
/* Default position for first card or when dragged */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):not([style*="left"]):not([style*="top"]) {
|
||||
top: 8px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
/* Stagger positions for multiple cards using nth-of-type */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(1) {
|
||||
top: 8px;
|
||||
left: 20px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(2) {
|
||||
top: 38px;
|
||||
left: 50px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(3) {
|
||||
top: 68px;
|
||||
left: 80px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(4) {
|
||||
top: 98px;
|
||||
left: 110px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(5) {
|
||||
top: 128px;
|
||||
left: 140px;
|
||||
}
|
||||
|
||||
/* For expanded cards, set dimensions */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):not(.card-details-collapsed) {
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
/* Collapsed card state - hide content and set height to title row only */
|
||||
.card-details.card-details-collapsed .card-details-canvas > *:not(.card-details-header) {
|
||||
display: none !important;
|
||||
}
|
||||
.card-details.card-details-collapsed {
|
||||
height: auto !important;
|
||||
bottom: auto !important;
|
||||
overflow: visible;
|
||||
}
|
||||
body.desktop-mode .card-details.card-details-collapsed {
|
||||
bottom: auto !important;
|
||||
}
|
||||
.card-details .mCustomScrollBox {
|
||||
padding-left: 0;
|
||||
}
|
||||
@@ -99,18 +196,47 @@
|
||||
}
|
||||
.card-details .card-details-header {
|
||||
margin: 0 -20px 5px;
|
||||
padding: 7px 20px;
|
||||
padding: 8px 20px;
|
||||
background: #ededed;
|
||||
border-bottom: 1px solid #dbdbdb;
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
z-index: 500;
|
||||
display: flow-root;
|
||||
min-height: 44px;
|
||||
}
|
||||
.card-details .card-details-header .card-number {
|
||||
color: #b3b3b3;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* Collapse toggle triangle */
|
||||
.card-details .card-details-header .card-collapse-toggle {
|
||||
float: left;
|
||||
font-size: 20px;
|
||||
padding: 7px 10px;
|
||||
margin-left: -10px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: #000;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-details .card-details-header .card-drag-handle {
|
||||
font-size: 20px;
|
||||
padding: 8px 10px;
|
||||
margin-right: 10px;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-details .card-details-header .close-card-details,
|
||||
.card-details .card-details-header .maximize-card-details,
|
||||
.card-details .card-details-header .minimize-card-details,
|
||||
@@ -128,11 +254,19 @@
|
||||
font-size: 24px;
|
||||
padding: 5px 10px 5px 10px;
|
||||
margin-right: -8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
transition: color 0.13s;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details-mobile-web {
|
||||
.card-details .card-details-header .close-card-details-mobile-web,
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
font-size: 24px;
|
||||
padding: 5px;
|
||||
margin-right: 40px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.card-details .card-details-header .card-copy-button {
|
||||
font-size: 17px;
|
||||
@@ -147,12 +281,44 @@
|
||||
.card-details .card-details-header .card-details-menu {
|
||||
font-size: 17px;
|
||||
padding: 10px;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.card-details .card-details-header .card-details-menu-mobile-web {
|
||||
font-size: 17px;
|
||||
padding: 10px;
|
||||
margin-right: 30px;
|
||||
}
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle,
|
||||
.card-details .card-details-header .card-zoom-in,
|
||||
.card-details .card-details-header .card-zoom-out {
|
||||
font-size: 24px;
|
||||
padding: 5px 10px 5px 10px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* Unify all card text to match title size */
|
||||
.card-details {
|
||||
font-size: 1em;
|
||||
}
|
||||
.card-details p,
|
||||
.card-details span,
|
||||
.card-details div,
|
||||
.card-details a,
|
||||
.card-details label,
|
||||
.card-details input,
|
||||
.card-details textarea,
|
||||
.card-details select,
|
||||
.card-details button,
|
||||
.card-details .card-details-item-title,
|
||||
.card-details .card-label,
|
||||
.card-details .viewer {
|
||||
font-size: inherit;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.card-details .card-details-header .card-details-watch {
|
||||
font-size: 17px;
|
||||
padding-left: 7px;
|
||||
@@ -160,9 +326,13 @@
|
||||
}
|
||||
.card-details .card-details-header .card-details-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.33em;
|
||||
font-size: 1.35em;
|
||||
margin: 7px 0 0;
|
||||
padding: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.card-details .card-details-header .linked-card-location {
|
||||
font-style: italic;
|
||||
@@ -177,10 +347,10 @@
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.card-details .card-details-header form.inlined-form .copied-tooltip {
|
||||
padding: 0px 10px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.card-details .card-details-header .card-details-list {
|
||||
font-size: 0.85em;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.card-details .card-details-header .card-details-list a.card-details-list-title {
|
||||
@@ -190,7 +360,7 @@
|
||||
display: inline-block;
|
||||
background: #e6e6e6;
|
||||
border-radius: 3px;
|
||||
padding: 0px 5px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
.card-details .card-details-header .copied-tooltip {
|
||||
margin-right: 10px;
|
||||
@@ -201,11 +371,13 @@
|
||||
}
|
||||
.card-details .card-description textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
.card-details .card-details-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 15px 0;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.card-details .card-details-items .card-details-item {
|
||||
margin-right: 0.5em;
|
||||
@@ -256,15 +428,28 @@
|
||||
position: fixed;
|
||||
resize: both;
|
||||
}
|
||||
|
||||
/* Override for mobile mode even on larger screens */
|
||||
body.mobile-mode .card-details {
|
||||
width: 100vw !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
.card-details-maximized {
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: calc(100% - 20px);
|
||||
will-change: flex-basis;
|
||||
overflow-y: scroll;
|
||||
overflow-x: scroll;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
background: #f7f7f7;
|
||||
border-radius: bottom 3px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
z-index: 100;
|
||||
animation: flexGrowIn 0.1s;
|
||||
box-shadow: 0 0 7px 0 #b3b3b3;
|
||||
@@ -307,19 +492,52 @@ input[type="submit"].attachment-add-link-submit {
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
.card-details {
|
||||
width: calc(100% - 1px);
|
||||
padding: 0px 20px 0px 20px;
|
||||
margin: 0px;
|
||||
width: 100% !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
transition: none;
|
||||
overflow-y: revert;
|
||||
overflow-x: revert;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 100 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Ensure card details are above everything on mobile */
|
||||
body.mobile-mode .card-details {
|
||||
z-index: 100 !important;
|
||||
width: 100vw !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
.card-details .card-details-canvas {
|
||||
width: 100%;
|
||||
padding-left: 0px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details {
|
||||
margin-right: 0px;
|
||||
display: block !important;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details-mobile-web {
|
||||
display: block !important;
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
display: block !important;
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
display: block !important;
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
.card-details .card-details-header .card-details-menu {
|
||||
margin-right: 40px;
|
||||
@@ -345,6 +563,62 @@ input[type="submit"].attachment-add-link-submit {
|
||||
.pop-over > .content-wrapper > .popup-container-depth-0 .card-details-header {
|
||||
margin: 0;
|
||||
}
|
||||
/* iPhone mobile: enlarge header buttons and increase spacing */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header {
|
||||
padding-right: 16px;
|
||||
}
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .maximize-card-details,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .minimize-card-details,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-details-menu-mobile-web,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-copy-mobile-button,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-mobile-desktop-toggle,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-in,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-out {
|
||||
font-size: 2em !important; /* 2x bigger */
|
||||
padding: 0.3em !important;
|
||||
margin-right: 0.75em !important; /* 2x space compared to default */
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
/* Avoid clipping of the close button on the right edge */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details {
|
||||
margin-right: 0.75em !important;
|
||||
}
|
||||
/* Enlarge the header title too */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-details-title {
|
||||
font-size: 1.2em !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile mode styles - apply when body has mobile-mode class regardless of screen size */
|
||||
body.mobile-mode .card-details {
|
||||
width: 100vw !important;
|
||||
padding: 0px !important;
|
||||
margin: 0px !important;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 100 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body.mobile-mode .card-details .card-details-canvas {
|
||||
width: 100% !important;
|
||||
padding: 0 15px !important;
|
||||
}
|
||||
|
||||
body.mobile-mode .card-details .card-details-header .close-card-details,
|
||||
body.mobile-mode .card-details .card-details-header .close-card-details-mobile-web {
|
||||
display: block !important;
|
||||
}
|
||||
.card-details-white {
|
||||
background: #fff !important;
|
||||
@@ -453,13 +727,15 @@ input[type="submit"].attachment-add-link-submit {
|
||||
.vote-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.vote-title .js-edit-date {
|
||||
align-self: baseline;
|
||||
margin-left: 5px;
|
||||
align-self: flex-start;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.vote-result {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.js-show-positive-votes {
|
||||
cursor: pointer;
|
||||
@@ -470,29 +746,33 @@ input[type="submit"].attachment-add-link-submit {
|
||||
.poker-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.poker-title .js-edit-date {
|
||||
align-self: baseline;
|
||||
margin-left: 5px;
|
||||
align-self: flex-start;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.poker-result {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
}
|
||||
.js-show-positive-poker-votes {
|
||||
cursor: pointer;
|
||||
}
|
||||
.poker-deck {
|
||||
display: grid;
|
||||
flex-direction: column;
|
||||
grid-auto-flow: row;
|
||||
text-align: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.poker-card-result {
|
||||
width: 32px;
|
||||
width: 34px;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
padding: 4px 2px 4px 2px;
|
||||
padding: 4px 2px;
|
||||
cursor: default;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.winner {
|
||||
font-weight: bold;
|
||||
@@ -503,6 +783,7 @@ input[type="submit"].attachment-add-link-submit {
|
||||
}
|
||||
.responsive-table {
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
}
|
||||
.poker-table {
|
||||
display: table;
|
||||
@@ -565,11 +846,15 @@ input[type="submit"].attachment-add-link-submit {
|
||||
margin: auto;
|
||||
margin-right: 10px;
|
||||
width: 100px;
|
||||
border-radius: 2px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
.estimation-add button {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin: auto;
|
||||
border-radius: 2px;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
.poker-card {
|
||||
width: 48px;
|
||||
@@ -588,6 +873,7 @@ input[type="submit"].attachment-add-link-submit {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.12s;
|
||||
}
|
||||
.poker-card .inner {
|
||||
display: table-cell;
|
||||
@@ -604,3 +890,167 @@ input[type="submit"].attachment-add-link-submit {
|
||||
transform: scale(1.01);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Sticker picker + card-detail stickers + due-complete toggle */
|
||||
.card-stickers-picker {
|
||||
max-width: 320px;
|
||||
}
|
||||
.card-sticker-select {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin: 2px;
|
||||
border-radius: 3px;
|
||||
font-size: 18px;
|
||||
color: #4d4d4d;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.card-sticker-select:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
.card-sticker-select.active {
|
||||
background: #d6ecd2;
|
||||
color: #3cb500;
|
||||
}
|
||||
.card-sticker-select .card-sticker-check {
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
right: 2px;
|
||||
font-size: 10px;
|
||||
color: #3cb500;
|
||||
}
|
||||
.card-stickers .card-sticker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin: 0 8px 4px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
.card-sticker .card-sticker-remove {
|
||||
margin-left: 3px;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
.card-sticker .card-sticker-remove:hover {
|
||||
color: #eb4646;
|
||||
}
|
||||
.card-due-complete-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Trello-style "mark complete" toggle, left of the card title */
|
||||
.card-details-complete-toggle {
|
||||
float: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* Title is 1.35em with 1.3 line-height (~28px box) and margin-top 7px; push
|
||||
the 13px checkbox down so it lands at the vertical middle of the title. */
|
||||
margin: 14px 10px 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Card locations: multiple locations, each editable with a remove button */
|
||||
.card-details-item-location .card-locations {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.card-details-item-location .card-location {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
border: 1px solid #e3e3e3;
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
.card-details-item-location .card-location-info {
|
||||
flex: 1;
|
||||
}
|
||||
.card-details-item-location .js-edit-location {
|
||||
cursor: pointer;
|
||||
}
|
||||
.card-details-item-location .card-location-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
.card-details-item-location .card-location-address {
|
||||
color: #4d4d4d;
|
||||
}
|
||||
.card-details-item-location .card-location-remove {
|
||||
margin-left: 8px;
|
||||
color: #8c8c8c;
|
||||
cursor: pointer;
|
||||
}
|
||||
.card-details-item-location .card-location-remove:hover {
|
||||
color: #eb4646;
|
||||
}
|
||||
|
||||
/* Sticker icon highlight styles (used instead of colour): taco pack => underline,
|
||||
pete pack => round (ring around the icon). Icon keeps its normal colour. */
|
||||
.card-sticker i.sticker-underline {
|
||||
border-bottom: 2px solid currentColor;
|
||||
padding-bottom: 1px;
|
||||
line-height: 1;
|
||||
}
|
||||
.card-sticker i.sticker-round {
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.6em;
|
||||
height: 1.6em;
|
||||
line-height: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* "Open map links at" provider selector in the location popup */
|
||||
.js-card-location-form .map-provider-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.js-card-location-form .map-provider-row .js-map-provider {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Opened card (card details popup) docks to the very top of the window and
|
||||
overlays the global + board header bars instead of opening below them. The
|
||||
base .pop-over is position:absolute with a small margin/top offset, which left
|
||||
the card's top behind the fixed header bars; fix it to the viewport top with a
|
||||
z-index above every header (headers top out around z-index 1000-2000). */
|
||||
.pop-over[data-popup="cardDetailsPopup"] {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
z-index: 100000 !important;
|
||||
}
|
||||
|
||||
/* Sticker highlight styles inside the sticker picker (mascot = underline,
|
||||
computer = round), matching how they render on the card. */
|
||||
.card-sticker-select i.sticker-underline {
|
||||
border-bottom: 2px solid currentColor;
|
||||
padding-bottom: 1px;
|
||||
line-height: 1;
|
||||
}
|
||||
.card-sticker-select i.sticker-round {
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.6em;
|
||||
height: 1.6em;
|
||||
line-height: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -5,49 +5,75 @@ template(name="cardDetails")
|
||||
|
||||
+attachmentViewer
|
||||
|
||||
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}'): .card-details-canvas
|
||||
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}' class='{{#if cardCollapsed}}card-details-collapsed{{/if}}'): .card-details-canvas
|
||||
.card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
|
||||
+inlinedForm(classNames="js-card-details-title")
|
||||
+editCardTitleForm
|
||||
else
|
||||
unless isMiniScreen
|
||||
unless isPopup
|
||||
a.fa.fa-times-thin.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
if canModifyCard
|
||||
if cardMaximized
|
||||
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
else
|
||||
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
span.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
|
||||
if cardCollapsed
|
||||
i.fa.fa-caret-right
|
||||
else
|
||||
i.fa.fa-caret-down
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
i.fa.fa-times-thin
|
||||
if cardMaximized
|
||||
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
else
|
||||
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
a.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
a.card-copy-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
if canModifyCard
|
||||
a.fa.fa-navicon.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
a.fa.fa-link.card-copy-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
class="fa-link"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
span.card-drag-handle.js-card-drag-handle(title="Drag card")
|
||||
i.fa.fa-arrows
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
else
|
||||
unless isPopup
|
||||
a.fa.fa-times-thin.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
if canModifyCard
|
||||
a.fa.fa-navicon.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
a.fa.fa-link.card-copy-mobile-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
class="fa-link"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
i.fa.fa-times-thin
|
||||
a.card-zoom-out.js-card-zoom-out(title="{{_ 'zoom-out'}}")
|
||||
i.fa.fa-search-minus
|
||||
a.card-zoom-in.js-card-zoom-in(title="{{_ 'zoom-in'}}")
|
||||
i.fa.fa-search-plus
|
||||
a.card-mobile-desktop-toggle.js-card-mobile-desktop-toggle(title="{{_ 'mobile-desktop-toggle'}}")
|
||||
if mobileMode
|
||||
i.fa.fa-desktop
|
||||
else
|
||||
i.fa.fa-mobile
|
||||
if cardMaximized
|
||||
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
else
|
||||
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
a.card-copy-mobile-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
if canModifyCard
|
||||
a.card-details-complete-toggle.js-toggle-due-complete(title="{{#if getDueComplete}}{{_ 'card-mark-incomplete'}}{{else}}{{_ 'card-mark-complete'}}{{/if}}")
|
||||
.materialCheckBox(class="{{#if getDueComplete}}is-checked{{/if}}")
|
||||
h2.card-details-title.js-card-title(
|
||||
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
|
||||
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{else}}js-card-title-drag-handle{{/if}}")
|
||||
+viewer
|
||||
if currentBoard.allowsCardNumber
|
||||
span.card-number
|
||||
| ##{getCardNumber}
|
||||
| ##{getCardNumber}
|
||||
= getTitle
|
||||
if isWatching
|
||||
i.card-details-watch.fa.fa-eye
|
||||
i.card-details-watch
|
||||
i.fa.fa-eye
|
||||
.card-details-path
|
||||
each parentList
|
||||
| >
|
||||
@@ -103,9 +129,60 @@ template(name="cardDetails")
|
||||
a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
.card-details-item.card-details-item-stickers
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sticky-note-o
|
||||
| {{_ 'stickers'}}
|
||||
.card-details-item-content.card-stickers
|
||||
each stickers
|
||||
span.card-sticker(title="{{#if name}}{{name}}{{else}}{{icon}}{{/if}}")
|
||||
i.fa(class="fa-{{icon}} {{#if highlight}}sticker-{{highlight}}{{/if}}")
|
||||
if canModifyCard
|
||||
a.card-sticker-remove.js-remove-sticker(data-index="{{@index}}" title="{{_ 'remove-sticker'}}")
|
||||
i.fa.fa-times
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-add-stickers(title="{{_ 'add-sticker'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
.card-details-item.card-details-item-location
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-map-marker
|
||||
| {{_ 'location'}}
|
||||
.card-details-item-content.card-locations
|
||||
each getLocations
|
||||
.card-location
|
||||
.card-location-info(class="{{#if canModifyCard}}js-edit-location{{/if}}" data-location-id="{{_id}}")
|
||||
if name
|
||||
.card-location-name= name
|
||||
if address
|
||||
.card-location-address= address
|
||||
if hasCoordinates
|
||||
a.card-location-map(href=mapUrl target="_blank" rel="noopener noreferrer")
|
||||
i.fa.fa-map-o
|
||||
= ' '
|
||||
| {{_ 'location-open-map'}}
|
||||
if canModifyCard
|
||||
a.card-location-remove.js-remove-location(data-location-id="{{_id}}" title="{{_ 'delete'}}")
|
||||
i.fa.fa-times
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-add-location(title="{{_ 'add-location'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
if currentBoard.hasAnyAllowsDate
|
||||
hr
|
||||
|
||||
.card-details-item.card-details-item-date-format
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-calendar
|
||||
| {{_ 'date-format'}}
|
||||
.card-details-item-content
|
||||
select.js-date-format-selector
|
||||
option(value="YYYY-MM-DD" selected="{{#if isDateFormat 'YYYY-MM-DD'}}selected{{/if}}") {{_ 'date-format-yyyy-mm-dd'}}
|
||||
option(value="DD-MM-YYYY" selected="{{#if isDateFormat 'DD-MM-YYYY'}}selected{{/if}}") {{_ 'date-format-dd-mm-yyyy'}}
|
||||
option(value="MM-DD-YYYY" selected="{{#if isDateFormat 'MM-DD-YYYY'}}selected{{/if}}") {{_ 'date-format-mm-dd-yyyy'}}
|
||||
|
||||
if currentBoard.allowsReceivedDate
|
||||
.card-details-item.card-details-item-received
|
||||
h3.card-details-item-title
|
||||
@@ -135,7 +212,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsDueDate
|
||||
.card-details-item.card-details-item-due
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sign-in
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'card-due'}}
|
||||
if getDue
|
||||
+cardDueDate
|
||||
@@ -248,7 +325,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsCardSortingByNumber
|
||||
.card-details-item.card-details-sort-order
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sort
|
||||
i.fa.fa-sort-numeric-asc
|
||||
| {{_ 'sort'}}
|
||||
if canModifyCard
|
||||
+inlinedForm(classNames="js-card-details-sort")
|
||||
@@ -264,7 +341,7 @@ template(name="cardDetails")
|
||||
i.fa.fa-list
|
||||
| {{_ 'list'}}
|
||||
select.js-select-card-details-lists(disabled="{{#unless canModifyCard}}disabled{{/unless}}")
|
||||
each currentBoard.lists
|
||||
each currentSwimlaneListsSorted
|
||||
option(value="{{_id}}" selected="{{#if isCurrentListId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
//.card-details-items
|
||||
@@ -287,7 +364,7 @@ template(name="cardDetails")
|
||||
hr
|
||||
.card-details-item.card-details-item-customfield
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-list-alt
|
||||
i.fa.fa-list
|
||||
= definition.name
|
||||
+cardCustomField
|
||||
|
||||
@@ -298,7 +375,7 @@ template(name="cardDetails")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleCustomFieldsGridButton")
|
||||
label.toggle-label(for="toggleCustomFieldsGridButton")
|
||||
a.fa.fa-plus.js-custom-fields.card-details-item.custom-fields(title="{{_ 'custom-fields'}}")
|
||||
a.js-custom-fields.card-details-item.custom-fields(title="{{_ 'custom-fields'}}")
|
||||
|
||||
if getVoteQuestion
|
||||
hr
|
||||
@@ -317,7 +394,8 @@ template(name="cardDetails")
|
||||
.card-label.card-label-green {{ voteCountPositive }}
|
||||
.card-label.card-label-red {{ voteCountNegative }}
|
||||
unless ($and currentBoard.isPublic voteAllowNonBoardMembers )
|
||||
.card-label.card-label-gray {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
.card-label.card-label-gray
|
||||
| {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
+viewer
|
||||
= getVoteQuestion
|
||||
if showVotingButtons
|
||||
@@ -327,7 +405,7 @@ template(name="cardDetails")
|
||||
| {{_ 'vote-for-it'}}
|
||||
button.card-details-red.js-vote.js-vote-negative(class="{{#if $eq voteState false}}voted{{/if}}")
|
||||
if $eq voteState false
|
||||
i.fa.fa-thumbs-down
|
||||
| 👎
|
||||
| {{_ 'vote-against'}}
|
||||
|
||||
if getPokerQuestion
|
||||
@@ -343,7 +421,8 @@ template(name="cardDetails")
|
||||
.poker-result
|
||||
if expiredPoker
|
||||
unless ($and currentBoard.isPublic pokerAllowNonBoardMembers )
|
||||
.card-label.card-label-gray {{ pokerCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
.card-label.card-label-gray
|
||||
| {{ pokerCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
if showPlanningPokerButtons
|
||||
.poker-result
|
||||
.poker-deck
|
||||
@@ -535,14 +614,14 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsDescriptionTitle
|
||||
hr
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-align-left
|
||||
i.fa.fa-file-text-o
|
||||
| {{_ 'description'}}
|
||||
if currentBoard.allowsDescriptionText
|
||||
+inlinedCardDescription(classNames="card-description js-card-description")
|
||||
+descriptionForm
|
||||
.edit-controls.clearfix
|
||||
button.primary(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
else
|
||||
if currentBoard.allowsDescriptionText
|
||||
a.js-open-inlined-form(title="{{_ 'edit'}}" value=title)
|
||||
@@ -589,22 +668,23 @@ template(name="cardDetails")
|
||||
+attachmentGallery
|
||||
hr
|
||||
|
||||
unless currentUser.isNoComments
|
||||
if currentBoard.allowsComments
|
||||
.comment-title
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-comment-o
|
||||
| {{_ 'comments'}}
|
||||
|
||||
if currentBoard.allowsComments
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isNoComments
|
||||
+commentForm
|
||||
+comments
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isNoComments
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
+commentForm
|
||||
+comments
|
||||
hr
|
||||
|
||||
.card-details-right
|
||||
|
||||
unless currentUser.isNoComments
|
||||
if currentUser.isBoardAdmin
|
||||
.activity-title
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-history
|
||||
@@ -617,7 +697,7 @@ template(name="cardDetails")
|
||||
input.toggle-switch(type="checkbox" id="toggleShowActivitiesCard")
|
||||
label.toggle-label(for="toggleShowActivitiesCard")
|
||||
|
||||
unless currentUser.isNoComments
|
||||
if currentUser.isBoardAdmin
|
||||
if isLoaded.get
|
||||
if isLinkedCard
|
||||
+activities(card=this mode="linkedcard")
|
||||
@@ -627,168 +707,339 @@ template(name="cardDetails")
|
||||
+activities(card=this mode="card")
|
||||
|
||||
template(name="editCardTitleForm")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
a(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
textarea.js-edit-card-title(rows='1' autofocus dir="auto")
|
||||
= getTitle
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-title-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
|
||||
template(name="editCardRequesterForm")
|
||||
input.js-edit-card-requester(type='text' autofocus value=getRequestedBy dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-requester-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
|
||||
template(name="editCardAssignerForm")
|
||||
input.js-edit-card-assigner(type='text' autofocus value=getAssignedBy dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-assigner-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
|
||||
template(name="editCardSortOrderForm")
|
||||
input.js-edit-card-sort(type='text' autofocus value=sort dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-sort-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
|
||||
template(name="cardDetailsActionsPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-toggle-watch-card
|
||||
if isWatching
|
||||
i.fa.fa-eye
|
||||
| {{_ 'unwatch'}}
|
||||
else
|
||||
i.fa.fa-eye-slash
|
||||
| {{_ 'watch'}}
|
||||
hr
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
//li: a.js-members {{_ 'card-edit-members'}}
|
||||
//li: a.js-labels {{_ 'card-edit-labels'}}
|
||||
//li: a.js-attachments {{_ 'card-edit-attachments'}}
|
||||
li
|
||||
a.js-start-voting
|
||||
i.fa.fa-thumbs-up
|
||||
| {{_ 'card-edit-voting'}}
|
||||
li
|
||||
a.js-start-planning-poker
|
||||
i.fa.fa-thumbs-up
|
||||
| {{_ 'card-edit-planning-poker'}}
|
||||
if currentUser.isBoardAdmin
|
||||
if currentUser
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-toggle-watch-card
|
||||
if isWatching
|
||||
i.fa.fa-eye
|
||||
| {{_ 'unwatch'}}
|
||||
else
|
||||
i.fa.fa-eye
|
||||
| {{_ 'watch'}}
|
||||
hr
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
//li: a.js-members {{_ 'card-edit-members'}}
|
||||
//li: a.js-labels {{_ 'card-edit-labels'}}
|
||||
//li: a.js-attachments {{_ 'card-edit-attachments'}}
|
||||
li
|
||||
a.js-custom-fields
|
||||
i.fa.fa-list-alt
|
||||
| {{_ 'card-edit-custom-fields'}}
|
||||
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
|
||||
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
|
||||
//li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
|
||||
//li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
|
||||
li
|
||||
a.js-spent-time
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'editCardSpentTimePopup-title'}}
|
||||
li
|
||||
a.js-set-card-color
|
||||
i.fa.fa-paint-brush
|
||||
| {{_ 'setCardColorPopup-title'}}
|
||||
hr
|
||||
a.js-start-voting
|
||||
i.fa.fa-thumbs-up
|
||||
| {{_ 'card-edit-voting'}}
|
||||
li
|
||||
a.js-start-planning-poker
|
||||
i.fa.fa-thumbs-up
|
||||
| {{_ 'card-edit-planning-poker'}}
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-custom-fields
|
||||
i.fa.fa-list
|
||||
| {{_ 'card-edit-custom-fields'}}
|
||||
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
|
||||
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
|
||||
//li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
|
||||
//li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
|
||||
li
|
||||
a.js-spent-time
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'editCardSpentTimePopup-title'}}
|
||||
li
|
||||
a.js-set-card-color
|
||||
i.fa.fa-paint-brush
|
||||
| {{_ 'setCardColorPopup-title'}}
|
||||
li
|
||||
a.js-toggle-show-list-on-minicard
|
||||
if showListOnMinicard
|
||||
i.fa.fa-eye
|
||||
| {{_ 'hide-list-on-minicard'}}
|
||||
else
|
||||
i.fa.fa-eye
|
||||
| {{_ 'show-list-on-minicard'}}
|
||||
if canModifyCard
|
||||
hr
|
||||
else
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-export-card
|
||||
i.fa.fa-share-alt
|
||||
i.fa.fa-upload
|
||||
| {{_ 'export-card'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-move-card-to-top
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-move-card
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
li
|
||||
a.js-copy-card
|
||||
i.fa.fa-copy
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-copy-checklist-cards
|
||||
i.fa.fa-copy
|
||||
i.fa.fa-copy
|
||||
| {{_ 'copyManyCardsPopup-title'}}
|
||||
unless archived
|
||||
if currentUser
|
||||
unless canModifyCard
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-move-card-to-top
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-move-card
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
li
|
||||
a.js-copy-card
|
||||
i.fa.fa-clipboard
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-copy-checklist-cards
|
||||
i.fa.fa-copy
|
||||
| {{_ 'copyManyCardsPopup-title'}}
|
||||
unless archived
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-archive
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archive-card'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
| {{_ 'cardMorePopup-title'}}
|
||||
if canModifyCard
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-archive
|
||||
i.fa.fa-arrow-right
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archive-card'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
i.fa.fa-link
|
||||
| {{_ 'cardMorePopup-title'}}
|
||||
a.js-move-card-to-top
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-move-card
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
li
|
||||
a.js-copy-card
|
||||
i.fa.fa-clipboard
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-copy-checklist-cards
|
||||
i.fa.fa-copy
|
||||
| {{_ 'copyManyCardsPopup-title'}}
|
||||
unless archived
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-archive
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archive-card'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
| {{_ 'cardMorePopup-title'}}
|
||||
|
||||
template(name="exportCardPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a(href="{{exportUrlCardPDF}}",, download="{{exportFilenameCardPDF}}")
|
||||
i.fa.fa-share-alt
|
||||
a(href="{{exportUrlCardPDF}}", download="{{exportFilenameCardPDF}}")
|
||||
i.fa.fa-file-pdf-o
|
||||
| {{_ 'export-card-pdf'}}
|
||||
hr
|
||||
p.quiet {{_ 'export-card-excel-fields'}}
|
||||
ul.pop-over-list
|
||||
each excelExportFields
|
||||
li(class="{{#if checked}}active{{/if}}")
|
||||
a.js-excel-field-toggle(data-field="{{field}}" href="#")
|
||||
i.fa.fa-check
|
||||
| {{_ label}}
|
||||
ul.pop-over-list
|
||||
li
|
||||
a(href="{{exportUrlCardExcel}}", download="{{exportFilenameCardExcel}}")
|
||||
i.fa.fa-file-excel-o
|
||||
| {{_ 'download'}}
|
||||
|
||||
template(name="moveCardPopup")
|
||||
+copyAndMoveCard
|
||||
label(for='move-card-title') {{_ 'title'}}:
|
||||
input#move-card-title.full-line(type="text" value="{{getTitle}}" autofocus)
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
||||
template(name="copyCardPopup")
|
||||
label(for='copy-card-title') {{_ 'title'}}:
|
||||
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
|
||||
= getTitle
|
||||
+copyAndMoveCard
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
||||
template(name="copyManyCardsPopup")
|
||||
label(for='copy-checklist-cards-title') {{_ 'copyManyCardsPopup-instructions'}}:
|
||||
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
|
||||
| {{_ 'copyManyCardsPopup-format'}}
|
||||
+copyAndMoveCard
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
||||
template(name="convertChecklistItemToCardPopup")
|
||||
label(for='convert-checklist-item-to-card-title') {{_ 'title'}}:
|
||||
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
|
||||
= item.title
|
||||
+copyAndMoveCard
|
||||
|
||||
template(name="copyAndMoveCard")
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
@@ -799,13 +1050,13 @@ template(name="cardMembersPopup")
|
||||
each members
|
||||
li.item(class="{{#if isCardMember}}active{{/if}}")
|
||||
a.name.js-select-member(href="#")
|
||||
+userAvatar(userId=user._id)
|
||||
+userAvatar(userId=userId)
|
||||
span.full-name
|
||||
= user.profile.fullname
|
||||
| (<span class="username">{{ user.username }}</span>)
|
||||
= userData.profile.fullname
|
||||
if userData.username
|
||||
| (#{userData.username})
|
||||
if isCardMember
|
||||
i.fa.fa-check
|
||||
|
||||
template(name="cardAssigneesPopup")
|
||||
input.card-assignees-filter(type="text" placeholder="{{_ 'search'}}")
|
||||
unless currentUser.isWorker
|
||||
@@ -813,10 +1064,11 @@ template(name="cardAssigneesPopup")
|
||||
each members
|
||||
li.item(class="{{#if isCardAssignee}}active{{/if}}")
|
||||
a.name.js-select-assignee(href="#")
|
||||
+userAvatar(userId=user._id)
|
||||
+userAvatar(userId=userId)
|
||||
span.full-name
|
||||
= user.profile.fullname
|
||||
| (<span class="username">{{ user.username }}</span>)
|
||||
= userData.profile.fullname
|
||||
if userData.username
|
||||
| (#{userData.username})
|
||||
if isCardAssignee
|
||||
i.fa.fa-check
|
||||
if currentUser.isWorker
|
||||
@@ -826,10 +1078,10 @@ template(name="cardAssigneesPopup")
|
||||
+userAvatar(userId=currentUser._id)
|
||||
span.full-name
|
||||
= currentUser.profile.fullname
|
||||
| (<span class="username">{{ currentUser.username }}</span>)
|
||||
if currentUser.username
|
||||
| (#{currentUser.username})
|
||||
if currentUser.isCardAssignee
|
||||
i.fa.fa-check
|
||||
|
||||
template(name="cardAssigneePopup")
|
||||
.board-assignee-menu
|
||||
.mini-profile-info
|
||||
@@ -852,7 +1104,7 @@ template(name="cardMorePopup")
|
||||
span.clearfix
|
||||
span {{_ 'link-card'}}
|
||||
= ' '
|
||||
i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
input.inline-input(type="text" id="cardURL" readonly value="{{ originRelativeUrl }}" autofocus="autofocus")
|
||||
button.js-copy-card-link-to-clipboard(class="btn" id="clipboard") {{_ 'copy-card-link-to-clipboard'}}
|
||||
.copied-tooltip {{_ 'copied'}}
|
||||
@@ -884,19 +1136,20 @@ template(name="cardMorePopup")
|
||||
option(value="{{_id}}") {{title}}
|
||||
br
|
||||
| {{_ 'added'}}
|
||||
span.date(title=card.createdAt) {{ moment createdAt 'LLL' }}
|
||||
span.date(title=card.createdAt) {{ displayDate createdAt 'LLL' }}
|
||||
if currentUser.isBoardAdmin
|
||||
a.js-delete(title="{{_ 'card-delete-notice'}}") {{_ 'delete'}}
|
||||
|
||||
template(name="setCardColorPopup")
|
||||
form.edit-label
|
||||
.palette-colors: each colors
|
||||
unless $eq color 'white'
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
.palette-colors
|
||||
each colors
|
||||
unless $eq color 'white'
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
|
||||
template(name="cardDeletePopup")
|
||||
p {{_ "card-delete-pop"}}
|
||||
@@ -928,7 +1181,7 @@ template(name="cardStartVotingPopup")
|
||||
.materialCheckBox#vote-public(name="vote-public" class="{{#if votePublic}}is-checked{{/if}}")
|
||||
span {{_ 'vote-public'}}
|
||||
.check-div.flex
|
||||
i.fa.fa-hourglass-end
|
||||
i.fa.fa-clock-o
|
||||
a.js-end-date
|
||||
span
|
||||
| {{_ 'card-end'}}
|
||||
@@ -974,7 +1227,7 @@ template(name="cardStartPlanningPokerPopup")
|
||||
.materialCheckBox#poker-allow-non-members(name="poker-allow-non-members" class="{{#if pokerAllowNonBoardMembers}}is-checked{{/if}}")
|
||||
span {{_ 'allowNonBoardMembers'}}
|
||||
.check-div.flex
|
||||
i.fa.fa-hourglass-end
|
||||
i.fa.fa-clock-o
|
||||
a.js-end-date
|
||||
span
|
||||
| {{_ 'card-end'}}
|
||||
@@ -987,3 +1240,39 @@ template(name="cardStartPlanningPokerPopup")
|
||||
if getPokerQuestion
|
||||
if currentUser.isBoardAdmin
|
||||
button.js-remove-poker.negate.wide.right {{_ 'delete'}}
|
||||
|
||||
template(name="cardLocationsPopup")
|
||||
form.js-card-location-form
|
||||
label
|
||||
| {{_ 'location-detect-from-map'}}
|
||||
input.js-location-map-link(type="text" placeholder="https://..." autofocus)
|
||||
button.primary.js-detect-location(type="button") {{_ 'location-detect'}}
|
||||
if detectMessage
|
||||
p.quiet.js-detect-message {{detectMessage}}
|
||||
hr
|
||||
label
|
||||
| {{_ 'location-name'}}
|
||||
input.js-location-name(type="text" value="{{location.name}}")
|
||||
label
|
||||
| {{_ 'location-address'}}
|
||||
input.js-location-address(type="text" value="{{location.address}}")
|
||||
label
|
||||
| {{_ 'location-latitude'}}
|
||||
input.js-location-latitude(type="number" step="any" value="{{location.latitude}}")
|
||||
label
|
||||
| {{_ 'location-longitude'}}
|
||||
input.js-location-longitude(type="number" step="any" value="{{location.longitude}}")
|
||||
button.primary.confirm.js-submit-location(type="submit") {{_ 'save'}}
|
||||
if location._id
|
||||
button.negate.wide.right.js-delete-location(type="button") {{_ 'delete'}}
|
||||
hr
|
||||
.map-provider-row
|
||||
span.map-provider-label {{_ 'location-open-map-at'}}
|
||||
select.js-map-provider
|
||||
option(value="openstreetmap" selected="{{#if isMapProvider 'openstreetmap'}}selected{{/if}}") OpenStreetMap
|
||||
option(value="google" selected="{{#if isMapProvider 'google'}}selected{{/if}}") Google Maps
|
||||
option(value="bing" selected="{{#if isMapProvider 'bing'}}selected{{/if}}") Bing Maps
|
||||
option(value="apple" selected="{{#if isMapProvider 'apple'}}selected{{/if}}") Apple Maps
|
||||
button.primary.js-save-map-provider(type="button") {{_ 'save'}}
|
||||
if mapSavedMessage
|
||||
p.quiet.js-map-saved {{mapSavedMessage}}
|
||||
+1828
-1218
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,7 @@
|
||||
template(name="cardStickersPopup")
|
||||
.card-stickers-picker.clearfix
|
||||
each stickerIcons
|
||||
a.card-sticker-select.js-select-sticker(class="{{#if isSelected}}active{{/if}}" title="{{stickerTitle}}")
|
||||
i.fa(class="fa-{{icon}} {{#if highlight}}sticker-{{highlight}}{{/if}}")
|
||||
if isSelected
|
||||
i.fa.fa-check.card-sticker-check
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { STICKER_PICKER } from '/models/metadata/stickers';
|
||||
|
||||
// Picker popup that lets a user add/remove stickers on a card. Each entry is
|
||||
// { icon, highlight?, name? }: the plain Font Awesome icons plus the mascot
|
||||
// (underlined) and computer (ringed) highlighted stickers. Opened from the card
|
||||
// details stickers section with the card as the data context.
|
||||
|
||||
Template.cardStickersPopup.onCreated(function () {
|
||||
const data = Template.currentData();
|
||||
this.cardId = data && data._id;
|
||||
});
|
||||
|
||||
function pickerCard(tpl) {
|
||||
return ReactiveCache.getCard(tpl.cardId);
|
||||
}
|
||||
|
||||
Template.cardStickersPopup.helpers({
|
||||
stickerIcons() {
|
||||
return STICKER_PICKER;
|
||||
},
|
||||
isSelected() {
|
||||
const card = pickerCard(Template.instance());
|
||||
return !!(card && card.hasSticker(this.icon, this.highlight));
|
||||
},
|
||||
stickerTitle() {
|
||||
return this.name || this.icon;
|
||||
},
|
||||
});
|
||||
|
||||
Template.cardStickersPopup.events({
|
||||
'click .js-select-sticker'(event) {
|
||||
event.preventDefault();
|
||||
const card = pickerCard(Template.instance());
|
||||
if (card) {
|
||||
card.toggleSticker(this.icon, this.highlight, this.name);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
template(name="editCardSpentTime")
|
||||
template(name="editCardSpentTimePopup")
|
||||
.edit-card-time
|
||||
form.edit-time
|
||||
.fields
|
||||
@@ -13,10 +13,10 @@ template(name="editCardSpentTime")
|
||||
button.primary.wide.left.js-submit-time(type="submit") {{_ 'save'}}
|
||||
button.js-delete-time.negate.wide.right {{_ 'delete'}}
|
||||
|
||||
template(name="timeBadge")
|
||||
template(name="cardSpentTime")
|
||||
if canModifyCard
|
||||
a.js-edit-time.card-time(title="{{showTitle}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
|
||||
| {{showTime}}
|
||||
a.js-edit-time.card-time(title="{{_ 'time'}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
|
||||
| ⏱️ {{showTime}}
|
||||
else
|
||||
a.card-time(title="{{showTitle}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
|
||||
| {{showTime}}
|
||||
a.card-time(title="{{_ 'time'}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
|
||||
| ⏱️ {{showTime}}
|
||||
@@ -1,85 +1,91 @@
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import Cards from '/models/cards';
|
||||
import { getCurrentCardIdFromContext } from '/client/lib/currentCard';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'editCardSpentTime';
|
||||
function getCardId() {
|
||||
return getCurrentCardIdFromContext();
|
||||
}
|
||||
|
||||
Template.editCardSpentTimePopup.onCreated(function () {
|
||||
this.error = new ReactiveVar('');
|
||||
this.card = Cards.findOne(getCardId());
|
||||
});
|
||||
|
||||
Template.editCardSpentTimePopup.helpers({
|
||||
error() {
|
||||
return Template.instance().error;
|
||||
},
|
||||
onCreated() {
|
||||
this.error = new ReactiveVar('');
|
||||
this.card = this.data();
|
||||
card() {
|
||||
return Cards.findOne(getCardId());
|
||||
},
|
||||
toggleOvertime() {
|
||||
this.card.setIsOvertime(!this.card.getIsOvertime());
|
||||
getIsOvertime() {
|
||||
const card = Cards.findOne(getCardId());
|
||||
return card?.getIsOvertime ? card.getIsOvertime() : false;
|
||||
},
|
||||
});
|
||||
|
||||
Template.editCardSpentTimePopup.events({
|
||||
//TODO : need checking this portion
|
||||
'submit .edit-time'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const card = Cards.findOne(getCardId());
|
||||
if (!card) return;
|
||||
|
||||
const spentTime = parseFloat(evt.target.time.value);
|
||||
let isOvertime = false;
|
||||
if ($('#overtime').attr('class').indexOf('is-checked') >= 0) {
|
||||
isOvertime = true;
|
||||
}
|
||||
if (spentTime >= 0) {
|
||||
card.setSpentTime(spentTime);
|
||||
card.setIsOvertime(isOvertime);
|
||||
Popup.back();
|
||||
} else {
|
||||
tpl.error.set('invalid-time');
|
||||
evt.target.time.focus();
|
||||
}
|
||||
},
|
||||
'click .js-delete-time'(evt) {
|
||||
evt.preventDefault();
|
||||
const card = Cards.findOne(getCardId());
|
||||
if (!card) return;
|
||||
card.setSpentTime(null);
|
||||
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');
|
||||
},
|
||||
storeTime(spentTime, isOvertime) {
|
||||
this.card.setSpentTime(spentTime);
|
||||
this.card.setIsOvertime(isOvertime);
|
||||
},
|
||||
deleteTime() {
|
||||
this.card.setSpentTime(null);
|
||||
this.card.setIsOvertime(false);
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
//TODO : need checking this portion
|
||||
'submit .edit-time'(evt) {
|
||||
evt.preventDefault();
|
||||
});
|
||||
|
||||
const spentTime = parseFloat(evt.target.time.value);
|
||||
//const isOvertime = this.card.getIsOvertime();
|
||||
let isOvertime = false;
|
||||
if ($('#overtime').attr('class').indexOf('is-checked') >= 0) {
|
||||
isOvertime = true;
|
||||
}
|
||||
if (spentTime >= 0) {
|
||||
this.storeTime(spentTime, isOvertime);
|
||||
Popup.back();
|
||||
} else {
|
||||
this.error.set('invalid-time');
|
||||
evt.target.time.focus();
|
||||
}
|
||||
},
|
||||
'click .js-delete-time'(evt) {
|
||||
evt.preventDefault();
|
||||
this.deleteTime();
|
||||
Popup.back();
|
||||
},
|
||||
'click a.js-toggle-overtime': this.toggleOvertime,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('editCardSpentTimePopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'timeBadge';
|
||||
},
|
||||
onCreated() {
|
||||
const self = this;
|
||||
self.time = ReactiveVar();
|
||||
},
|
||||
Template.cardSpentTime.helpers({
|
||||
showTitle() {
|
||||
if (this.data().getIsOvertime()) {
|
||||
const card = Cards.findOne(this._id) || this;
|
||||
if (card.getIsOvertime && card.getIsOvertime()) {
|
||||
return `${TAPi18n.__(
|
||||
'overtime',
|
||||
)} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
|
||||
} else {
|
||||
)} ${card.getSpentTime()} ${TAPi18n.__('hours')}`;
|
||||
} else if (card.getSpentTime) {
|
||||
return `${TAPi18n.__(
|
||||
'card-spent',
|
||||
)} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
|
||||
)} ${card.getSpentTime()} ${TAPi18n.__('hours')}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
showTime() {
|
||||
return this.data().getSpentTime();
|
||||
const card = Cards.findOne(this._id) || this;
|
||||
return card.getSpentTime ? card.getSpentTime() : '';
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-edit-time': Popup.open('editCardSpentTime'),
|
||||
},
|
||||
];
|
||||
getIsOvertime() {
|
||||
const card = Cards.findOne(this._id) || this;
|
||||
return card.getIsOvertime ? card.getIsOvertime() : false;
|
||||
},
|
||||
}).register('cardSpentTime');
|
||||
});
|
||||
|
||||
Template.cardSpentTime.events({
|
||||
'click .js-edit-time': Popup.open('editCardSpentTime'),
|
||||
});
|
||||
@@ -37,10 +37,12 @@ textarea.js-edit-checklist-item {
|
||||
.checklist-progress-bar-container .checklist-progress-bar {
|
||||
width: 80%;
|
||||
height: 10px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
|
||||
color: #fff !important;
|
||||
background-color: #2196f3 !important;
|
||||
color: #fff;
|
||||
background-color: #666;
|
||||
padding: 0.01em 16px;
|
||||
border-radius: 16px;
|
||||
height: 100%;
|
||||
|
||||
@@ -4,7 +4,8 @@ template(name="checklists")
|
||||
i.fa.fa-check
|
||||
| {{_ 'checklists'}}
|
||||
if canModifyCard
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId position="top")
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId
|
||||
position="top")
|
||||
+addChecklistItemForm
|
||||
else
|
||||
a.add-checklist-top.js-open-inlined-form(title="{{_ 'add-checklist'}}")
|
||||
@@ -33,7 +34,7 @@ template(name="checklists")
|
||||
template(name="checklistDetail")
|
||||
.js-checklist.checklist.nodragscroll
|
||||
+inlinedForm(classNames="js-edit-checklist-title" checklist = checklist)
|
||||
+editChecklistItemForm(checklist = checklist)
|
||||
+editChecklistForm(checklist = checklist)
|
||||
else
|
||||
.checklist-title
|
||||
span
|
||||
@@ -62,6 +63,10 @@ template(name="checklistDeletePopup")
|
||||
p {{_ 'confirm-checklist-delete-popup'}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="checklistItemDeletePopup")
|
||||
p {{_ 'confirm-checklist-item-delete-popup'}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="addChecklistItemForm")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
@@ -80,6 +85,19 @@ template(name="addChecklistItemForm")
|
||||
label.toggle-label(for="toggleNewlineBecomesNewChecklistItemOriginOrder")
|
||||
| {{_ 'originOrder'}}
|
||||
|
||||
template(name="editChecklistForm")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
textarea.js-edit-checklist-item(rows='1' autofocus dir="auto")
|
||||
if $eq type 'item'
|
||||
= item.title
|
||||
else
|
||||
= checklist.title
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
|
||||
span(title=createdAt) {{ displayDate createdAt }}
|
||||
|
||||
template(name="editChecklistItemForm")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
@@ -91,7 +109,7 @@ template(name="editChecklistItemForm")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
|
||||
span(title=createdAt) {{ moment createdAt }}
|
||||
span(title=createdAt) {{ displayDate createdAt }}
|
||||
if canModifyCard
|
||||
a.js-delete-checklist-item {{_ "delete"}}...
|
||||
a.js-convert-checklist-item-to-card
|
||||
@@ -166,6 +184,15 @@ template(name="checklistActionsPopup")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleHideAllChecklistItems_{{checklist._id}}")
|
||||
label.toggle-label(for="toggleHideAllChecklistItems_{{checklist._id}}")
|
||||
a.js-show-checklist-at-minicard
|
||||
i.fa.fa-th-list
|
||||
| {{_ "show-on-minicard"}} ...
|
||||
.material-toggle-switch(title="{{_ 'show-on-minicard'}}")
|
||||
if checklist.showChecklistAtMinicard
|
||||
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}" checked="checked")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}")
|
||||
label.toggle-label(for="toggleShowChecklistAtMinicard_{{checklist._id}}")
|
||||
|
||||
template(name="copyChecklistPopup")
|
||||
+copyAndMoveChecklist
|
||||
|
||||
@@ -2,10 +2,15 @@ import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import Cards from '/models/cards';
|
||||
import Boards from '/models/boards';
|
||||
import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard';
|
||||
import ChecklistItems from '/models/checklistItems';
|
||||
import Checklists from '/models/checklists';
|
||||
import { BoardSwimlaneListCardDialog } from '/client/lib/dialogWithBoardSwimlaneListCard';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import autosize from 'autosize';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
const { calculateIndexData, capitalize } = Utils;
|
||||
// SubsManager removed for Meteor 3 migration
|
||||
const { calculateIndexData } = Utils;
|
||||
|
||||
function initSorting(items) {
|
||||
items.sortable({
|
||||
@@ -45,55 +50,66 @@ function initSorting(items) {
|
||||
});
|
||||
}
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
const self = this;
|
||||
self.itemsDom = this.$('.js-checklist-items');
|
||||
initSorting(self.itemsDom);
|
||||
self.itemsDom.mousedown(function (evt) {
|
||||
evt.stopPropagation();
|
||||
});
|
||||
Template.checklistDetail.onRendered(function () {
|
||||
const tpl = this;
|
||||
tpl.itemsDom = this.$('.js-checklist-items');
|
||||
initSorting(tpl.itemsDom);
|
||||
tpl.itemsDom.mousedown(function (evt) {
|
||||
evt.stopPropagation();
|
||||
});
|
||||
|
||||
function userIsMember() {
|
||||
return ReactiveCache.getCurrentUser()?.isBoardMember();
|
||||
}
|
||||
function userIsMember() {
|
||||
return ReactiveCache.getCurrentUser()?.isBoardMember();
|
||||
}
|
||||
|
||||
// Disable sorting if the current user is not a board member
|
||||
self.autorun(() => {
|
||||
const $itemsDom = $(self.itemsDom);
|
||||
if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
|
||||
$(self.itemsDom).sortable('option', 'disabled', !userIsMember());
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$(self.itemsDom).sortable({
|
||||
handle: 'span.fa.checklistitem-handle',
|
||||
});
|
||||
}
|
||||
// Disable sorting if the current user is not a board member
|
||||
tpl.autorun(() => {
|
||||
const $itemsDom = $(tpl.itemsDom);
|
||||
if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
|
||||
$(tpl.itemsDom).sortable('option', 'disabled', !userIsMember());
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$(tpl.itemsDom).sortable({
|
||||
handle: 'span.fa.checklistitem-handle',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.checklistDetail.helpers({
|
||||
/** returns the finished percent of the checklist */
|
||||
finishedPercent() {
|
||||
const ret = this.data().checklist.finishedPercent();
|
||||
const ret = this.checklist.finishedPercent();
|
||||
return ret;
|
||||
},
|
||||
}).register('checklistDetail');
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
addChecklist(event) {
|
||||
Template.checklists.helpers({
|
||||
checklists() {
|
||||
const card = ReactiveCache.getCard(this.cardId);
|
||||
if (!card || typeof card.checklists !== 'function') {
|
||||
return [];
|
||||
}
|
||||
const ret = card.checklists();
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
|
||||
Template.checklists.events({
|
||||
'click .js-open-checklist-details-menu': Popup.open('checklistActions'),
|
||||
'submit .js-add-checklist'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-add-checklist-item');
|
||||
const textarea = tpl.find('textarea.js-add-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
let cardId = this.currentData().cardId;
|
||||
let cardId = Template.currentData().cardId;
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
//if (card.isLinked()) cardId = card.linkedId;
|
||||
if (card.isLinkedCard()) {
|
||||
cardId = card.linkedId;
|
||||
}
|
||||
|
||||
let sortIndex;
|
||||
let checklistItemIndex;
|
||||
if (this.currentData().position === 'top') {
|
||||
if (Template.currentData().position === 'top') {
|
||||
sortIndex = Utils.calculateIndexData(null, card.firstChecklist()).base;
|
||||
checklistItemIndex = 0;
|
||||
} else {
|
||||
@@ -107,35 +123,63 @@ BlazeComponent.extendComponent({
|
||||
title,
|
||||
sort: sortIndex,
|
||||
});
|
||||
this.closeAllInlinedForms();
|
||||
tpl.$('.js-close-inlined-form').click();
|
||||
setTimeout(() => {
|
||||
this.$('.add-checklist-item')
|
||||
tpl.$('.add-checklist-item')
|
||||
.eq(checklistItemIndex)
|
||||
.click();
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
addChecklistItem(event) {
|
||||
'submit .js-edit-checklist-title'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-add-checklist-item');
|
||||
const newlineBecomesNewChecklistItem = this.find('input#toggleNewlineBecomesNewChecklistItem');
|
||||
const newlineBecomesNewChecklistItemOriginOrder = this.find('input#toggleNewlineBecomesNewChecklistItemOriginOrder');
|
||||
const textarea = tpl.find('textarea.js-edit-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
const checklist = this.currentData().checklist;
|
||||
const formData = Blaze.getData(event.currentTarget) || Blaze.getData(event.target);
|
||||
const checklist = formData?.checklist;
|
||||
if (checklist) {
|
||||
checklist.setTitle(title);
|
||||
}
|
||||
},
|
||||
'submit .js-add-checklist-item'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = tpl.find('textarea.js-add-checklist-item');
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
const newlineBecomesNewChecklistItem = tpl.find('input#toggleNewlineBecomesNewChecklistItem');
|
||||
const newlineBecomesNewChecklistItemOriginOrder = tpl.find('input#toggleNewlineBecomesNewChecklistItemOriginOrder');
|
||||
const title = textarea.value.trim();
|
||||
const currentData = Template.currentData() || {};
|
||||
let resolvedData = currentData;
|
||||
let checklist = currentData.checklist;
|
||||
if (!checklist) {
|
||||
const form = event.currentTarget?.closest
|
||||
? event.currentTarget.closest('form')
|
||||
: $(event.target).closest('form').get(0);
|
||||
const formData = form ? Blaze.getData(form) : null;
|
||||
if (formData) {
|
||||
resolvedData = formData;
|
||||
}
|
||||
checklist = formData?.checklist;
|
||||
}
|
||||
if (!checklist) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (title) {
|
||||
let checklistItems = [title];
|
||||
if (newlineBecomesNewChecklistItem.checked) {
|
||||
if (newlineBecomesNewChecklistItem?.checked) {
|
||||
checklistItems = title.split('\n').map(_value => _value.trim());
|
||||
if (this.currentData().position === 'top') {
|
||||
if (newlineBecomesNewChecklistItemOriginOrder.checked === false) {
|
||||
if (resolvedData.position === 'top') {
|
||||
if (newlineBecomesNewChecklistItemOriginOrder?.checked === false) {
|
||||
checklistItems = checklistItems.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
let addIndex;
|
||||
let sortIndex;
|
||||
if (this.currentData().position === 'top') {
|
||||
if (resolvedData.position === 'top') {
|
||||
sortIndex = Utils.calculateIndexData(null, checklist.firstItem()).base;
|
||||
addIndex = -1;
|
||||
} else {
|
||||
@@ -156,33 +200,33 @@ BlazeComponent.extendComponent({
|
||||
textarea.value = '';
|
||||
textarea.focus();
|
||||
},
|
||||
|
||||
deleteItem() {
|
||||
const checklist = this.currentData().checklist;
|
||||
const item = this.currentData().item;
|
||||
if (checklist && item && item._id) {
|
||||
ChecklistItems.remove(item._id);
|
||||
'submit .js-edit-checklist-item'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = tpl.find('textarea.js-edit-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
const formData = Blaze.getData(event.currentTarget) || Blaze.getData(event.target);
|
||||
const item = formData?.item;
|
||||
if (item) {
|
||||
item.setTitle(title);
|
||||
}
|
||||
},
|
||||
|
||||
editChecklist(event) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-edit-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
const checklist = this.currentData().checklist;
|
||||
checklist.setTitle(title);
|
||||
'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
|
||||
'click .js-delete-checklist-item': Popup.afterConfirm('checklistItemDelete', function () {
|
||||
Popup.back();
|
||||
const item = this?.item || this;
|
||||
if (item && item._id) {
|
||||
ChecklistItems.remove(item._id);
|
||||
}
|
||||
}),
|
||||
// add and delete checklist / checklist-item
|
||||
'click .js-open-inlined-form'(event, tpl) {
|
||||
tpl.$('.js-close-inlined-form').click();
|
||||
},
|
||||
|
||||
editChecklistItem(event) {
|
||||
'click #toggleHideFinishedChecklist'(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const textarea = this.find('textarea.js-edit-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
const item = this.currentData().item;
|
||||
item.setTitle(title);
|
||||
Template.currentData().card.toggleHideFinishedChecklist();
|
||||
},
|
||||
|
||||
pressKey(event) {
|
||||
'keydown textarea.js-add-checklist-item'(event) {
|
||||
//If user press enter key inside a form, submit it
|
||||
//Unless the user is also holding down the 'shift' key
|
||||
if (event.keyCode === 13 && !event.shiftKey) {
|
||||
@@ -191,201 +235,206 @@ BlazeComponent.extendComponent({
|
||||
$form.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
focusChecklistItem(event) {
|
||||
// If a new checklist is created, pre-fill the title and select it.
|
||||
const checklist = this.currentData().checklist;
|
||||
if (!checklist) {
|
||||
const textarea = event.target;
|
||||
textarea.value = capitalize(TAPi18n.__('r-checklist'));
|
||||
textarea.select();
|
||||
}
|
||||
},
|
||||
// NOTE: boardsSwimlanesAndLists template was removed from jade but JS was left behind.
|
||||
// This is dead code — the template no longer exists in any jade file.
|
||||
|
||||
/** closes all inlined forms (checklist and checklist-item input fields) */
|
||||
closeAllInlinedForms() {
|
||||
this.$('.js-close-inlined-form').click();
|
||||
},
|
||||
Template.addChecklistItemForm.onRendered(function () {
|
||||
autosize(this.$('textarea.js-add-checklist-item'));
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-open-checklist-details-menu': Popup.open('checklistActions'),
|
||||
'submit .js-add-checklist': this.addChecklist,
|
||||
'submit .js-edit-checklist-title': this.editChecklist,
|
||||
'submit .js-add-checklist-item': this.addChecklistItem,
|
||||
'submit .js-edit-checklist-item': this.editChecklistItem,
|
||||
'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
|
||||
'click .js-delete-checklist-item': this.deleteItem,
|
||||
'focus .js-add-checklist-item': this.focusChecklistItem,
|
||||
// add and delete checklist / checklist-item
|
||||
'click .js-open-inlined-form': this.closeAllInlinedForms,
|
||||
'click #toggleHideFinishedChecklist'(event) {
|
||||
event.preventDefault();
|
||||
this.data().card.toggleHideFinishedChecklist();
|
||||
},
|
||||
keydown: this.pressKey,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('checklists');
|
||||
Template.addChecklistItemForm.events({
|
||||
'click a.fa.fa-copy'(event, tpl) {
|
||||
const $editor = tpl.$('textarea');
|
||||
const promise = Utils.copyTextToClipboard($editor[0].value);
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
subManager.subscribe('board', Session.get('currentBoard'), false);
|
||||
this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
|
||||
},
|
||||
|
||||
boards() {
|
||||
const ret = ReactiveCache.getBoards(
|
||||
{
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
_id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() },
|
||||
},
|
||||
{
|
||||
sort: { sort: 1 /* boards default sorting */ },
|
||||
},
|
||||
);
|
||||
return ret;
|
||||
},
|
||||
|
||||
swimlanes() {
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
return board.swimlanes();
|
||||
},
|
||||
|
||||
aBoardLists() {
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
return board.lists();
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'change .js-select-boards'(event) {
|
||||
this.selectedBoardId.set($(event.currentTarget).val());
|
||||
subManager.subscribe('board', this.selectedBoardId.get(), false);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('boardsSwimlanesAndLists');
|
||||
|
||||
Template.checklists.helpers({
|
||||
checklists() {
|
||||
const card = ReactiveCache.getCard(this.cardId);
|
||||
const ret = card.checklists();
|
||||
return ret;
|
||||
const $tooltip = tpl.$('.copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
autosize(this.$('textarea.js-add-checklist-item'));
|
||||
Template.checklistActionsPopup.events({
|
||||
'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () {
|
||||
Popup.back(2);
|
||||
const checklist = this.checklist;
|
||||
if (checklist && checklist._id) {
|
||||
Checklists.remove(checklist._id);
|
||||
}
|
||||
}),
|
||||
'click .js-move-checklist': Popup.open('moveChecklist'),
|
||||
'click .js-copy-checklist': Popup.open('copyChecklist'),
|
||||
'click .js-hide-checked-checklist-items'(event) {
|
||||
event.preventDefault();
|
||||
Template.currentData().checklist.toggleHideCheckedChecklistItems();
|
||||
Popup.back();
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click a.fa.fa-copy'(event) {
|
||||
const $editor = this.$('textarea');
|
||||
const promise = Utils.copyTextToClipboard($editor[0].value);
|
||||
|
||||
const $tooltip = this.$('.copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
}
|
||||
];
|
||||
}
|
||||
}).register('addChecklistItemForm');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () {
|
||||
Popup.back(2);
|
||||
const checklist = this.checklist;
|
||||
if (checklist && checklist._id) {
|
||||
Checklists.remove(checklist._id);
|
||||
}
|
||||
}),
|
||||
'click .js-move-checklist': Popup.open('moveChecklist'),
|
||||
'click .js-copy-checklist': Popup.open('copyChecklist'),
|
||||
'click .js-hide-checked-checklist-items'(event) {
|
||||
event.preventDefault();
|
||||
this.data().checklist.toggleHideCheckedChecklistItems();
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-hide-all-checklist-items'(event) {
|
||||
event.preventDefault();
|
||||
this.data().checklist.toggleHideAllChecklistItems();
|
||||
Popup.back();
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}).register('checklistActionsPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
autosize(this.$('textarea.js-edit-checklist-item'));
|
||||
'click .js-hide-all-checklist-items'(event) {
|
||||
event.preventDefault();
|
||||
Template.currentData().checklist.toggleHideAllChecklistItems();
|
||||
Popup.back();
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click a.fa.fa-copy'(event) {
|
||||
const $editor = this.$('textarea');
|
||||
const promise = Utils.copyTextToClipboard($editor[0].value);
|
||||
'click .js-show-checklist-at-minicard'(event) {
|
||||
event.preventDefault();
|
||||
Template.currentData().checklist.toggleShowChecklistAtMinicard();
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
const $tooltip = this.$('.copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
}
|
||||
];
|
||||
}
|
||||
}).register('editChecklistItemForm');
|
||||
Template.editChecklistItemForm.onRendered(function () {
|
||||
autosize(this.$('textarea.js-edit-checklist-item'));
|
||||
});
|
||||
|
||||
Template.editChecklistItemForm.events({
|
||||
'click a.fa.fa-copy'(event, tpl) {
|
||||
const $editor = tpl.$('textarea');
|
||||
const promise = Utils.copyTextToClipboard($editor[0].value);
|
||||
|
||||
const $tooltip = tpl.$('.copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
});
|
||||
|
||||
Template.checklistItemDetail.helpers({
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
toggleItem() {
|
||||
const checklist = this.currentData().checklist;
|
||||
const item = this.currentData().item;
|
||||
Template.checklistItemDetail.events({
|
||||
'click .js-checklist-item .check-box-container'() {
|
||||
const checklist = Template.currentData().checklist;
|
||||
const item = Template.currentData().item;
|
||||
if (checklist && item && item._id) {
|
||||
item.toggleItem();
|
||||
}
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-checklist-item .check-box-container': this.toggleItem,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to find the dialog instance from a parent popup template.
|
||||
* copyAndMoveChecklist is included inside moveChecklistPopup / copyChecklistPopup,
|
||||
* so we traverse up the view hierarchy to find the parent template's dialog.
|
||||
*/
|
||||
function getParentDialog(tpl) {
|
||||
let view = tpl.view.parentView;
|
||||
while (view) {
|
||||
if (view.templateInstance && view.templateInstance() && view.templateInstance().dialog) {
|
||||
return view.templateInstance().dialog;
|
||||
}
|
||||
view = view.parentView;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Shared helpers for copyAndMoveChecklist sub-template */
|
||||
Template.copyAndMoveChecklist.helpers({
|
||||
boards() {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.boards() : [];
|
||||
},
|
||||
}).register('checklistItemDetail');
|
||||
swimlanes() {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.swimlanes() : [];
|
||||
},
|
||||
lists() {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.lists() : [];
|
||||
},
|
||||
cards() {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.cards() : [];
|
||||
},
|
||||
isDialogOptionBoardId(boardId) {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.isDialogOptionBoardId(boardId) : false;
|
||||
},
|
||||
isDialogOptionSwimlaneId(swimlaneId) {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.isDialogOptionSwimlaneId(swimlaneId) : false;
|
||||
},
|
||||
isDialogOptionListId(listId) {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.isDialogOptionListId(listId) : false;
|
||||
},
|
||||
isDialogOptionCardId(cardId) {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.isDialogOptionCardId(cardId) : false;
|
||||
},
|
||||
isTitleDefault(title) {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.isTitleDefault(title) : title;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper: register standard card dialog events on a checklist popup template.
|
||||
* Events bubble up from the copyAndMoveChecklist sub-template to the parent popup.
|
||||
*/
|
||||
function registerChecklistDialogEvents(templateName) {
|
||||
Template[templateName].events({
|
||||
async 'click .js-done'(event, tpl) {
|
||||
const dialog = tpl.dialog;
|
||||
const boardSelect = tpl.$('.js-select-boards')[0];
|
||||
const boardId = boardSelect.options[boardSelect.selectedIndex].value;
|
||||
|
||||
const listSelect = tpl.$('.js-select-lists')[0];
|
||||
const listId = listSelect.options[listSelect.selectedIndex].value;
|
||||
|
||||
const swimlaneSelect = tpl.$('.js-select-swimlanes')[0];
|
||||
const swimlaneId = swimlaneSelect.options[swimlaneSelect.selectedIndex].value;
|
||||
|
||||
const cardSelect = tpl.$('.js-select-cards')[0];
|
||||
const cardId = cardSelect.options.length > 0
|
||||
? cardSelect.options[cardSelect.selectedIndex].value
|
||||
: null;
|
||||
|
||||
const options = { boardId, swimlaneId, listId, cardId };
|
||||
try {
|
||||
await dialog.setDone(cardId, options);
|
||||
} catch (e) {
|
||||
console.error('Error in card dialog operation:', e);
|
||||
}
|
||||
Popup.back(2);
|
||||
},
|
||||
'change .js-select-boards'(event, tpl) {
|
||||
tpl.dialog.getBoardData($(event.currentTarget).val());
|
||||
},
|
||||
'change .js-select-swimlanes'(event, tpl) {
|
||||
tpl.dialog.selectedSwimlaneId.set($(event.currentTarget).val());
|
||||
tpl.dialog.setFirstListId();
|
||||
},
|
||||
'change .js-select-lists'(event, tpl) {
|
||||
tpl.dialog.selectedListId.set($(event.currentTarget).val());
|
||||
tpl.dialog.selectedCardId.set('');
|
||||
},
|
||||
'change .js-select-cards'(event, tpl) {
|
||||
tpl.dialog.selectedCardId.set($(event.currentTarget).val());
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Move Checklist Dialog */
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
|
||||
this.data().checklist.move(cardId);
|
||||
}
|
||||
}).register('moveChecklistPopup');
|
||||
Template.moveChecklistPopup.onCreated(function () {
|
||||
this.dialog = new BoardSwimlaneListCardDialog(this, {
|
||||
getDialogOptions() {
|
||||
return ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
|
||||
},
|
||||
async setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
|
||||
await Template.currentData().checklist.move(cardId);
|
||||
},
|
||||
});
|
||||
});
|
||||
registerChecklistDialogEvents('moveChecklistPopup');
|
||||
|
||||
/** Copy Checklist Dialog */
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
|
||||
this.data().checklist.copy(cardId);
|
||||
}
|
||||
}).register('copyChecklistPopup');
|
||||
Template.copyChecklistPopup.onCreated(function () {
|
||||
this.dialog = new BoardSwimlaneListCardDialog(this, {
|
||||
getDialogOptions() {
|
||||
return ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
|
||||
},
|
||||
async setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
|
||||
await Template.currentData().checklist.copy(cardId);
|
||||
},
|
||||
});
|
||||
});
|
||||
registerChecklistDialogEvents('copyChecklistPopup');
|
||||
@@ -0,0 +1,6 @@
|
||||
template(name='inlinedCardDescription')
|
||||
if isOpen.get
|
||||
form.inlined-form.js-inlined-form(id=id class=classNames)
|
||||
+Template.contentBlock
|
||||
else
|
||||
+Template.elseBlock
|
||||
@@ -38,6 +38,7 @@
|
||||
.palette-colors {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start; /* left-align color chips in wider popovers */
|
||||
}
|
||||
.palette-colors .palette-color {
|
||||
flex-grow: 1;
|
||||
@@ -222,9 +223,13 @@
|
||||
.card-label-edit-button:hover {
|
||||
background: #dbdbdb;
|
||||
}
|
||||
ul.edit-labels-pop-over span.fa.label-handle {
|
||||
ul.edit-labels-pop-over span.label-handle {
|
||||
padding-right: 10px;
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
ul.edit-labels-pop-over span.fa.label-handle + .card-label {
|
||||
ul.edit-labels-pop-over span.label-handle + .card-label {
|
||||
max-width: 180px;
|
||||
}
|
||||
@@ -1,119 +1,117 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { LABEL_COLORS } from '/models/metadata/colors';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
let labelColors;
|
||||
Meteor.startup(() => {
|
||||
labelColors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
|
||||
labelColors = LABEL_COLORS;
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentColor = new ReactiveVar(this.data().color);
|
||||
},
|
||||
const getFallbackLabelColor = () => {
|
||||
if (Array.isArray(labelColors) && labelColors.length > 0) {
|
||||
return labelColors[0];
|
||||
}
|
||||
return 'green';
|
||||
};
|
||||
|
||||
Template.formLabel.onCreated(function () {
|
||||
const initialColor = this.data?.color || getFallbackLabelColor();
|
||||
this.currentColor = new ReactiveVar(initialColor);
|
||||
});
|
||||
|
||||
Template.formLabel.helpers({
|
||||
labels() {
|
||||
return labelColors.map(color => ({ color, name: '' }));
|
||||
const colors = Array.isArray(labelColors) ? labelColors : [getFallbackLabelColor()];
|
||||
return colors.map(color => ({ color, name: '' }));
|
||||
},
|
||||
|
||||
isSelected(color) {
|
||||
return this.currentColor.get() === color;
|
||||
return Template.instance().currentColor.get() === color;
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-palette-color'() {
|
||||
this.currentColor.set(this.currentData().color);
|
||||
},
|
||||
},
|
||||
];
|
||||
Template.formLabel.events({
|
||||
'click .js-palette-color'(event, tpl) {
|
||||
const paletteData = Blaze.getData(event.currentTarget);
|
||||
const selectedColor = paletteData?.color || Template.currentData()?.color || getFallbackLabelColor();
|
||||
tpl.currentColor.set(selectedColor);
|
||||
},
|
||||
}).register('formLabel');
|
||||
});
|
||||
|
||||
Template.createLabelPopup.helpers({
|
||||
// This is the default color for a new label. We search the first color that
|
||||
// is not already used in the board (although it's not a problem if two
|
||||
// labels have the same color).
|
||||
defaultColor() {
|
||||
const labels = Utils.getCurrentBoard().labels;
|
||||
const usedColors = _.pluck(labels, 'color');
|
||||
const availableColors = _.difference(labelColors, usedColors);
|
||||
return availableColors.length > 1 ? availableColors[0] : labelColors[0];
|
||||
const board = Utils.getCurrentBoard();
|
||||
const colors = Array.isArray(labelColors) ? labelColors : [getFallbackLabelColor()];
|
||||
const labels = Array.isArray(board?.labels) ? board.labels : [];
|
||||
const usedColors = labels.map(l => l.color);
|
||||
const availableColors = colors.filter(c => !usedColors.includes(c));
|
||||
return availableColors.length > 0 ? availableColors[0] : colors[0];
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
const itemsSelector = 'li.js-card-label-item:not(.placeholder)';
|
||||
const $labels = this.$('.edit-labels-pop-over');
|
||||
Template.cardLabelsPopup.onRendered(function () {
|
||||
const tpl = this;
|
||||
const itemsSelector = 'li.js-card-label-item:not(.placeholder)';
|
||||
const $labels = tpl.$('.edit-labels-pop-over');
|
||||
|
||||
$labels.sortable({
|
||||
connectWith: '.edit-labels-pop-over',
|
||||
tolerance: 'pointer',
|
||||
appendTo: '.edit-labels-pop-over',
|
||||
helper(element, currentItem) {
|
||||
let ret = currentItem.clone();
|
||||
if (currentItem.closest('.popup-container-depth-0').length == 0)
|
||||
{ // only set css transform at every sub-popup, not at the main popup
|
||||
const content = currentItem.closest('.content')[0]
|
||||
const offsetLeft = content.offsetLeft;
|
||||
const offsetTop = $('.pop-over > .header').height() * -1;
|
||||
ret.css("transform", `translate(${offsetLeft}px, ${offsetTop}px)`);
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
distance: 7,
|
||||
items: itemsSelector,
|
||||
placeholder: 'card-label-wrapper placeholder',
|
||||
start(evt, ui) {
|
||||
ui.helper.css('z-index', 1000);
|
||||
ui.placeholder.height(ui.helper.height());
|
||||
EscapeActions.clickExecute(evt.target, 'inlinedForm');
|
||||
},
|
||||
stop(evt, ui) {
|
||||
const newLabelOrderOnlyIds = ui.item.parent().children().toArray().map(_element => Blaze.getData(_element)._id)
|
||||
const card = Blaze.getData(this);
|
||||
card.board().setNewLabelOrder(newLabelOrderOnlyIds);
|
||||
},
|
||||
});
|
||||
|
||||
// Disable drag-dropping if the current user is not a board member or is comment only
|
||||
this.autorun(() => {
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$labels.sortable({
|
||||
handle: '.label-handle',
|
||||
});
|
||||
$labels.sortable({
|
||||
connectWith: '.edit-labels-pop-over',
|
||||
tolerance: 'pointer',
|
||||
appendTo: '.edit-labels-pop-over',
|
||||
helper(element, currentItem) {
|
||||
let ret = currentItem.clone();
|
||||
if (currentItem.closest('.popup-container-depth-0').length == 0)
|
||||
{ // only set css transform at every sub-popup, not at the main popup
|
||||
const content = currentItem.closest('.content')[0]
|
||||
const offsetLeft = content.offsetLeft;
|
||||
const offsetTop = $('.pop-over > .header').height() * -1;
|
||||
ret.css("transform", `translate(${offsetLeft}px, ${offsetTop}px)`);
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
distance: 7,
|
||||
items: itemsSelector,
|
||||
placeholder: 'card-label-wrapper placeholder',
|
||||
start(evt, ui) {
|
||||
ui.helper.css('z-index', 1000);
|
||||
ui.placeholder.height(ui.helper.height());
|
||||
EscapeActions.clickExecute(evt.target, 'inlinedForm');
|
||||
},
|
||||
stop(evt, ui) {
|
||||
const newLabelOrderOnlyIds = ui.item.parent().children().toArray().map(_element => Blaze.getData(_element)._id)
|
||||
const card = Blaze.getData(this);
|
||||
card.board().setNewLabelOrder(newLabelOrderOnlyIds);
|
||||
},
|
||||
});
|
||||
|
||||
// Disable drag-dropping if the current user is not a board member or is comment only
|
||||
tpl.autorun(() => {
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$labels.sortable({
|
||||
handle: '.label-handle',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.cardLabelsPopup.helpers({
|
||||
isLabelSelected(cardId) {
|
||||
return (ReactiveCache.getCard(cardId).labelIds || []).includes(this._id);
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-select-label'(event) {
|
||||
const card = this.data();
|
||||
const labelId = this.currentData()._id;
|
||||
card.toggleLabel(labelId);
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-edit-label': Popup.open('editLabel'),
|
||||
'click .js-add-label': Popup.open('createLabel'),
|
||||
}
|
||||
];
|
||||
}
|
||||
}).register('cardLabelsPopup');
|
||||
});
|
||||
|
||||
Template.cardLabelsPopup.events({
|
||||
});
|
||||
|
||||
Template.formLabel.events({
|
||||
'click .js-palette-color'(event) {
|
||||
const $this = $(event.currentTarget);
|
||||
|
||||
// hide selected ll colors
|
||||
$('.js-palette-select').addClass('hide');
|
||||
|
||||
// show select color
|
||||
$this.find('.js-palette-select').removeClass('hide');
|
||||
'click .js-select-label'(event) {
|
||||
const card = Template.currentData();
|
||||
const labelId = this._id;
|
||||
card.toggleLabel(labelId);
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-edit-label': Popup.open('editLabel'),
|
||||
'click .js-add-label': Popup.open('createLabel'),
|
||||
});
|
||||
|
||||
Template.createLabelPopup.events({
|
||||
@@ -121,11 +119,19 @@ Template.createLabelPopup.events({
|
||||
'submit .create-label'(event, templateInstance) {
|
||||
event.preventDefault();
|
||||
const board = Utils.getCurrentBoard();
|
||||
if (!board) {
|
||||
return;
|
||||
}
|
||||
const name = templateInstance
|
||||
.$('#labelName')
|
||||
.val()
|
||||
.trim();
|
||||
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
|
||||
const selectedColorIcon = templateInstance.find('.js-palette-color .fa-check');
|
||||
const selectedPaletteNode = selectedColorIcon?.closest
|
||||
? selectedColorIcon.closest('.js-palette-color')
|
||||
: null;
|
||||
const selectedColorData = selectedPaletteNode && Blaze.getData(selectedPaletteNode);
|
||||
const color = selectedColorData?.color || getFallbackLabelColor();
|
||||
board.addLabel(name, color);
|
||||
Popup.back();
|
||||
},
|
||||
@@ -140,18 +146,20 @@ Template.editLabelPopup.events({
|
||||
'submit .edit-label'(event, templateInstance) {
|
||||
event.preventDefault();
|
||||
const board = Utils.getCurrentBoard();
|
||||
if (!board) {
|
||||
return;
|
||||
}
|
||||
const name = templateInstance
|
||||
.$('#labelName')
|
||||
.val()
|
||||
.trim();
|
||||
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
|
||||
const selectedColorIcon = templateInstance.find('.js-palette-color .fa-check');
|
||||
const selectedPaletteNode = selectedColorIcon?.closest
|
||||
? selectedColorIcon.closest('.js-palette-color')
|
||||
: null;
|
||||
const selectedColorData = selectedPaletteNode && Blaze.getData(selectedPaletteNode);
|
||||
const color = selectedColorData?.color || getFallbackLabelColor();
|
||||
board.editLabel(this._id, name, color);
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
Template.cardLabelsPopup.helpers({
|
||||
isLabelSelected(cardId) {
|
||||
return _.contains(ReactiveCache.getCard(cardId).labelIds, this._id);
|
||||
},
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1.2vh;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.minicard-wrapper.placeholder {
|
||||
background: #ccc;
|
||||
@@ -16,8 +16,8 @@
|
||||
}
|
||||
.minicard-wrapper.ui-sortable-helper .and-n-other {
|
||||
width: 100%;
|
||||
height: 2.5vh;
|
||||
padding: 0.5vh;
|
||||
height: 25px;
|
||||
padding: 5px;
|
||||
background-color: #f2f2f2;
|
||||
text-align: center;
|
||||
border-radius: 0.4vw;
|
||||
@@ -28,15 +28,27 @@
|
||||
.minicard-wrapper .multi-selection-checkbox + .minicard {
|
||||
margin-left: 1vw;
|
||||
}
|
||||
/* Enlarge the clickable area of the small selection checkbox so it can be
|
||||
reliably clicked at the left side of the minicard. The visible 13px box (and
|
||||
its rotated is-checked checkmark) keep their size; an invisible overlay only
|
||||
extends the hit target. */
|
||||
.minicard-wrapper .multi-selection-checkbox::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: -8px;
|
||||
right: -8px;
|
||||
bottom: -8px;
|
||||
}
|
||||
@media only screen {
|
||||
.minicard {
|
||||
padding: 0.8vh 1vw 0.3vh;
|
||||
padding: 8px 1vw 3px;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
background-color: #fff;
|
||||
min-height: 2.5vh;
|
||||
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.15);
|
||||
min-height: 25px;
|
||||
box-shadow: 0 2px 3px rgba(0,0,0,0.15);
|
||||
border-radius: 0.3vw;
|
||||
color: #4d4d4d;
|
||||
overflow: hidden;
|
||||
@@ -45,9 +57,10 @@
|
||||
}
|
||||
.minicard-details-menu-with-handle {
|
||||
float: right;
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
padding-right: 4vw;
|
||||
padding-left: 0.7vw;
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
padding: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.minicard-details-menu {
|
||||
float: right;
|
||||
@@ -76,7 +89,7 @@
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
z-index: 25;
|
||||
box-shadow: -0.3vw 0.2vh 0.3vh rgba(0,0,0,0.2);
|
||||
box-shadow: -0.3vw 2px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
.minicard:hover:not(.minicard-composer),
|
||||
.is-selected .minicard,
|
||||
@@ -90,20 +103,21 @@
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
height: 20vh;
|
||||
height: 150px;
|
||||
user-select: none;
|
||||
margin: 0.8vh -1vw 0.8vh -1vw;
|
||||
margin: 8px -1vw 8px -1vw;
|
||||
border-radius: top 0.3vw;
|
||||
}
|
||||
.minicard .minicard-labels {
|
||||
float: none;
|
||||
margin-right: 6vw;
|
||||
}
|
||||
.minicard .minicard-labels .minicard-label {
|
||||
width: 1.5vw;
|
||||
height: 1.5vw;
|
||||
width: clamp(12px, 1.5vw, 16px);
|
||||
height: clamp(12px, 1.5vw, 16px);
|
||||
border-radius: 0.3vw;
|
||||
margin-right: 0.4vw;
|
||||
margin-bottom: 0.4vh;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.minicard .minicard-labels-no-text {
|
||||
display: flex;
|
||||
@@ -111,6 +125,7 @@
|
||||
}
|
||||
.minicard .minicard-custom-fields {
|
||||
display: block;
|
||||
margin-right: 6vw;
|
||||
}
|
||||
.minicard .minicard-custom-field {
|
||||
display: flex;
|
||||
@@ -130,21 +145,28 @@
|
||||
margin-right: 0.5vw;
|
||||
}
|
||||
.minicard .handle {
|
||||
width: 2.5vw;
|
||||
height: 2.5vw;
|
||||
width: clamp(20px, 2.5vw, 28px);
|
||||
height: clamp(20px, 2.5vw, 28px);
|
||||
position: absolute;
|
||||
right: 0.7vw;
|
||||
top: 0.7vh;
|
||||
right: 0vw;
|
||||
top: 28px;
|
||||
display: none;
|
||||
z-index: 1;
|
||||
}
|
||||
@media only screen {
|
||||
.minicard .handle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.minicard .handle .fa-arrows {
|
||||
.minicard .handle .drag-handle {
|
||||
font-size: clamp(16px, 3vw, 20px);
|
||||
color: #ccc;
|
||||
display: inline-block;
|
||||
width: 1.4em;
|
||||
text-align: center;
|
||||
}
|
||||
.minicard .minicard-title {
|
||||
margin-right: 1.5vw;
|
||||
}
|
||||
.minicard .minicard-title .card-number {
|
||||
color: #b3b3b3;
|
||||
@@ -164,13 +186,146 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
margin-right: 6vw;
|
||||
clear: both;
|
||||
}
|
||||
.minicard .date {
|
||||
margin-right: 0.4vw;
|
||||
}
|
||||
|
||||
/* Unicode icons for minicard dates - matching cardDate.css */
|
||||
.minicard .card-date.end-date time::before {
|
||||
content: "🏁"; /* Finish flag - represents end/completion */
|
||||
}
|
||||
.minicard .card-date.due-date time::before {
|
||||
content: "⏰"; /* Alarm clock - represents due/deadline */
|
||||
}
|
||||
.minicard .card-date.start-date time::before {
|
||||
content: "🚀"; /* Rocket - represents start/launch */
|
||||
}
|
||||
.minicard .card-date.received-date time::before {
|
||||
content: "📥"; /* Inbox tray - represents received/incoming */
|
||||
}
|
||||
|
||||
.minicard .card-date time::before {
|
||||
font-size: inherit;
|
||||
margin-right: 0.3em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Date type specific colors for minicards - matching cardDate.css */
|
||||
.minicard .card-date.received-date {
|
||||
background-color: #dbdbdb; /* Grey for received - same as base card-date */
|
||||
}
|
||||
|
||||
.minicard .card-date.received-date:hover,
|
||||
.minicard .card-date.received-date.is-active {
|
||||
background-color: #b3b3b3;
|
||||
}
|
||||
|
||||
.minicard .card-date.start-date {
|
||||
background-color: #90ee90; /* Light green for start */
|
||||
color: #000; /* Black text for start */
|
||||
}
|
||||
|
||||
.minicard .card-date.start-date:hover,
|
||||
.minicard .card-date.start-date.is-active {
|
||||
background-color: #7dd87d;
|
||||
}
|
||||
|
||||
.minicard .card-date.due-date {
|
||||
background-color: #ffd700; /* Yellow for due */
|
||||
color: #000; /* Black text for due */
|
||||
}
|
||||
|
||||
.minicard .card-date.due-date:hover,
|
||||
.minicard .card-date.due-date.is-active {
|
||||
background-color: #e6c200;
|
||||
}
|
||||
|
||||
.minicard .card-date.end-date {
|
||||
background-color: #ffb3b3; /* Light red for end */
|
||||
color: #000; /* Black text for end */
|
||||
}
|
||||
|
||||
.minicard .card-date.end-date:hover,
|
||||
.minicard .card-date.end-date.is-active {
|
||||
background-color: #ff9999;
|
||||
}
|
||||
|
||||
/* Date status colors for minicards - matching cardDate.css */
|
||||
.minicard .card-date.overdue {
|
||||
background-color: #ff4444 !important; /* Red for overdue */
|
||||
color: #fff !important;
|
||||
}
|
||||
.minicard .card-date.overdue:hover,
|
||||
.minicard .card-date.overdue.is-active {
|
||||
background-color: #cc3333 !important;
|
||||
}
|
||||
|
||||
.minicard .card-date.due-soon {
|
||||
background-color: #ffaa00 !important; /* Amber for due soon */
|
||||
color: #000 !important;
|
||||
}
|
||||
.minicard .card-date.due-soon:hover,
|
||||
.minicard .card-date.due-soon.is-active {
|
||||
background-color: #e69900 !important;
|
||||
}
|
||||
|
||||
.minicard .card-date.not-due {
|
||||
/* No special background - uses default date type colors */
|
||||
}
|
||||
|
||||
.minicard .card-date.current {
|
||||
background-color: #5ba639 !important; /* Green for current/active */
|
||||
color: #fff !important;
|
||||
}
|
||||
.minicard .card-date.current:hover,
|
||||
.minicard .card-date.current.is-active {
|
||||
background-color: #46802c !important;
|
||||
}
|
||||
|
||||
.minicard .card-date.completed {
|
||||
background-color: #90ee90 !important; /* Light green for completed */
|
||||
color: #000 !important;
|
||||
}
|
||||
.minicard .card-date.completed:hover,
|
||||
.minicard .card-date.completed.is-active {
|
||||
background-color: #7dd87d !important;
|
||||
}
|
||||
|
||||
.minicard .card-date.completed-early {
|
||||
background-color: #4caf50 !important; /* Green for completed early */
|
||||
color: #fff !important;
|
||||
}
|
||||
.minicard .card-date.completed-early:hover,
|
||||
.minicard .card-date.completed-early.is-active {
|
||||
background-color: #45a049 !important;
|
||||
}
|
||||
|
||||
.minicard .card-date.completed-late {
|
||||
background-color: #ff9800 !important; /* Orange for completed late */
|
||||
color: #fff !important;
|
||||
}
|
||||
.minicard .card-date.completed-late:hover,
|
||||
.minicard .card-date.completed-late.is-active {
|
||||
background-color: #f57c00 !important;
|
||||
}
|
||||
|
||||
.minicard .card-date.completed-on-time {
|
||||
background-color: #2196f3 !important; /* Blue for completed on time */
|
||||
color: #fff !important;
|
||||
}
|
||||
.minicard .card-date.completed-on-time:hover,
|
||||
.minicard .card-date.completed-on-time.is-active {
|
||||
background-color: #1976d2 !important;
|
||||
}
|
||||
|
||||
.minicard .badges {
|
||||
float: left;
|
||||
margin-top: 1vh;
|
||||
margin-top: 8px;
|
||||
color: #808080;
|
||||
}
|
||||
.minicard .badges:empty {
|
||||
@@ -179,7 +334,7 @@
|
||||
.minicard .badges .badge {
|
||||
float: left;
|
||||
margin-right: 1.5vw;
|
||||
margin-bottom: 0.4vh;
|
||||
margin-bottom: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.minicard .badges .badge.is-finished {
|
||||
@@ -213,29 +368,29 @@
|
||||
.minicard .minicard-creator {
|
||||
float: right;
|
||||
margin-left: 0.7vw;
|
||||
margin-bottom: 0.5vh;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.minicard .minicard-members .member,
|
||||
.minicard .minicard-assignees .member,
|
||||
.minicard .minicard-creator .member {
|
||||
float: right;
|
||||
border-radius: 50%;
|
||||
height: 3.5vw;
|
||||
width: 3.5vw;
|
||||
margin-bottom: 0.5vh;
|
||||
height: clamp(24px, 3.5vw, 32px);
|
||||
width: clamp(24px, 3.5vw, 32px);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.minicard .minicard-members .assignee,
|
||||
.minicard .minicard-assignees .assignee,
|
||||
.minicard .minicard-creator .assignee {
|
||||
float: right;
|
||||
border-radius: 50%;
|
||||
height: 3.5vw;
|
||||
width: 3.5vw;
|
||||
height: clamp(24px, 3.5vw, 32px);
|
||||
width: clamp(24px, 3.5vw, 32px);
|
||||
}
|
||||
.minicard .minicard-members + .badges,
|
||||
.minicard .minicard-assignees + .badges,
|
||||
.minicard .minicard-creator + .badges {
|
||||
margin-top: 0.7vh;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.minicard .minicard-assignees {
|
||||
border-bottom: 1px solid #f00;
|
||||
@@ -248,11 +403,11 @@
|
||||
display: none;
|
||||
}
|
||||
.minicard .minicard-description {
|
||||
padding: 0.8vh 0 0 1vw;
|
||||
padding: 8px 0 0 1vw;
|
||||
color: #000;
|
||||
background-color: #eee;
|
||||
width: 100%;
|
||||
margin-bottom: 0.3vh;
|
||||
margin-bottom: 3px;
|
||||
margin-left: -0.5vw;
|
||||
border-radius: 0.4vw;
|
||||
overflow: hidden;
|
||||
@@ -262,7 +417,7 @@
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.minicard.minicard-composer {
|
||||
margin-bottom: 1.3vh;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.minicard.minicard-composer textarea.minicard-composer-textarea,
|
||||
.minicard.minicard-composer textarea.minicard-composer-textarea:focus {
|
||||
@@ -273,9 +428,9 @@
|
||||
height: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: 22vh;
|
||||
min-height: 5vh;
|
||||
margin-bottom: 2.5vh;
|
||||
max-height: 200px;
|
||||
min-height: 50px;
|
||||
margin-bottom: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.parent-prefix {
|
||||
@@ -588,3 +743,126 @@
|
||||
transform: scale(1.02);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* List name display on minicard */
|
||||
.minicard-list-name {
|
||||
font-size: 0.75em;
|
||||
color: #8c8c8c;
|
||||
margin-top: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3vw;
|
||||
}
|
||||
|
||||
/* Checklist display on minicard */
|
||||
.minicard-checklist {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
clear: both;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 5px;
|
||||
padding: 3px 0.5vw;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 0.3vw;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-title {
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
color: #4d4d4d;
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-menu {
|
||||
font-size: 1.2em;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
padding: 0 0.3vw;
|
||||
border-radius: 0.2vw;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-menu:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item {
|
||||
font-size: 0.75em;
|
||||
color: #666;
|
||||
margin-bottom: 2px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.3vw;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
padding: 2px 0;
|
||||
border-radius: 0.2vw;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item.is-checked {
|
||||
text-decoration: line-through;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item .check-box-unicode {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.8em;
|
||||
margin-top: 1px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item:hover .check-box-unicode {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item .item-title {
|
||||
flex: 1;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Trello-style "mark complete" toggle, left of the minicard title */
|
||||
.minicard-title .minicard-complete-toggle {
|
||||
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-right: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Sticker icon highlight styles (used instead of colour): taco pack => underline,
|
||||
pete pack => round (ring around the icon). Icon keeps its normal colour. */
|
||||
.minicard-sticker i.sticker-underline {
|
||||
border-bottom: 2px solid currentColor;
|
||||
padding-bottom: 1px;
|
||||
line-height: 1;
|
||||
}
|
||||
.minicard-sticker i.sticker-round {
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.6em;
|
||||
height: 1.6em;
|
||||
line-height: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -3,28 +3,29 @@ template(name="minicard")
|
||||
class="{{#if isLinkedCard}}linked-card{{/if}}"
|
||||
class="{{#if isLinkedBoard}}linked-board{{/if}}"
|
||||
class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}")
|
||||
if canModifyCard
|
||||
if canMoveCard
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
a.fa.fa-navicon.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
.handle
|
||||
.fa.fa-arrows
|
||||
else
|
||||
a.fa.fa-navicon.minicard-details-menu.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
i.fa.fa-arrows
|
||||
if canModifyCard
|
||||
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
.dates
|
||||
if getReceived
|
||||
unless getStart
|
||||
unless getDue
|
||||
unless getEnd
|
||||
.date
|
||||
+minicardReceivedDate
|
||||
if getStart
|
||||
.date
|
||||
+minicardStartDate
|
||||
if getDue
|
||||
.date
|
||||
+minicardDueDate
|
||||
if getEnd
|
||||
+minicardEndDate
|
||||
if showReceived
|
||||
if getReceived
|
||||
.date
|
||||
+minicardReceivedDate
|
||||
if showStart
|
||||
if getStart
|
||||
.date
|
||||
+minicardStartDate
|
||||
if showDue
|
||||
if getDue
|
||||
.date(class="{{#if getDueComplete}}due-complete{{/if}}")
|
||||
+minicardDueDate
|
||||
if showEnd
|
||||
if getEnd
|
||||
+minicardEndDate
|
||||
if getSpentTime
|
||||
.date
|
||||
+cardSpentTime
|
||||
@@ -45,7 +46,7 @@ template(name="minicard")
|
||||
.upload-progress-fill(style="width: {{progress}}%")
|
||||
if $eq status 'error'
|
||||
.upload-progress-error
|
||||
i.fa.fa-exclamation-triangle
|
||||
i.fa.fa-warning
|
||||
span {{_ 'upload-failed'}}
|
||||
else if $eq status 'completed'
|
||||
.upload-progress-success
|
||||
@@ -53,6 +54,9 @@ template(name="minicard")
|
||||
span {{_ 'upload-completed'}}
|
||||
|
||||
.minicard-title
|
||||
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 ' > ' }}
|
||||
@@ -61,32 +65,30 @@ template(name="minicard")
|
||||
| {{ parentCardName }}
|
||||
if isLinkedBoard
|
||||
a.js-linked-link
|
||||
span.linked-icon.fa.fa-folder
|
||||
span.linked-icon
|
||||
i.fa.fa-folder
|
||||
else if isLinkedCard
|
||||
a.js-linked-link
|
||||
span.linked-icon.fa.fa-id-card
|
||||
span.linked-icon
|
||||
i.fa.fa-id-card
|
||||
if getArchived
|
||||
span.linked-icon.linked-archived.fa.fa-archive
|
||||
span.linked-icon.linked-archived
|
||||
i.fa.fa-archive
|
||||
+viewer
|
||||
if currentBoard.allowsCardNumber
|
||||
if showCardNumber
|
||||
span.card-number
|
||||
| ##{getCardNumber}
|
||||
| ##{getCardNumber}
|
||||
= getTitle
|
||||
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentString ' > ' }}
|
||||
if $eq 'subtext-with-parent' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentCardName }}
|
||||
if labels
|
||||
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
|
||||
each labels
|
||||
unless hiddenMinicardLabelText
|
||||
span.js-card-label.card-label(class="card-label-{{color}}" title=name)
|
||||
+viewer
|
||||
= name
|
||||
if hiddenMinicardLabelText
|
||||
.minicard-label(class="card-label-{{color}}" title="{{name}}")
|
||||
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="card-label-{{color}}" title=name)
|
||||
+viewer
|
||||
= name
|
||||
if hiddenMinicardLabelText
|
||||
.minicard-label(class="card-label-{{color}}" title="{{name}}")
|
||||
|
||||
.minicard-custom-fields
|
||||
each customFieldsWD
|
||||
@@ -149,109 +151,106 @@ template(name="minicard")
|
||||
.minicard-creator
|
||||
+userAvatar(userId=this.userId noRemove=true)
|
||||
|
||||
if shouldShowChecklistAtMinicard
|
||||
each shouldShowChecklistAtMinicard
|
||||
+minicardChecklist(checklist=. card=..)
|
||||
.badges
|
||||
if canModifyCard
|
||||
if comments.length
|
||||
.badge(title="{{_ 'card-comments-title' comments.length }}")
|
||||
span.badge-icon.fa.fa-comment-o.badge-comment.badge-text
|
||||
= ' '
|
||||
= comments.length
|
||||
if stickers.length
|
||||
.badge.badge-state-image-only.minicard-stickers
|
||||
each stickers
|
||||
span.badge-icon.minicard-sticker(title="{{#if name}}{{name}}{{else}}{{icon}}{{/if}}")
|
||||
i.fa(class="fa-{{icon}} {{#if highlight}}sticker-{{highlight}}{{/if}}")
|
||||
if comments.length
|
||||
.badge(title="{{_ 'card-comments-title' comments.length }}")
|
||||
span.badge-icon.badge-comment.badge-text
|
||||
i.fa.fa-comment-o
|
||||
= ' '
|
||||
= comments.length
|
||||
//span.badge-comment.badge-text
|
||||
//| {{_ 'comment'}}
|
||||
//|
|
||||
{{_ 'comment'}}
|
||||
if getDescription
|
||||
unless currentBoard.allowsDescriptionTextOnMinicard
|
||||
.badge.badge-state-image-only(title=getDescription)
|
||||
span.badge-icon.fa.fa-align-left
|
||||
span.badge-icon
|
||||
i.fa.fa-file-text-o
|
||||
if getVoteQuestion
|
||||
.badge.badge-state-image-only(title=getVoteQuestion)
|
||||
span.badge-icon.fa.fa-thumbs-up(class="{{#if voteState}}text-green{{/if}}")
|
||||
span.badge-icon(class="{{#if voteState}}text-green{{/if}}")
|
||||
i.fa.fa-thumbs-up
|
||||
span.badge-text {{ voteCountPositive }}
|
||||
span.badge-icon.fa.fa-thumbs-down(class="{{#if $eq voteState false}}text-red{{/if}}")
|
||||
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}")
|
||||
i.fa.fa-thumbs-down
|
||||
span.badge-text {{ voteCountNegative }}
|
||||
if getPokerQuestion
|
||||
.badge.badge-state-image-only(title=getPokerQuestion)
|
||||
span.badge-icon.fa.fa-check(class="{{#if pokerState}}text-green{{/if}}")
|
||||
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}")
|
||||
i.fa.fa-check-square
|
||||
if expiredPoker
|
||||
span.badge-text {{ getPokerEstimation }}
|
||||
if attachments.length
|
||||
if currentBoard.allowsBadgeAttachmentOnMinicard
|
||||
.badge
|
||||
span.badge-icon.fa.fa-paperclip
|
||||
span.badge-icon
|
||||
i.fa.fa-paperclip
|
||||
span.badge-text= attachments.length
|
||||
if checklists.length
|
||||
.badge(class="{{#if checklistFinished}}is-finished{{/if}}")
|
||||
span.badge-icon.fa.fa-check-square-o
|
||||
span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
|
||||
if allSubtasks.count
|
||||
.badge
|
||||
span.badge-icon.fa.fa-sitemap
|
||||
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
|
||||
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
|
||||
if showSubtasks
|
||||
if allSubtasks.count
|
||||
.badge
|
||||
span.badge-icon
|
||||
i.fa.fa-globe
|
||||
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
|
||||
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
|
||||
if currentBoard.allowsCardSortingByNumber
|
||||
if currentBoard.allowsCardSortingByNumberOnMinicard
|
||||
.badge
|
||||
span.badge-icon.fa.fa-sort
|
||||
span.badge-icon
|
||||
i.fa.fa-sort-numeric-asc
|
||||
span.badge-text.check-list-sort {{ sort }}
|
||||
if currentBoard.allowsDescriptionTextOnMinicard
|
||||
if getDescription
|
||||
.minicard-description
|
||||
+viewer
|
||||
| {{ getDescription }}
|
||||
if shouldShowListOnMinicard
|
||||
.minicard-list-name
|
||||
i.fa.fa-list
|
||||
| {{ listName }}
|
||||
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentString ' > ' }}
|
||||
if $eq 'subtext-with-parent' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentCardName }}
|
||||
|
||||
template(name="editCardSortOrderPopup")
|
||||
input.js-edit-card-sort-popup(type='text' autofocus value=sort dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-sort-popup(type="submit") {{_ 'save'}}
|
||||
|
||||
template(name="minicardDetailsActionsPopup")
|
||||
ul.pop-over-list
|
||||
if canModifyCard
|
||||
li
|
||||
a.js-move-card
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
li
|
||||
a.js-copy-card
|
||||
i.fa.fa-copy
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
hr
|
||||
li
|
||||
a.js-archive
|
||||
i.fa.fa-arrow-right
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archive-card'}}
|
||||
hr
|
||||
li
|
||||
a.js-move-card-to-top
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
li
|
||||
a.js-add-labels
|
||||
i.fa.fa-tags
|
||||
| {{_ 'card-edit-labels'}}
|
||||
li
|
||||
a.js-due-date
|
||||
i.fa.fa-sign-in
|
||||
| {{_ 'editCardDueDatePopup-title'}}
|
||||
li
|
||||
a.js-set-card-color
|
||||
i.fa.fa-paint-brush
|
||||
| {{_ 'setCardColorPopup-title'}}
|
||||
li
|
||||
a.js-link
|
||||
i.fa.fa-link
|
||||
| {{_ 'link-card'}}
|
||||
li
|
||||
a.js-toggle-watch-card
|
||||
if isWatching
|
||||
i.fa.fa-eye
|
||||
| {{_ 'unwatch'}}
|
||||
template(name="minicardChecklist")
|
||||
.minicard-checklist
|
||||
.checklist-header
|
||||
if canModifyCard
|
||||
+inlinedForm(classNames="js-edit-checklist-title" checklist=checklist)
|
||||
+editChecklistForm(checklist=checklist)
|
||||
else
|
||||
i.fa.fa-eye-slash
|
||||
| {{_ 'watch'}}
|
||||
h4.checklist-title.js-open-inlined-form.is-editable= checklist.title
|
||||
else
|
||||
h4.checklist-title= checklist.title
|
||||
if canModifyCard
|
||||
a.checklist-menu.js-open-checklist-menu(title="{{_ 'checklistActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
.checklist-items.js-checklist-items
|
||||
each visibleItems
|
||||
+inlinedForm(classNames="js-edit-checklist-item" item=. checklist=checklist)
|
||||
+editChecklistItemForm(type='item' item=. checklist=checklist)
|
||||
else
|
||||
+checklistItemDetail(item=. checklist=checklist card=card)
|
||||
if canModifyCard
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist=checklist)
|
||||
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=false)
|
||||
else
|
||||
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
+298
-175
@@ -3,18 +3,27 @@ import { TAPi18n } from '/imports/i18n';
|
||||
import { CustomFieldStringTemplate } from '/client/lib/customFields';
|
||||
import { handleFileUpload } from './attachments';
|
||||
import uploadProgressManager from '../../lib/uploadProgressManager';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import ChecklistItems from '/models/checklistItems';
|
||||
|
||||
function getMinicardFlag(board, onMinicardField, legacyField, defaultValue) {
|
||||
if (!board) return false;
|
||||
if (board[onMinicardField] !== null && board[onMinicardField] !== undefined) {
|
||||
return board[onMinicardField];
|
||||
}
|
||||
if (legacyField && board[legacyField] !== null && board[legacyField] !== undefined) {
|
||||
return board[legacyField];
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// Template.cards.events({
|
||||
// 'click .member': Popup.open('cardMember')
|
||||
// });
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'minicard';
|
||||
},
|
||||
|
||||
Template.minicard.helpers({
|
||||
formattedCurrencyCustomFieldValue(definition) {
|
||||
const customField = this.data()
|
||||
const customField = this
|
||||
.customFieldsWD()
|
||||
.find(f => f._id === definition._id);
|
||||
const customFieldTrueValue =
|
||||
@@ -28,7 +37,7 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
formattedStringtemplateCustomFieldValue(definition) {
|
||||
const customField = this.data()
|
||||
const customField = this
|
||||
.customFieldsWD()
|
||||
.find(f => f._id === definition._id);
|
||||
|
||||
@@ -41,7 +50,7 @@ BlazeComponent.extendComponent({
|
||||
|
||||
showCreatorOnMinicard() {
|
||||
// cache "board" to reduce the mini-mongodb access
|
||||
const board = this.data().board();
|
||||
const board = this.board();
|
||||
let ret = false;
|
||||
if (board) {
|
||||
ret = board.allowsCreatorOnMinicard ?? false;
|
||||
@@ -49,125 +58,47 @@ BlazeComponent.extendComponent({
|
||||
return ret;
|
||||
},
|
||||
isWatching() {
|
||||
const card = this.currentData();
|
||||
return card.findWatcher(Meteor.userId());
|
||||
return this.findWatcher(Meteor.userId());
|
||||
},
|
||||
|
||||
showMembers() {
|
||||
// cache "board" to reduce the mini-mongodb access
|
||||
const board = this.data().board();
|
||||
let ret = false;
|
||||
if (board) {
|
||||
ret =
|
||||
board.allowsMembers === null ||
|
||||
board.allowsMembers === undefined ||
|
||||
board.allowsMembers
|
||||
;
|
||||
}
|
||||
return ret;
|
||||
const board = this.board();
|
||||
return getMinicardFlag(board, 'allowsMembersOnMinicard', 'allowsMembers', true);
|
||||
},
|
||||
|
||||
showAssignee() {
|
||||
// cache "board" to reduce the mini-mongodb access
|
||||
const board = this.data().board();
|
||||
let ret = false;
|
||||
if (board) {
|
||||
ret =
|
||||
board.allowsAssignee === null ||
|
||||
board.allowsAssignee === undefined ||
|
||||
board.allowsAssignee
|
||||
;
|
||||
}
|
||||
return ret;
|
||||
const board = this.board();
|
||||
return getMinicardFlag(board, 'allowsAssigneeOnMinicard', 'allowsAssignee', true);
|
||||
},
|
||||
showReceived() {
|
||||
const board = this.board();
|
||||
return getMinicardFlag(board, 'allowsReceivedDateOnMinicard', 'allowsReceivedDate', true);
|
||||
},
|
||||
showStart() {
|
||||
const board = this.board();
|
||||
return getMinicardFlag(board, 'allowsStartDateOnMinicard', 'allowsStartDate', true);
|
||||
},
|
||||
showDue() {
|
||||
const board = this.board();
|
||||
return getMinicardFlag(board, 'allowsDueDateOnMinicard', 'allowsDueDate', true);
|
||||
},
|
||||
showEnd() {
|
||||
const board = this.board();
|
||||
return getMinicardFlag(board, 'allowsEndDateOnMinicard', 'allowsEndDate', true);
|
||||
},
|
||||
showLabels() {
|
||||
const board = this.board();
|
||||
return getMinicardFlag(board, 'allowsLabelsOnMinicard', 'allowsLabels', true);
|
||||
},
|
||||
showCardNumber() {
|
||||
const board = this.board();
|
||||
return getMinicardFlag(board, 'allowsCardNumberOnMinicard', 'allowsCardNumber', false);
|
||||
},
|
||||
showSubtasks() {
|
||||
const board = this.board();
|
||||
return getMinicardFlag(board, 'allowsSubtasksOnMinicard', 'allowsSubtasks', true);
|
||||
},
|
||||
|
||||
/** opens the card label popup only if clicked onto a label
|
||||
* <li> this is necessary to have the data context of the minicard.
|
||||
* if .js-card-label is used at click event, then only the data context of the label itself is available at this.currentData()
|
||||
*/
|
||||
cardLabelsPopup(event) {
|
||||
if (this.find('.js-card-label:hover')) {
|
||||
Popup.open("cardLabels")(event, {dataContextIfCurrentDataIsUndefined: this.currentData()});
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-linked-link'() {
|
||||
if (this.data().isLinkedCard()) Utils.goCardId(this.data().linkedId);
|
||||
else if (this.data().isLinkedBoard())
|
||||
Utils.goBoardId(this.data().linkedId);
|
||||
},
|
||||
'click .js-toggle-minicard-label-text'() {
|
||||
if (window.localStorage.getItem('hiddenMinicardLabelText')) {
|
||||
window.localStorage.removeItem('hiddenMinicardLabelText'); //true
|
||||
} else {
|
||||
window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true
|
||||
}
|
||||
},
|
||||
'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
|
||||
'click .minicard-labels' : this.cardLabelsPopup,
|
||||
'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
|
||||
// Drag and drop file upload handlers
|
||||
'dragover .minicard'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
'dragenter .minicard'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const card = this.data();
|
||||
const board = card.board();
|
||||
// Only allow drag-and-drop if user can modify card and board allows attachments
|
||||
if (card.canModifyCard() && board && board.allowsAttachments) {
|
||||
// Check if the drag contains files
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
$(event.currentTarget).addClass('is-dragging-over');
|
||||
}
|
||||
}
|
||||
},
|
||||
'dragleave .minicard'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
},
|
||||
'drop .minicard'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
|
||||
const card = this.data();
|
||||
const board = card.board();
|
||||
|
||||
// Check permissions
|
||||
if (!card.canModifyCard() || !board || !board.allowsAttachments) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a file drop (not a card reorder)
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the drop contains files (not just text/HTML)
|
||||
if (!dataTransfer.types.includes('Files')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileUpload(card, files);
|
||||
}
|
||||
},
|
||||
}
|
||||
];
|
||||
},
|
||||
}).register('minicard');
|
||||
|
||||
Template.minicard.helpers({
|
||||
hiddenMinicardLabelText() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (currentUser) {
|
||||
@@ -178,15 +109,24 @@ Template.minicard.helpers({
|
||||
return false;
|
||||
}
|
||||
},
|
||||
cover() {
|
||||
if (!this.coverId) return null;
|
||||
const attachment = ReactiveCache.getAttachment(this.coverId);
|
||||
if (!attachment) return null;
|
||||
const coverLink = typeof attachment.link === 'function' ? attachment.link() : '';
|
||||
if (!coverLink) return null;
|
||||
return {
|
||||
link() {
|
||||
return coverLink;
|
||||
},
|
||||
};
|
||||
},
|
||||
// XXX resolve this nasty hack for https://github.com/veliovgroup/Meteor-Files/issues/763
|
||||
sess() {
|
||||
return Meteor.connection && Meteor.connection._lastSessionId
|
||||
? Meteor.connection._lastSessionId
|
||||
: null;
|
||||
},
|
||||
isWatching() {
|
||||
return this.findWatcher(Meteor.userId());
|
||||
},
|
||||
// Upload progress helpers
|
||||
hasActiveUploads() {
|
||||
return uploadProgressManager.hasActiveUploads(this._id);
|
||||
@@ -196,65 +136,248 @@ Template.minicard.helpers({
|
||||
},
|
||||
uploadCount() {
|
||||
return uploadProgressManager.getUploadCountForCard(this._id);
|
||||
},
|
||||
listName() {
|
||||
const list = this.list();
|
||||
return list ? list.title : '';
|
||||
},
|
||||
|
||||
shouldShowListOnMinicard() {
|
||||
// Show list name if either:
|
||||
// 1. Board-wide setting is enabled, OR
|
||||
// 2. This specific card has the setting enabled
|
||||
const currentBoard = this.board();
|
||||
if (!currentBoard) return false;
|
||||
return currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
|
||||
},
|
||||
|
||||
shouldShowChecklistAtMinicard() {
|
||||
// Return checklists that should be shown on minicard
|
||||
const currentBoard = this.board();
|
||||
if (!currentBoard) return [];
|
||||
|
||||
const checklists = this.checklists();
|
||||
const visibleChecklists = [];
|
||||
|
||||
checklists.forEach(checklist => {
|
||||
// Show checklist if either:
|
||||
// 1. Board-wide setting is enabled, OR
|
||||
// 2. This specific checklist has the setting enabled
|
||||
if (currentBoard.allowsChecklistAtMinicard || checklist.showChecklistAtMinicard) {
|
||||
visibleChecklists.push(checklist);
|
||||
}
|
||||
});
|
||||
|
||||
return visibleChecklists;
|
||||
}
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'keydown input.js-edit-card-sort-popup'(evt) {
|
||||
// enter = save
|
||||
if (evt.keyCode === 13) {
|
||||
this.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
'click button.js-submit-edit-card-sort-popup'(event) {
|
||||
// save button pressed
|
||||
event.preventDefault();
|
||||
const sort = this.$('.js-edit-card-sort-popup')[0]
|
||||
.value
|
||||
.trim();
|
||||
if (!Number.isNaN(sort)) {
|
||||
let card = this.data();
|
||||
card.move(card.boardId, card.swimlaneId, card.listId, sort);
|
||||
Popup.back();
|
||||
}
|
||||
},
|
||||
Template.minicard.events({
|
||||
'click .js-linked-link'() {
|
||||
if (this.isLinkedCard()) Utils.goCardId(this.linkedId);
|
||||
else if (this.isLinkedBoard())
|
||||
Utils.goBoardId(this.linkedId);
|
||||
},
|
||||
'click .js-toggle-card-complete'(event) {
|
||||
// Trello-style "mark complete" toggle (left of the title). Don't let the
|
||||
// click open the card.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!Utils.canModifyCard()) {
|
||||
return;
|
||||
}
|
||||
this.setDueComplete(!this.getDueComplete());
|
||||
},
|
||||
'click .js-toggle-minicard-label-text'() {
|
||||
if (window.localStorage.getItem('hiddenMinicardLabelText')) {
|
||||
window.localStorage.removeItem('hiddenMinicardLabelText'); //true
|
||||
} else {
|
||||
window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true
|
||||
}
|
||||
},
|
||||
'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
|
||||
'click .minicard-labels'(event, tpl) {
|
||||
if (tpl.find('.js-card-label:hover')) {
|
||||
Popup.open("cardLabels")(event, {dataContextIfCurrentDataIsUndefined: Template.currentData()});
|
||||
}
|
||||
},
|
||||
'click .js-open-minicard-details-menu'(event, tpl) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const card = Template.currentData();
|
||||
Popup.open('cardDetailsActions').call({currentData: () => card}, event);
|
||||
},
|
||||
// Drag and drop file upload handlers
|
||||
'dragover .minicard'(event) {
|
||||
// Only prevent default for file drags to avoid interfering with sortable
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
'dragenter .minicard'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const card = this;
|
||||
const board = card.board();
|
||||
// Only allow drag-and-drop if user can modify card and board allows attachments
|
||||
if (Utils.canModifyCard() && board && board.allowsAttachments) {
|
||||
$(event.currentTarget).addClass('is-dragging-over');
|
||||
}
|
||||
]
|
||||
}
|
||||
}).register('editCardSortOrderPopup');
|
||||
}
|
||||
},
|
||||
'dragleave .minicard'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
}
|
||||
},
|
||||
'drop .minicard'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
|
||||
Template.minicardDetailsActionsPopup.events({
|
||||
'click .js-due-date': Popup.open('editCardDueDate'),
|
||||
'click .js-move-card': Popup.open('moveCard'),
|
||||
'click .js-copy-card': Popup.open('copyCard'),
|
||||
'click .js-set-card-color': Popup.open('setCardColor'),
|
||||
'click .js-add-labels': Popup.open('cardLabels'),
|
||||
'click .js-link': Popup.open('linkCard'),
|
||||
'click .js-move-card-to-top'(event) {
|
||||
event.preventDefault();
|
||||
const minOrder = this.getMinSort();
|
||||
this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
|
||||
Popup.back();
|
||||
const card = this;
|
||||
const board = card.board();
|
||||
|
||||
// Check permissions
|
||||
if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a file drop (not a card reorder)
|
||||
if (!dataTransfer.files || dataTransfer.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileUpload(card, files);
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-move-card-to-bottom'(event) {
|
||||
event.preventDefault();
|
||||
const maxOrder = this.getMaxSort();
|
||||
this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1);
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-archive': Popup.afterConfirm('cardArchive', function () {
|
||||
Popup.close();
|
||||
this.archive();
|
||||
Utils.goBoardId(this.boardId);
|
||||
}),
|
||||
'click .js-toggle-watch-card'() {
|
||||
const currentCard = this;
|
||||
const level = currentCard.findWatcher(Meteor.userId()) ? null : 'watching';
|
||||
Meteor.call('watch', 'card', currentCard._id, level, (err, ret) => {
|
||||
if (!err && ret) Popup.back();
|
||||
});
|
||||
|
||||
Template.minicardChecklist.helpers({
|
||||
visibleItems() {
|
||||
const checklist = this.checklist || this;
|
||||
const items = checklist.items();
|
||||
|
||||
return items.filter(item => {
|
||||
// Hide finished items if hideCheckedChecklistItems is true
|
||||
if (item.isFinished && checklist.hideCheckedChecklistItems) {
|
||||
return false;
|
||||
}
|
||||
// Hide all items if hideAllChecklistItems is true
|
||||
if (checklist.hideAllChecklistItems) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Template.minicardChecklist.events({
|
||||
'click .js-convert-checklist-item-to-card'(event) {
|
||||
event.stopPropagation();
|
||||
const formData = Blaze.getData(event.currentTarget);
|
||||
if (!formData) return;
|
||||
const context = { currentData: () => formData };
|
||||
Popup.open('convertChecklistItemToCard').call(context, event);
|
||||
},
|
||||
'click .js-open-checklist-menu'(event) {
|
||||
const data = Template.currentData();
|
||||
const checklist = data.checklist || data;
|
||||
const card = data.card || this;
|
||||
const context = { currentData: () => ({ checklist, card }) };
|
||||
Popup.open('checklistActions').call(context, event);
|
||||
},
|
||||
'click .js-checklist-item .check-box-container'(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const data = Blaze.getData(event.target) || Blaze.getData(event.currentTarget);
|
||||
const item = data && data.item;
|
||||
if (item && item._id) {
|
||||
item.toggleItem();
|
||||
}
|
||||
},
|
||||
'click .js-submit-edit-checklist-item-form'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const $btn = $(event.currentTarget);
|
||||
const $form = $btn.closest('form');
|
||||
const textarea = $form.find('textarea.js-edit-checklist-item')[0];
|
||||
if (!textarea) return;
|
||||
const title = textarea.value.trim();
|
||||
if (!title) return;
|
||||
const formData = Blaze.getData($form[0]);
|
||||
if (formData && formData.item && formData.item._id) {
|
||||
formData.item.setTitle(title);
|
||||
} else if (formData && formData.checklist && formData.checklist._id) {
|
||||
formData.checklist.setTitle(title);
|
||||
}
|
||||
$form.find('.js-close-inlined-form').trigger('click');
|
||||
},
|
||||
'submit .js-add-checklist-item'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = tpl.find('textarea.js-add-checklist-item');
|
||||
if (!textarea) return;
|
||||
const title = textarea.value.trim();
|
||||
const data = Template.currentData() || {};
|
||||
const checklist = data.checklist;
|
||||
if (!checklist || !title) return;
|
||||
const items = checklist.items();
|
||||
const lastItem = items[items.length - 1] || null;
|
||||
const sortIndex = Utils.calculateIndexData(lastItem, null).base;
|
||||
ChecklistItems.insert({
|
||||
title,
|
||||
checklistId: checklist._id,
|
||||
cardId: checklist.cardId,
|
||||
sort: sortIndex,
|
||||
});
|
||||
textarea.value = '';
|
||||
textarea.focus();
|
||||
},
|
||||
'click .js-delete-checklist-item': Popup.afterConfirm('checklistItemDelete', function () {
|
||||
Popup.back();
|
||||
const item = this && this.item ? this.item : this;
|
||||
if (item && item._id) {
|
||||
ChecklistItems.remove(item._id);
|
||||
}
|
||||
}),
|
||||
'keydown textarea.js-add-checklist-item'(event) {
|
||||
if (event.keyCode === 13 && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
const $form = $(event.currentTarget).closest('form');
|
||||
$form.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.editCardSortOrderPopup.events({
|
||||
'keydown input.js-edit-card-sort-popup'(evt, tpl) {
|
||||
// enter = save
|
||||
if (evt.keyCode === 13) {
|
||||
tpl.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
'click button.js-submit-edit-card-sort-popup'(event, tpl) {
|
||||
// save button pressed
|
||||
event.preventDefault();
|
||||
const sort = tpl.$('.js-edit-card-sort-popup')[0]
|
||||
.value
|
||||
.trim();
|
||||
if (!Number.isNaN(sort)) {
|
||||
let card = this;
|
||||
card.move(card.boardId, card.swimlaneId, card.listId, sort);
|
||||
Popup.back();
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
.minicard-title .minicard-complete-toggle {
|
||||
display: none;
|
||||
}
|
||||
@@ -11,7 +11,7 @@ template(name="resultCard")
|
||||
= getBoard.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| NULL
|
||||
| {{_ 'no-name'}}
|
||||
if getBoard.archived
|
||||
i.fa.fa-archive
|
||||
li.result-card-context.result-card-context-separator
|
||||
@@ -25,7 +25,7 @@ template(name="resultCard")
|
||||
= getSwimlane.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| NULL
|
||||
| {{_ 'no-name'}}
|
||||
if getSwimlane.archived
|
||||
i.fa.fa-archive
|
||||
li.result-card-context.result-card-context-separator
|
||||
@@ -39,6 +39,6 @@ template(name="resultCard")
|
||||
= getList.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| NULL
|
||||
| {{_ 'no-name'}}
|
||||
if getList.archived
|
||||
i.fa.fa-archive
|
||||
@@ -1,35 +1,67 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
|
||||
Template.resultCard.helpers({
|
||||
userId() {
|
||||
return Meteor.userId();
|
||||
},
|
||||
getBoard() {
|
||||
const card = Template.currentData();
|
||||
try {
|
||||
if (card && card.boardId) {
|
||||
const board = ReactiveCache.getBoard(card.boardId);
|
||||
return board || null;
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently handle errors
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getSwimlane() {
|
||||
const card = Template.currentData();
|
||||
try {
|
||||
if (card && card.swimlaneId) {
|
||||
const swimlane = ReactiveCache.getSwimlane(card.swimlaneId);
|
||||
return swimlane || null;
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently handle errors
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getList() {
|
||||
const card = Template.currentData();
|
||||
try {
|
||||
if (card && card.listId) {
|
||||
const list = ReactiveCache.getList(card.listId);
|
||||
return list || null;
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently handle errors
|
||||
}
|
||||
return null;
|
||||
},
|
||||
originRelativeUrl() {
|
||||
const card = Template.currentData();
|
||||
if (card && card.boardId && card._id) {
|
||||
return `/board/${card.boardId}?cardId=${card._id}`;
|
||||
}
|
||||
return '#';
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
clickOnMiniCard(evt) {
|
||||
evt.preventDefault();
|
||||
const this_ = this;
|
||||
const cardId = this.currentData()._id;
|
||||
const boardId = this.currentData().boardId;
|
||||
Template.resultCard.events({
|
||||
'click .js-minicard'(event) {
|
||||
event.preventDefault();
|
||||
const cardId = Template.currentData()._id;
|
||||
const boardId = Template.currentData().boardId;
|
||||
Meteor.subscribe('popupCardData', cardId, {
|
||||
onReady() {
|
||||
Session.set('popupCardId', cardId);
|
||||
Session.set('popupCardBoardId', boardId);
|
||||
this_.cardDetailsPopup(evt);
|
||||
if (!Popup.isOpen()) {
|
||||
Popup.open("cardDetails")(event);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
cardDetailsPopup(event) {
|
||||
if (!Popup.isOpen()) {
|
||||
Popup.open("cardDetails")(event);
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-minicard': this.clickOnMiniCard,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('resultCard');
|
||||
});
|
||||
@@ -15,7 +15,7 @@ textarea.js-edit-subtask-item {
|
||||
color: #8c8c8c;
|
||||
text-decoration: underline;
|
||||
word-wrap: break-word;
|
||||
float: right;
|
||||
flex-shrink: 0;
|
||||
padding-top: 6px;
|
||||
}
|
||||
.delete-text:hover,
|
||||
@@ -24,8 +24,13 @@ textarea.js-edit-subtask-item {
|
||||
.js-delete-subtask-item:hover {
|
||||
color: inherit;
|
||||
}
|
||||
.subtask-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.subtask-title .checkbox {
|
||||
float: left;
|
||||
flex-shrink: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 18px;
|
||||
@@ -34,10 +39,12 @@ textarea.js-edit-subtask-item {
|
||||
.subtask-title .title {
|
||||
font-size: 18px;
|
||||
line-height: 25px;
|
||||
flex: 1;
|
||||
order: 1;
|
||||
}
|
||||
.subtask-title .subtasks-stat {
|
||||
margin: 0 0.5em;
|
||||
float: right;
|
||||
flex-shrink: 0;
|
||||
padding-top: 6px;
|
||||
}
|
||||
.subtask-title .subtasks-stat.is-finished {
|
||||
@@ -46,6 +53,9 @@ textarea.js-edit-subtask-item {
|
||||
.subtask-title .js-delete-subtask {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
.subtask-title .js-view-subtask {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
.js-confirm-subtask-delete {
|
||||
background-color: #f7f7f7;
|
||||
position: absolute;
|
||||
@@ -87,6 +97,15 @@ textarea.js-edit-subtask-item {
|
||||
top: 0;
|
||||
bottom: -600px;
|
||||
right: 0;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
/* Fix for mobile Safari: ensure this doesn't block card interaction */
|
||||
@media screen and (max-width: 800px) {
|
||||
#card-details-overlay {
|
||||
z-index: 15;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
.subtasks {
|
||||
background: #f7f7f7;
|
||||
@@ -120,6 +139,9 @@ textarea.js-edit-subtask-item {
|
||||
.subtasks-item:hover {
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
.subtasks-item .check-box-container {
|
||||
padding-right: 10px;
|
||||
}
|
||||
.subtasks-item .check-box {
|
||||
margin: 0.1em 0 0 0;
|
||||
}
|
||||
@@ -127,6 +149,25 @@ textarea.js-edit-subtask-item {
|
||||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
}
|
||||
/* Unicode checkbox icons styling */
|
||||
.subtasks-item .check-box-unicode {
|
||||
font-size: 1.3em;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .subtasks-item .check-box.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
body.grey-icons-enabled .subtasks-item .check-box-unicode {
|
||||
filter: grayscale(100%);
|
||||
-webkit-filter: grayscale(100%);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.subtasks-item .item-title {
|
||||
flex: 1;
|
||||
padding-left: 10px;
|
||||
@@ -147,6 +188,7 @@ textarea.js-edit-subtask-item {
|
||||
display: inline-block;
|
||||
}
|
||||
.subtask-details-menu {
|
||||
float: right;
|
||||
flex-shrink: 0;
|
||||
padding: 6px 10px 6px 10px;
|
||||
order: 2;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
template(name="subtasks")
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sitemap
|
||||
i.fa.fa-globe
|
||||
| {{_ 'subtasks'}}
|
||||
if currentUser.isBoardAdmin
|
||||
if toggleDeleteDialog.get
|
||||
@@ -40,7 +40,7 @@ template(name="addSubtaskItemForm")
|
||||
textarea.js-add-subtask-item(rows='1' autofocus dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-add-subtask-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
|
||||
|
||||
template(name="editSubtaskItemForm")
|
||||
textarea.js-edit-subtask-item(rows='1' autofocus dir="auto")
|
||||
@@ -50,8 +50,8 @@ template(name="editSubtaskItemForm")
|
||||
= subtask.title
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-subtask-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
span(title=createdAt) {{ moment createdAt }}
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
|
||||
span(title=createdAt) {{ displayDate createdAt }}
|
||||
if canModifyCard
|
||||
if currentUser.isBoardAdmin
|
||||
a.js-delete-subtask-item {{_ "delete"}}...
|
||||
@@ -74,12 +74,13 @@ template(name="subtasksItems")
|
||||
template(name='subtaskItemDetail')
|
||||
.js-subtasks-item.subtasks-item
|
||||
if canModifyCard
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
.check-box-container
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished}}is-checked{{/if}}")
|
||||
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
else
|
||||
.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if item.isFinished}}is-checked{{/if}}")
|
||||
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
@@ -94,6 +95,10 @@ template(name="subtaskActionsPopup")
|
||||
a.js-view-subtask(title="{{ subtask.title }}")
|
||||
i.fa.fa-eye
|
||||
| {{_ "view-it"}}
|
||||
li
|
||||
a.js-go-to-subtask-board
|
||||
i.fa.fa-folder
|
||||
| {{_ "go-to-board"}}
|
||||
if currentUser.isBoardAdmin
|
||||
a.js-delete-subtask.delete-subtask
|
||||
i.fa.fa-trash
|
||||
|
||||
@@ -1,16 +1,35 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import Cards from '/models/cards';
|
||||
import { Filter } from '/client/lib/filter';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
addSubtask(event) {
|
||||
Template.subtasks.events({
|
||||
'click .js-open-subtask-details-menu'(event) {
|
||||
// Close any existing popup first to avoid accumulating content
|
||||
if (Popup.isOpen()) {
|
||||
Popup.close();
|
||||
}
|
||||
// Now open the popup for this specific subtask
|
||||
Popup.open('subtaskActions').call(this, event);
|
||||
},
|
||||
async 'submit .js-add-subtask'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-add-subtask-item');
|
||||
const textarea = tpl.find('textarea.js-add-subtask-item');
|
||||
const title = textarea.value.trim();
|
||||
const cardId = this.currentData().cardId;
|
||||
const cardId = Template.currentData().cardId;
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
const sortIndex = -1;
|
||||
const crtBoard = ReactiveCache.getBoard(card.boardId);
|
||||
const targetBoard = crtBoard.getDefaultSubtasksBoard();
|
||||
const listId = targetBoard.getDefaultSubtasksListId();
|
||||
const targetBoard = await crtBoard.getDefaultSubtasksBoardAsync();
|
||||
if (!targetBoard) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetList = await targetBoard.getDefaultSubtasksListAsync();
|
||||
if (!targetList) {
|
||||
return;
|
||||
}
|
||||
const listId = targetList._id;
|
||||
|
||||
//Get the full swimlane data for the parent task.
|
||||
const parentSwimlane = ReactiveCache.getSwimlane({
|
||||
@@ -18,20 +37,22 @@ BlazeComponent.extendComponent({
|
||||
_id: card.swimlaneId,
|
||||
});
|
||||
//find the swimlane of the same name in the target board.
|
||||
const targetSwimlane = ReactiveCache.getSwimlane({
|
||||
boardId: targetBoard._id,
|
||||
title: parentSwimlane.title,
|
||||
});
|
||||
const targetSwimlane = parentSwimlane
|
||||
? ReactiveCache.getSwimlane({
|
||||
boardId: targetBoard._id,
|
||||
title: parentSwimlane.title,
|
||||
})
|
||||
: undefined;
|
||||
//If no swimlane with a matching title exists in the target board, fall back to the default swimlane.
|
||||
const swimlaneId =
|
||||
targetSwimlane === undefined
|
||||
? targetBoard.getDefaultSwimline()._id
|
||||
: targetSwimlane._id;
|
||||
|
||||
const nextCardNumber = targetBoard.getNextCardNumber();
|
||||
const nextCardNumber = await targetBoard.getNextCardNumber();
|
||||
|
||||
if (title) {
|
||||
const _id = Cards.insert({
|
||||
const _id = await Cards.insertAsync({
|
||||
title,
|
||||
parentId: cardId,
|
||||
members: [],
|
||||
@@ -42,7 +63,7 @@ BlazeComponent.extendComponent({
|
||||
sort: sortIndex,
|
||||
swimlaneId,
|
||||
type: 'cardType-card',
|
||||
cardNumber: nextCardNumber
|
||||
cardNumber: nextCardNumber,
|
||||
});
|
||||
|
||||
// In case the filter is active we need to add the newly inserted card in
|
||||
@@ -52,7 +73,7 @@ BlazeComponent.extendComponent({
|
||||
Filter.addException(_id);
|
||||
|
||||
setTimeout(() => {
|
||||
this.$('.add-subtask-item')
|
||||
tpl.$('.add-subtask-item')
|
||||
.last()
|
||||
.click();
|
||||
}, 100);
|
||||
@@ -60,27 +81,20 @@ BlazeComponent.extendComponent({
|
||||
textarea.value = '';
|
||||
textarea.focus();
|
||||
},
|
||||
|
||||
deleteSubtask() {
|
||||
const subtask = this.currentData().subtask;
|
||||
if (subtask && subtask._id) {
|
||||
subtask.archive();
|
||||
}
|
||||
},
|
||||
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
|
||||
editSubtask(event) {
|
||||
'submit .js-edit-subtask-title'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-edit-subtask-item');
|
||||
const textarea = tpl.find('textarea.js-edit-subtask-item');
|
||||
const title = textarea.value.trim();
|
||||
const subtask = this.currentData().subtask;
|
||||
const subtask = Template.currentData().subtask;
|
||||
subtask.setTitle(title);
|
||||
},
|
||||
|
||||
pressKey(event) {
|
||||
async 'click .js-delete-subtask-item'() {
|
||||
const subtask = Template.currentData().subtask;
|
||||
if (subtask && subtask._id) {
|
||||
await subtask.archive();
|
||||
}
|
||||
},
|
||||
keydown(event) {
|
||||
//If user press enter key inside a form, submit it
|
||||
//Unless the user is also holding down the 'shift' key
|
||||
if (event.keyCode === 13 && !event.shiftKey) {
|
||||
@@ -89,53 +103,66 @@ BlazeComponent.extendComponent({
|
||||
$form.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-open-subtask-details-menu': Popup.open('subtaskActions'),
|
||||
'submit .js-add-subtask': this.addSubtask,
|
||||
'submit .js-edit-subtask-title': this.editSubtask,
|
||||
'click .js-delete-subtask-item': this.deleteSubtask,
|
||||
keydown: this.pressKey,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('subtasks');
|
||||
Template.subtasks.onCreated(function () {
|
||||
this.toggleDeleteDialog = new ReactiveVar(false);
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
// ...
|
||||
}).register('subtaskItemDetail');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.subtasks.helpers({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-view-subtask'(event) {
|
||||
if ($(event.target).hasClass('js-view-subtask')) {
|
||||
const subtask = this.currentData().subtask;
|
||||
const board = subtask.board();
|
||||
FlowRouter.go('card', {
|
||||
boardId: board._id,
|
||||
slug: board.slug,
|
||||
cardId: subtask._id,
|
||||
});
|
||||
}
|
||||
},
|
||||
'click .js-delete-subtask' : Popup.afterConfirm('subtaskDelete', function () {
|
||||
Popup.back(2);
|
||||
const subtask = this.subtask;
|
||||
if (subtask && subtask._id) {
|
||||
subtask.archive();
|
||||
}
|
||||
}),
|
||||
}
|
||||
]
|
||||
}
|
||||
}).register('subtaskActionsPopup');
|
||||
toggleDeleteDialog() {
|
||||
return Template.instance().toggleDeleteDialog;
|
||||
},
|
||||
});
|
||||
|
||||
Template.subtaskItemDetail.events({
|
||||
async 'click .js-subtasks-item .check-box-container'() {
|
||||
const item = Template.currentData().item;
|
||||
if (item && item._id) {
|
||||
await item.toggleItem();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.subtaskActionsPopup.helpers({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
});
|
||||
|
||||
Template.subtaskActionsPopup.events({
|
||||
'click .js-view-subtask'(event) {
|
||||
if ($(event.target).hasClass('js-view-subtask')) {
|
||||
const subtask = Template.currentData().subtask;
|
||||
const board = subtask.board();
|
||||
FlowRouter.go('card', {
|
||||
boardId: board._id,
|
||||
slug: board.slug,
|
||||
cardId: subtask._id,
|
||||
swimlaneId: subtask.swimlaneId,
|
||||
listId: subtask.listId,
|
||||
});
|
||||
}
|
||||
},
|
||||
'click .js-go-to-subtask-board'() {
|
||||
const subtask = Template.currentData().subtask;
|
||||
const board = subtask.board();
|
||||
if (board) {
|
||||
Popup.close();
|
||||
FlowRouter.go('board', { id: board._id, slug: board.slug });
|
||||
}
|
||||
},
|
||||
'click .js-delete-subtask' : Popup.afterConfirm('subtaskDelete', async function () {
|
||||
Popup.back(2);
|
||||
const subtask = this.subtask;
|
||||
if (subtask && subtask._id) {
|
||||
await subtask.archive();
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
Template.editSubtaskItemForm.helpers({
|
||||
user() {
|
||||
@@ -145,5 +172,3 @@ Template.editSubtaskItemForm.helpers({
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/* Original Position Component Styles */
|
||||
.original-position-info {
|
||||
margin: 5px 0;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.original-position-loading {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.original-position-loading i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.original-position-details {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 3px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.original-position-moved {
|
||||
color: #856404;
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 3px;
|
||||
padding: 4px 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.original-position-moved i {
|
||||
color: #f39c12;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.original-position-unchanged {
|
||||
color: #155724;
|
||||
background-color: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
border-radius: 3px;
|
||||
padding: 4px 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.original-position-unchanged i {
|
||||
color: #28a745;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.original-position-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.original-title {
|
||||
color: #6c757d;
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.original-title strong {
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
/* Integration with existing Wekan styles */
|
||||
.swimlane .original-position-info,
|
||||
.list .original-position-info,
|
||||
.card .original-position-info {
|
||||
margin: 2px 0;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.original-position-info {
|
||||
font-size: 11px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.original-position-details {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.original-position-moved,
|
||||
.original-position-unchanged {
|
||||
padding: 3px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.original-position-details {
|
||||
background-color: #2d3748;
|
||||
border-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.original-position-moved {
|
||||
background-color: #744210;
|
||||
border-color: #b7791f;
|
||||
color: #fbd38d;
|
||||
}
|
||||
|
||||
.original-position-unchanged {
|
||||
background-color: #22543d;
|
||||
border-color: #38a169;
|
||||
color: #9ae6b4;
|
||||
}
|
||||
|
||||
.original-title {
|
||||
color: #a0aec0;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
.original-title strong {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
template(name="originalPosition")
|
||||
.original-position-info
|
||||
if isLoading
|
||||
.original-position-loading
|
||||
| ⏳ Loading original position...
|
||||
else
|
||||
if showOriginalPosition
|
||||
.original-position-details
|
||||
if hasMovedFromOriginal
|
||||
.original-position-moved
|
||||
span.original-position-text ℹ️ {{getOriginalPositionDescription}}
|
||||
else
|
||||
.original-position-unchanged
|
||||
span.original-position-text ✅ In original position
|
||||
|
||||
if getOriginalTitle
|
||||
.original-title
|
||||
strong Original title:
|
||||
| {{getOriginalTitle}}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
/**
|
||||
* Component to display original position information for swimlanes, lists, and cards
|
||||
*/
|
||||
|
||||
Template.originalPosition.onCreated(function () {
|
||||
this.originalPosition = new ReactiveVar(null);
|
||||
this.isLoading = new ReactiveVar(false);
|
||||
this.hasMoved = new ReactiveVar(false);
|
||||
|
||||
const tpl = this;
|
||||
|
||||
function loadOriginalPosition(entityId, entityType) {
|
||||
tpl.isLoading.set(true);
|
||||
|
||||
const methodName = `positionHistory.get${entityType.charAt(0).toUpperCase() + entityType.slice(1)}OriginalPosition`;
|
||||
|
||||
Meteor.call(methodName, entityId, (error, result) => {
|
||||
tpl.isLoading.set(false);
|
||||
if (error) {
|
||||
console.error('Error loading original position:', error);
|
||||
tpl.originalPosition.set(null);
|
||||
} else {
|
||||
tpl.originalPosition.set(result);
|
||||
|
||||
// Check if the entity has moved
|
||||
const movedMethodName = `positionHistory.has${entityType.charAt(0).toUpperCase() + entityType.slice(1)}Moved`;
|
||||
Meteor.call(movedMethodName, entityId, (movedError, movedResult) => {
|
||||
if (!movedError) {
|
||||
tpl.hasMoved.set(movedResult);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.autorun(() => {
|
||||
const data = Template.currentData();
|
||||
if (data && data.entityId && data.entityType) {
|
||||
loadOriginalPosition(data.entityId, data.entityType);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.originalPosition.helpers({
|
||||
getOriginalPosition() {
|
||||
return Template.instance().originalPosition.get();
|
||||
},
|
||||
|
||||
isLoading() {
|
||||
return Template.instance().isLoading.get();
|
||||
},
|
||||
|
||||
hasMovedFromOriginal() {
|
||||
return Template.instance().hasMoved.get();
|
||||
},
|
||||
|
||||
getOriginalPositionDescription() {
|
||||
const position = Template.instance().originalPosition.get();
|
||||
if (!position) return 'No original position data';
|
||||
|
||||
if (position.originalPosition) {
|
||||
const data = Template.currentData();
|
||||
const entityType = data.entityType;
|
||||
let description = `Original position: ${position.originalPosition.sort || 0}`;
|
||||
|
||||
if (entityType === 'list' && position.originalSwimlaneId) {
|
||||
description += ` in swimlane ${position.originalSwimlaneId}`;
|
||||
} else if (entityType === 'card') {
|
||||
if (position.originalSwimlaneId) {
|
||||
description += ` in swimlane ${position.originalSwimlaneId}`;
|
||||
}
|
||||
if (position.originalListId) {
|
||||
description += ` in list ${position.originalListId}`;
|
||||
}
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
return 'No original position data';
|
||||
},
|
||||
|
||||
getOriginalTitle() {
|
||||
const position = Template.instance().originalPosition.get();
|
||||
return position ? position.originalTitle : '';
|
||||
},
|
||||
|
||||
showOriginalPosition() {
|
||||
return Template.instance().originalPosition.get() !== null;
|
||||
},
|
||||
});
|
||||
@@ -4,11 +4,10 @@ template(name="datepicker")
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="text" name="date" value=showDate placeholder=dateFormat autofocus)
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="text" name="time" value=showTime placeholder=timeFormat)
|
||||
.js-datepicker
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
setupDatePicker,
|
||||
datePickerRendered,
|
||||
datePickerHelpers,
|
||||
datePickerEvents,
|
||||
} from '/client/lib/datepicker';
|
||||
|
||||
Template.datepicker.onCreated(function () {
|
||||
setupDatePicker(this);
|
||||
});
|
||||
|
||||
Template.datepicker.onRendered(function () {
|
||||
datePickerRendered(this);
|
||||
});
|
||||
|
||||
Template.datepicker.helpers(datePickerHelpers());
|
||||
@@ -130,8 +130,8 @@ textarea.editor {
|
||||
}
|
||||
input[type="submit"],
|
||||
button {
|
||||
background: #cfcfcf;
|
||||
background: linear-gradient(#cfcfcf, #c2c2c2);
|
||||
background: #000;
|
||||
background: linear-gradient(#000, #000);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
@@ -139,6 +139,7 @@ button {
|
||||
line-height: 1.3;
|
||||
padding: 1vh 2.5vw;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
input[type="submit"] .wide,
|
||||
button .wide {
|
||||
@@ -149,14 +150,16 @@ input[type="submit"]:hover,
|
||||
button:hover,
|
||||
input[type="submit"]:focus,
|
||||
button:focus {
|
||||
background: #c2c2c2;
|
||||
background: linear-gradient(#c2c2c2, #b5b5b5);
|
||||
background: #222;
|
||||
background: linear-gradient(#222, #222);
|
||||
color: #fff;
|
||||
}
|
||||
input[type="submit"]:active,
|
||||
button:active {
|
||||
background: #b5b5b5;
|
||||
background: linear-gradient(#b5b5b5, #a8a8a8);
|
||||
box-shadow: inset 0 3px 6px rgba(0,0,0,0.1);
|
||||
background: #111;
|
||||
background: linear-gradient(#111, #111);
|
||||
box-shadow: inset 0 3px 6px rgba(0,0,0,0.3);
|
||||
color: #fff;
|
||||
}
|
||||
input[type="submit"]:active:hover,
|
||||
button:active:hover,
|
||||
@@ -183,6 +186,12 @@ input[type="submit"].primary:active,
|
||||
button.primary:active {
|
||||
background: #01628c;
|
||||
}
|
||||
input[type="submit"].negate,
|
||||
button.negate {
|
||||
background: #eb5a46;
|
||||
box-shadow: 0 1px 0 #4d4d4d;
|
||||
color: #fff;
|
||||
}
|
||||
input[type="submit"].negate:hover,
|
||||
button.negate:hover,
|
||||
input[type="submit"].negate:focus,
|
||||
@@ -217,10 +226,10 @@ input[type="submit"]:disabled:active,
|
||||
input[type="button"].disabled:active,
|
||||
button.disabled:active,
|
||||
.button.disabled:active {
|
||||
background: #cfcfcf;
|
||||
background: #555;
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
color: #a8a8a8;
|
||||
color: #999;
|
||||
}
|
||||
fieldset {
|
||||
border: 1px solid #bfbfbf;
|
||||
@@ -315,11 +324,18 @@ textarea::-moz-placeholder {
|
||||
margin-right: 6px;
|
||||
border-top: 2px solid transparent;
|
||||
border-left: 2px solid transparent;
|
||||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
transform: rotate(40deg);
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
transform-origin: 100% 100%;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .materialCheckBox.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
.button-link {
|
||||
background: #fff;
|
||||
background: linear-gradient(#fff, #f5f5f5);
|
||||
@@ -393,12 +409,12 @@ textarea::-moz-placeholder {
|
||||
.button-link.setting.disabled.primary,
|
||||
.button-link.setting.disabled.primary:hover,
|
||||
.button-link.setting.disabled.primary:active {
|
||||
background: #cfcfcf;
|
||||
border-color: #c2c2c2;
|
||||
border-bottom-color: #b5b5b5;
|
||||
background: #555;
|
||||
border-color: #444;
|
||||
border-bottom-color: #333;
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
color: #a8a8a8;
|
||||
color: #999;
|
||||
}
|
||||
.button-link.setting .label {
|
||||
color: #222;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
template(name='inlinedForm')
|
||||
if isOpen.get
|
||||
form.inlined-form.js-inlined-form(id=id class=classNames)
|
||||
+Template.contentBlock
|
||||
else
|
||||
+Template.elseBlock
|
||||
.js-inlined-form-wrapper(style='display: contents;' data-position=position)
|
||||
if isOpen.get
|
||||
form.inlined-form.js-inlined-form(id=id class=classNames)
|
||||
+Template.contentBlock
|
||||
else
|
||||
+Template.elseBlock
|
||||
@@ -0,0 +1,203 @@
|
||||
/* Gantt chart cell background colors for Received, Start, Due, End (matching cardDetails) */
|
||||
.ganttview-received {
|
||||
background-color: #dbdbdb !important;
|
||||
color: #000 !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.ganttview-start {
|
||||
background-color: #90ee90 !important;
|
||||
color: #000 !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.ganttview-due {
|
||||
background-color: #ffd700 !important;
|
||||
color: #000 !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.ganttview-end {
|
||||
background-color: #ffb3b3 !important;
|
||||
color: #000 !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
/* Gantt View Styles */
|
||||
|
||||
.gantt-view {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.gantt-view.swimlane {
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.gantt-container {
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
background-color: #fff;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gantt-container table,
|
||||
.gantt-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
min-width: 800px;
|
||||
border: 2px solid #666;
|
||||
font-family: sans-serif;
|
||||
font-size: 13px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.gantt-container thead {
|
||||
background-color: #e8e8e8;
|
||||
border-bottom: 2px solid #666;
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.gantt-container thead th,
|
||||
.gantt-container thead tr > td:first-child {
|
||||
border-right: 2px solid #666;
|
||||
padding: 4px; /* half of 8px */
|
||||
width: 100px; /* half of 200px */
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
background-color: #e8e8e8;
|
||||
min-width: 100px; /* half of 200px */
|
||||
}
|
||||
|
||||
.gantt-container thead td {
|
||||
border-right: 1px solid #999;
|
||||
padding: 2px 1px; /* half */
|
||||
text-align: center;
|
||||
background-color: #f5f5f5;
|
||||
font-size: 11px;
|
||||
min-width: 15px; /* half of 30px */
|
||||
font-weight: bold;
|
||||
height: auto;
|
||||
line-height: 1.2;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.gantt-container tbody tr {
|
||||
border-bottom: 1px solid #999;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.gantt-container tbody tr:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.gantt-container tbody tr:hover td {
|
||||
background-color: #f9f9f9 !important;
|
||||
}
|
||||
|
||||
.gantt-container tbody td {
|
||||
border-right: 1px solid #ccc;
|
||||
padding: 1px; /* half */
|
||||
text-align: center;
|
||||
min-width: 15px; /* half of 30px */
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
line-height: 28px;
|
||||
background-color: #ffffff;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.gantt-container tbody td:nth-child(even) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.gantt-container tbody td:first-child {
|
||||
border-right: 2px solid #666;
|
||||
padding: 4px; /* half of 8px */
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background-color: #fafafa !important;
|
||||
text-align: left;
|
||||
width: 100px; /* half of 200px */
|
||||
min-width: 100px; /* half of 200px */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.gantt-container tbody td:first-child:hover {
|
||||
background-color: #f0f0f0 !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.js-gantt-task-cell {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.js-gantt-date-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gantt-container .ganttview-weekend {
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
.gantt-container .ganttview-today {
|
||||
background-color: #fcf8e3;
|
||||
border-right: 2px solid #ffb347;
|
||||
}
|
||||
|
||||
/* Task bar styling - VERY VISIBLE */
|
||||
.gantt-container tbody td.ganttview-block {
|
||||
background-color: #4CAF50 !important;
|
||||
color: #fff !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: bold !important;
|
||||
padding: 2px !important;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.gantt-container table {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.gantt-container thead td {
|
||||
min-width: 20px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.gantt-container tbody td {
|
||||
min-width: 20px;
|
||||
padding: 1px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.gantt-container tbody td:first-child {
|
||||
width: 100px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.gantt-container {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.gantt-container table {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//- Gantt Chart View Template
|
||||
template(name="ganttView")
|
||||
link(rel="stylesheet" href="/client/components/gantt/gantt.css")
|
||||
link(rel="stylesheet" href="/client/components/gantt/ganttCard.css")
|
||||
.gantt-view
|
||||
h2 {{_ 'board-view-gantt'}}
|
||||
if hasSelectedCard
|
||||
+ganttCard(selectedCard)
|
||||
each weeks
|
||||
table.gantt-table
|
||||
thead
|
||||
tr
|
||||
th {{_ 'task'}} {{_ 'predicate-week'}} {{week}}
|
||||
each weekDays this
|
||||
th
|
||||
| {{formattedDate .}} {{weekdayLabel .}}
|
||||
tbody
|
||||
each cardsInWeek this
|
||||
tr(data-card-id="{{cardId .}}")
|
||||
td.js-gantt-task-cell
|
||||
a.js-gantt-card-title(href="#")
|
||||
+viewer
|
||||
| {{cardTitle .}}
|
||||
each weekDays ..
|
||||
td(class="{{cellClasses .. .}}" data-card-id="{{cardId ..}}" data-date-type="{{cellContentClass .. .}}")
|
||||
| {{cellContent .. .}}
|
||||
|
||||
Loaded 100 of 1519 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user