mirror of
https://github.com/wekan/wekan.git
synced 2026-11-08 19:35:19 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
8486bb1e87 | ||
|
|
655f44919a | ||
|
|
17ea7978f0 | ||
|
|
b518946c41 | ||
|
|
ca8a4af354 | ||
|
|
1682f8834d | ||
|
|
eac472c54b | ||
|
|
3a873559f8 | ||
|
|
aec6779601 | ||
|
|
973d9eccae | ||
|
|
7468b30001 | ||
|
|
e68cb5f32f | ||
|
|
a157ae5d1c | ||
|
|
620e5ce9dc | ||
|
|
2e3fbf25db | ||
|
|
e8fee653c3 | ||
|
|
0d3f3474d1 | ||
|
|
6ecb6bdaa7 | ||
|
|
073c297490 |
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
-12
@@ -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,10 +123,8 @@
|
||||
"Template": false,
|
||||
"$": false,
|
||||
"_": false,
|
||||
"autosize": false,
|
||||
"Avatar": true,
|
||||
"Avatars": true,
|
||||
|
||||
"CollectionHooks": false,
|
||||
"ESSearchResults": false,
|
||||
"FastRender": false,
|
||||
@@ -111,7 +139,6 @@
|
||||
"Ps": true,
|
||||
"Restivus": false,
|
||||
"SimpleSchema": false,
|
||||
"SubsManager": false,
|
||||
"T9n": false,
|
||||
"TAPi18n": false,
|
||||
"AccountsTemplates": true,
|
||||
@@ -146,7 +173,6 @@
|
||||
"Checklists": true,
|
||||
"Settings": true,
|
||||
"InvitationCodes": true,
|
||||
"JsonRoutes": true,
|
||||
"Authentication": true,
|
||||
"Integrations": true,
|
||||
"HTTP": true,
|
||||
|
||||
@@ -11,4 +11,4 @@ jobs:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
uses: actions/dependency-review-action@v5
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
# 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@d08e5c354a6adb9ed34480a06d141179aa583294
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
name: Playwright E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: playwright-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
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: Install Meteor
|
||||
run: curl https://install.meteor.com/ | sh
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run Meteor unit tests
|
||||
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: Install Meteor
|
||||
run: curl https://install.meteor.com/ | sh
|
||||
|
||||
- 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 &
|
||||
# Wait up to 120 s for WeKan to become available
|
||||
for i in $(seq 1 24); do
|
||||
curl -sf http://localhost:3000 > /dev/null && break
|
||||
echo "Waiting for WeKan... attempt $i"
|
||||
sleep 5
|
||||
done
|
||||
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: Install Meteor
|
||||
run: curl https://install.meteor.com/ | sh
|
||||
|
||||
- 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 &
|
||||
for i in $(seq 1 24); do
|
||||
curl -sf http://localhost:3000 > /dev/null && break
|
||||
sleep 5
|
||||
done
|
||||
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,487 @@
|
||||
name: Release All Platforms
|
||||
|
||||
# Triggered manually via GitHub CLI or Actions UI.
|
||||
# Usage:
|
||||
# gh workflow run release-all.yml -f old_version=8.42 -f new_version=8.43
|
||||
#
|
||||
# Or use the convenience script (commits pending changes, pushes, and triggers):
|
||||
# ./releases/release-all.sh 8.42 8.43
|
||||
#
|
||||
# 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 - Snapcraft store credentials
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
old_version:
|
||||
description: 'Previous version number (e.g. 8.42, without v prefix)'
|
||||
required: true
|
||||
type: string
|
||||
new_version:
|
||||
description: 'New version number (e.g. 8.43, without v prefix)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
|
||||
# ─── 1. Extract version and release notes ───────────────────────────────────
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
changelog: ${{ steps.changelog.outputs.content }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: refs/tags/v${{ needs.prepare.outputs.version }}
|
||||
|
||||
- name: Set version from input
|
||||
id: version
|
||||
run: |
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
VERSION="${VERSION#v}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Extract release notes from CHANGELOG.md
|
||||
id: changelog
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
# Try to find the version-specific section first, then fall back to Upcoming
|
||||
NOTES=$(python3 - <<PYEOF
|
||||
import re, sys
|
||||
version = "$VERSION"
|
||||
with open("CHANGELOG.md", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
# Pattern: section heading through (but not including) the next heading
|
||||
patterns = [
|
||||
rf"(# v{re.escape(version)} .*?)(?=\n# v[0-9]|\Z)",
|
||||
r"(# Upcoming 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" >> $GITHUB_OUTPUT
|
||||
echo "$NOTES" >> $GITHUB_OUTPUT
|
||||
echo "GHEOF" >> $GITHUB_OUTPUT
|
||||
|
||||
# ─── 2. Build amd64 bundle (full Meteor 3 build) ────────────────────────────
|
||||
# Meteor 3.5-beta.7 installer supports only: Linux x86_64, Linux aarch64, macOS, Windows.
|
||||
# It explicitly rejects armhf, s390x, and ppc64le at install time.
|
||||
# Therefore the full Meteor build always runs here on x86_64.
|
||||
# All other Linux architectures then rebuild only the native npm modules
|
||||
# from this bundle (much faster than a full Meteor build).
|
||||
# Node.js version must match what Meteor 3.5-beta.7 bundles (detected automatically).
|
||||
build-amd64:
|
||||
needs: prepare
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 180
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
NODE_VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: refs/tags/v${{ inputs.new_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
|
||||
|
||||
# Pin to detected latest Node.js 24.x — must match Meteor's bundled version.
|
||||
# Using the same version for build and npm rebuild ensures consistent native module ABI.
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '${{ needs.prepare.outputs.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: 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) ─────────────────
|
||||
# Meteor 3.5-beta.7 does support Linux aarch64, but running a full Meteor build a
|
||||
# second time would double the build time for the same result. npm rebuild on
|
||||
# the native arm64 GitHub Actions runner is much faster and produces the same
|
||||
# platform-specific native modules.
|
||||
build-arm64:
|
||||
needs: [prepare, build-amd64]
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
NODE_VERSION: ${{ needs.prepare.outputs.version }}
|
||||
NODE_VERSION_ARM64: ''
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential g++ make python3 curl unzip zip
|
||||
|
||||
|
||||
- name: Set Node.js version for arm64
|
||||
id: node_arm64
|
||||
run: |
|
||||
echo "NODE_VERSION_ARM64=24" >> $GITHUB_ENV
|
||||
|
||||
- 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
|
||||
# Use the latest available Node.js 24.x slim image for arm64
|
||||
docker run --rm --platform linux/arm64 \
|
||||
-v "$(pwd)/bundle:/bundle" \
|
||||
-w /bundle/programs/server \
|
||||
node:24-slim \
|
||||
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
|
||||
|
||||
- name: Clean up and 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
|
||||
|
||||
# ─── 4. Build s390x bundle (rebuild native modules via QEMU container) ───────
|
||||
# Meteor 3.5-beta.7 does NOT support s390x (installer exits with error on that arch).
|
||||
# GitHub Actions has no native s390x runners.
|
||||
# Solution: QEMU emulation + node:<version>-slim (s390x variant) for npm rebuild.
|
||||
build-s390x:
|
||||
needs: [prepare, build-amd64]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
NODE_VERSION: ${{ needs.prepare.outputs.version }}
|
||||
NODE_VERSION_S390X: ''
|
||||
steps:
|
||||
- name: Set up QEMU for s390x
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: s390x
|
||||
|
||||
- name: Install unzip/zip
|
||||
run: sudo apt-get update && sudo apt-get install -y unzip zip
|
||||
|
||||
- name: Download amd64 bundle
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: bundle-amd64
|
||||
|
||||
- name: Set Node.js version for s390x
|
||||
id: node_s390x
|
||||
run: |
|
||||
echo "NODE_VERSION_S390X=24" >> $GITHUB_ENV
|
||||
|
||||
- name: Rebuild native modules for s390x in container
|
||||
run: |
|
||||
unzip wekan-${VERSION}-amd64.zip
|
||||
rm -rf bundle/programs/server/node_modules
|
||||
# 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
|
||||
# Use the latest available Node.js 24.x slim image for s390x
|
||||
docker run --rm --platform linux/s390x \
|
||||
-v "$(pwd)/bundle:/bundle" \
|
||||
-w /bundle/programs/server \
|
||||
node:24-slim \
|
||||
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
|
||||
|
||||
- name: Clean up and create s390x zip
|
||||
run: |
|
||||
zip -r wekan-${VERSION}-s390x.zip bundle
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bundle-s390x
|
||||
path: wekan-${{ needs.prepare.outputs.version }}-s390x.zip
|
||||
retention-days: 1
|
||||
|
||||
# ─── 4b. Build ppc64le bundle (rebuild native modules via QEMU container) ────
|
||||
# Meteor 3.5-beta.7 does NOT support ppc64le (installer exits with error on that arch).
|
||||
# GitHub Actions has no native ppc64le runners.
|
||||
# Solution: QEMU emulation + node:<version>-slim (ppc64le variant) for npm rebuild.
|
||||
build-ppc64le:
|
||||
needs: [prepare, build-amd64]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
NODE_VERSION: ${{ needs.prepare.outputs.version }}
|
||||
NODE_VERSION_PPC64LE: ''
|
||||
steps:
|
||||
- name: Set up QEMU for ppc64le
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: ppc64le
|
||||
|
||||
- name: Install unzip/zip
|
||||
run: sudo apt-get update && sudo apt-get install -y unzip zip
|
||||
|
||||
- name: Download amd64 bundle
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: bundle-amd64
|
||||
|
||||
- name: Set Node.js version for ppc64le
|
||||
id: node_ppc64le
|
||||
run: |
|
||||
echo "NODE_VERSION_PPC64LE=24" >> $GITHUB_ENV
|
||||
|
||||
- name: Rebuild native modules for ppc64le in container
|
||||
run: |
|
||||
unzip wekan-${VERSION}-amd64.zip
|
||||
rm -rf bundle/programs/server/node_modules
|
||||
# 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
|
||||
# Use the latest available Node.js 24.x slim image for ppc64le
|
||||
docker run --rm --platform linux/ppc64le \
|
||||
-v "$(pwd)/bundle:/bundle" \
|
||||
-w /bundle/programs/server \
|
||||
node:24-slim \
|
||||
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
|
||||
|
||||
- name: Clean up and create ppc64le zip
|
||||
run: |
|
||||
zip -r wekan-${VERSION}-ppc64le.zip bundle
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bundle-ppc64le
|
||||
path: wekan-${{ needs.prepare.outputs.version }}-ppc64le.zip
|
||||
retention-days: 1
|
||||
|
||||
# ─── 4c. Build armhf bundle — COMMENTED OUT ─────────────────────────────────
|
||||
# No official Node.js binaries exist for armhf (arm/v7).
|
||||
#build-armhf:
|
||||
# needs: [prepare, build-amd64]
|
||||
# runs-on: ubuntu-latest
|
||||
# timeout-minutes: 120
|
||||
# env:
|
||||
# VERSION: ${{ needs.prepare.outputs.version }}
|
||||
# steps:
|
||||
# - name: Set up QEMU for arm/v7
|
||||
# uses: docker/setup-qemu-action@v4
|
||||
# with:
|
||||
# platforms: arm
|
||||
#
|
||||
# - name: Install unzip/zip
|
||||
# run: sudo apt-get update && sudo apt-get install -y unzip zip
|
||||
#
|
||||
# - name: Download amd64 bundle
|
||||
# uses: actions/download-artifact@v8
|
||||
# with:
|
||||
# name: bundle-amd64
|
||||
#
|
||||
# - name: Rebuild native modules for armhf in container
|
||||
# run: |
|
||||
# unzip wekan-${VERSION}-amd64.zip
|
||||
# rm -rf bundle/programs/server/node_modules
|
||||
# # 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
|
||||
# # node:<version>-slim arm/v7 — same Node.js version as Meteor bundled
|
||||
# docker run --rm --platform linux/arm/v7 \
|
||||
# -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: Clean up and create armhf zip
|
||||
# run: |
|
||||
# zip -r wekan-${VERSION}-armhf.zip bundle
|
||||
#
|
||||
# - uses: actions/upload-artifact@v7
|
||||
# with:
|
||||
# name: bundle-armhf
|
||||
# path: wekan-${{ needs.prepare.outputs.version }}-armhf.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:
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '${{ needs.prepare.outputs.version }}'
|
||||
|
||||
## With Node 24 these are already preinstalled at GitHub Windows runners
|
||||
#- name: Install node-gyp and build tools
|
||||
# run: |
|
||||
# npm install -g windows-build-tools --vs2019 2>$null; exit 0
|
||||
|
||||
- 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: cmd
|
||||
run: |
|
||||
cd bundle\programs\server
|
||||
rmdir /s /q node_modules
|
||||
npm install
|
||||
cd ..\..\..
|
||||
|
||||
- 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. Create GitHub Release and upload all bundles ────────────────────────
|
||||
release:
|
||||
needs: [prepare, build-amd64, build-arm64, build-s390x, build-ppc64le, build-win64]
|
||||
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 }}-s390x.zip
|
||||
wekan-${{ needs.prepare.outputs.version }}-ppc64le.zip
|
||||
wekan-${{ needs.prepare.outputs.version }}-win64.zip
|
||||
@@ -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@v6
|
||||
#
|
||||
# - 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@v6
|
||||
#
|
||||
# - 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@v6
|
||||
#
|
||||
# - 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@v6
|
||||
|
||||
# CACHING
|
||||
- name: Install Meteor
|
||||
id: cache-meteor-install
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.meteor
|
||||
key: v1-meteor-${{ hashFiles('.meteor/versions') }}
|
||||
restore-keys: |
|
||||
v1-meteor-
|
||||
|
||||
- name: Cache NPM dependencies
|
||||
id: cache-meteor-npm
|
||||
uses: actions/cache@v5
|
||||
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@v5
|
||||
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@v3
|
||||
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@v7
|
||||
with:
|
||||
name: coverage-folder
|
||||
path: .coverage/
|
||||
|
||||
coverage:
|
||||
name: Coverage report
|
||||
runs-on: ubuntu-latest
|
||||
needs: [tests]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download coverage
|
||||
uses: actions/download-artifact@v8
|
||||
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!
|
||||
+24
@@ -35,7 +35,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/
|
||||
+30
-44
@@ -3,32 +3,28 @@
|
||||
# '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
|
||||
standard-minifier-js
|
||||
mquandalle:jade
|
||||
ecmascript@0.18.0
|
||||
standard-minifier-js@3.2.0
|
||||
rspack@1.1.0
|
||||
|
||||
# Polyfills
|
||||
es5-shim@4.8.0
|
||||
|
||||
blaze
|
||||
blaze@3.0.2
|
||||
|
||||
# Collections
|
||||
aldeed:collection2
|
||||
aldeed:schema-index@4.0.0-beta.5
|
||||
reywood:publish-composite
|
||||
dburles:collection-helpers
|
||||
mongo@1.16.8
|
||||
mongo@2.4.0-beta350.10
|
||||
|
||||
# Account system
|
||||
accounts-password@2.4.0
|
||||
communitypackages:core@2.0.3
|
||||
communitypackages:unstyled@2.0.1
|
||||
accounts-password@3.3.0-beta350.10
|
||||
communitypackages:core
|
||||
communitypackages:unstyled
|
||||
communitypackages:flow-routing
|
||||
# Routing
|
||||
ostrio:flow-router-extra
|
||||
simple:rest-accounts-password
|
||||
wekan-ldap
|
||||
wekan-accounts-cas
|
||||
wekan-accounts-sandstorm
|
||||
@@ -37,42 +33,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
|
||||
audit-argument-checks@1.0.7
|
||||
http@2.0.0! # force new http package
|
||||
|
||||
# Datepicker (disabled - using native HTML inputs)
|
||||
# wekan-bootstrap-datepicker
|
||||
check@1.5.0
|
||||
random@1.2.2
|
||||
reactive-dict@1.3.2
|
||||
session@1.2.3-beta350.10
|
||||
tracker@1.3.4
|
||||
audit-argument-checks@1.0.8
|
||||
|
||||
# UI components
|
||||
ostrio:i18n
|
||||
reactive-var@1.0.12
|
||||
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
|
||||
reactive-var@1.0.13
|
||||
shell-server@0.7.0
|
||||
email@3.2.0-beta350.10
|
||||
dynamic-import@0.7.4
|
||||
wekan-markdown
|
||||
quave:synced-cron
|
||||
ostrio:cookies
|
||||
ostrio:files@2.3.0
|
||||
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
|
||||
service-configuration@1.3.2
|
||||
communitypackages:picker
|
||||
minifier-css@1.6.4
|
||||
ejson@1.1.3
|
||||
logging@1.3.3
|
||||
service-configuration@1.3.5
|
||||
minifier-css@2.0.2-beta350.10
|
||||
ejson@1.2.0-beta350.10
|
||||
logging@1.3.6
|
||||
wekan-fullcalendar
|
||||
wekan-fontawesome
|
||||
|
||||
jam:offline
|
||||
jam:method
|
||||
jam:pub-sub
|
||||
+1
-1
@@ -1 +1 @@
|
||||
METEOR@2.16
|
||||
METEOR@3.5-beta.10
|
||||
+104
-117
@@ -1,126 +1,113 @@
|
||||
accounts-base@2.2.11
|
||||
accounts-oauth@1.4.4
|
||||
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
|
||||
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.4.1
|
||||
accounts-base@3.3.0-beta350.10
|
||||
accounts-oauth@1.4.7-beta350.10
|
||||
accounts-password@3.3.0-beta350.10
|
||||
aldeed:collection2@4.1.5
|
||||
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-beta350.10
|
||||
binary-heap@1.0.13-beta350.10
|
||||
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-beta350.10
|
||||
check@1.5.0
|
||||
communitypackages:core@2.0.3
|
||||
communitypackages:flow-routing@2.1.0
|
||||
communitypackages:picker@1.1.1
|
||||
communitypackages:unstyled@2.0.1
|
||||
dburles:collection-helpers@1.1.0
|
||||
ddp@1.4.1
|
||||
ddp-client@2.6.2
|
||||
ddp-common@1.4.1
|
||||
ddp-rate-limiter@1.2.1
|
||||
ddp-server@2.7.1
|
||||
deps@1.0.12
|
||||
diff-sequence@1.1.2
|
||||
dynamic-import@0.7.3
|
||||
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.6
|
||||
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
|
||||
inter-process-messaging@0.1.1
|
||||
jquery@3.0.0
|
||||
lmieulet:meteor-coverage@1.1.4
|
||||
localstorage@1.2.0
|
||||
logging@1.3.4
|
||||
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: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.8.0
|
||||
minifiers@1.1.8-faster-rebuild.0
|
||||
minimongo@1.9.4
|
||||
modern-browsers@0.1.10
|
||||
modules@0.20.0
|
||||
modules-runtime@0.13.1
|
||||
mongo@1.16.10
|
||||
mongo-decimal@0.1.3
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.8
|
||||
mquandalle:jade@0.4.9
|
||||
mquandalle:jade-compiler@0.4.5
|
||||
msavin:usercache@1.8.0
|
||||
npm-mongo@4.17.2
|
||||
oauth@2.2.1
|
||||
oauth2@1.3.2
|
||||
observe-sequence@1.0.21
|
||||
ordered-dict@1.1.0
|
||||
ostrio:cookies@2.7.2
|
||||
core-runtime@1.0.0
|
||||
ddp@1.4.2
|
||||
ddp-client@3.3.0-beta350.10
|
||||
ddp-common@1.4.5-beta350.10
|
||||
ddp-rate-limiter@1.3.0-beta350.10
|
||||
ddp-server@3.3.0-beta350.10
|
||||
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-beta350.10
|
||||
email@3.2.0-beta350.10
|
||||
es5-shim@4.8.1
|
||||
facts-base@1.0.2
|
||||
fetch@0.2.0-beta350.10
|
||||
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-beta350.10
|
||||
meteor-base@1.5.2
|
||||
meteortesting:browser-tests@1.7.0
|
||||
meteortesting:mocha@3.2.0
|
||||
meteortesting:mocha-core@8.2.0
|
||||
minifier-css@2.0.2-beta350.10
|
||||
minifier-js@3.2.0
|
||||
minimongo@2.2.0-beta350.10
|
||||
modern-browsers@0.2.3
|
||||
modules@0.20.3
|
||||
modules-runtime@0.13.2
|
||||
mongo@2.4.0-beta350.10
|
||||
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-beta.10
|
||||
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:flow-router-extra@3.10.1
|
||||
ostrio:files@3.0.1
|
||||
ostrio:flow-router-extra@3.14.0
|
||||
ostrio:i18n@3.2.1
|
||||
promise@0.12.2
|
||||
quave:synced-cron@2.2.1
|
||||
raix:eventemitter@0.1.3
|
||||
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
|
||||
promise@1.0.0
|
||||
quave:synced-cron@2.4.0
|
||||
raix:eventemitter@1.0.0
|
||||
random@1.2.2
|
||||
rate-limit@1.2.0-beta350.10
|
||||
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.1
|
||||
service-configuration@1.3.4
|
||||
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
|
||||
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
|
||||
underscore@1.6.1
|
||||
url@1.3.2
|
||||
webapp@1.13.8
|
||||
webapp-hashing@1.1.1
|
||||
routepolicy@1.1.2
|
||||
rspack@1.1.0
|
||||
service-configuration@1.3.5
|
||||
session@1.2.3-beta350.10
|
||||
sha@1.0.10
|
||||
shell-server@0.7.0
|
||||
socket-stream-client@0.7.0-beta350.10
|
||||
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-beta350.10
|
||||
webapp@2.2.0-beta350.10
|
||||
webapp-hashing@1.1.2
|
||||
wekan-accounts-cas@0.2.0
|
||||
wekan-accounts-lockout@1.1.0
|
||||
wekan-accounts-oidc@1.0.10
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
npm-packages/
|
||||
tests/
|
||||
+1310
-11
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -9,7 +9,7 @@ Not paid:
|
||||
- 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 propietary. WeKan is NOT Big Tech.
|
||||
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
|
||||
|
||||
+25
-21
@@ -1,23 +1,23 @@
|
||||
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"
|
||||
|
||||
# TARGETARCH is automatically provided by Docker Buildx
|
||||
# TARGETARCH and TARGETVARIANT are automatically provided by Docker Buildx
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
ARG VERSION=9.26
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
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.16 \
|
||||
NODE_VERSION=v24.16.0 \
|
||||
METEOR_RELEASE=METEOR@3.5-beta.10 \
|
||||
USE_EDGE=false \
|
||||
METEOR_EDGE=1.5-beta.17 \
|
||||
NPM_VERSION=6.14.17 \
|
||||
FIBERS_VERSION=4.0.1 \
|
||||
NPM_VERSION=11.11.0 \
|
||||
SRC_PATH=./ \
|
||||
WITH_API=true \
|
||||
MONGO_OPLOG_URL="" \
|
||||
@@ -169,25 +169,28 @@ apt-get update --assume-yes
|
||||
apt-get upgrade --assume-yes
|
||||
apt-get install --assume-yes --no-install-recommends ${BUILD_DEPS}
|
||||
|
||||
# Multi-arch mapping logic
|
||||
# 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" ;;
|
||||
"s390x") NODE_ARCH="s390x" WEKAN_ARCH="s390x" ;;
|
||||
"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
|
||||
# Node.js Installation (official nodejs.org builds for Node 22)
|
||||
cd /tmp
|
||||
wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz"
|
||||
wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt"
|
||||
grep "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt | shasum -a 256 -c -
|
||||
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
|
||||
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} --production
|
||||
npm install -g npm@${NPM_VERSION}
|
||||
chown --recursive wekan:wekan /home/wekan/
|
||||
|
||||
# Temporary Tar swap for Meteor bundle
|
||||
@@ -197,9 +200,10 @@ ln -sf $(which bsdtar) $(which tar)
|
||||
# WeKan Bundle Installation
|
||||
mkdir -p /home/wekan/app
|
||||
cd /home/wekan/app
|
||||
wget "https://github.com/wekan/wekan/releases/download/v8.41/wekan-8.41-${WEKAN_ARCH}.zip"
|
||||
unzip "wekan-8.41-${WEKAN_ARCH}.zip"
|
||||
rm "wekan-8.41-${WEKAN_ARCH}.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
|
||||
|
||||
# Restore original tar
|
||||
|
||||
@@ -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)
|
||||
|
||||
+77
-77
@@ -1,38 +1,35 @@
|
||||
## About money
|
||||
## About Money: No bounty
|
||||
|
||||
See [CONTRIBUTING](CONTRIBUTING.md)
|
||||
There is no bounty, like described at [CONTRIBUTING.md](CONTRIBUTING.md),
|
||||
because WeKan is NOT Big Tech. WeKan is FLOSS.
|
||||
|
||||
## Responsible Security Disclosure
|
||||
## Steps for Coordinated Vulnerability Disclosure
|
||||
|
||||
1. To send email, if possible, use PGP key [security-at-wekan.fi.asc](security-at-wekan.fi.asc)
|
||||
2. Send info about security issue ONLY to security@wekan.fi . NOT TO ANYWHERE ELSE. NO CC, NO BCC.
|
||||
3. Wait for new WeKan release that fixes security issue to appear to top of
|
||||
https://github.com/wekan/wekan/blob/main/CHANGELOG.md
|
||||
4. We will thank you by adding you to Hall of Fame: https://wekan.fi/hall-of-fame/
|
||||
5. All vulnerability details will be private to security@wekan.fi ,
|
||||
unless you help all WeKan platforms to have a way to upgrade, like sending
|
||||
database migrations code to security@wekan.fi or PRs to https://github.com/wekan/wekan/pulls .
|
||||
There is no benefit to Wordwide Security Community to have more details about vulnerabilities,
|
||||
if Worldwide Security Community does not help to make upgrades possible.
|
||||
6. If there some day becomes available a way to upgrade all WeKan platforms,
|
||||
this page will be updated to add permission for security researchers
|
||||
to request new GHSA or CVE ID and publish your vulnerability details at your blog, talks, etc,
|
||||
and send that info also to security@wekan.fi to be added to
|
||||
Hall of Fame: https://wekan.fi/hall-of-fame/ to get Upgrade Bonus Point Stars.
|
||||
In that case, it will become possible for security@wekan.fi to publish all
|
||||
remaining private security details, and publicly thank Worldwide Security Community.
|
||||
### 1. Security Researcher:
|
||||
|
||||
## Bonus Points
|
||||
- **Report**: Please email details regarding security vulnerabilities to **security@wekan.fi**.
|
||||
- **Proof of Concept (PoC)**: If possible, include a fix or a reproduction script/code.
|
||||
- **Encryption**: An optional PGP key is available at [security-at-wekan.fi.asc](security-at-wekan.fi.asc).
|
||||
|
||||
- If you include code for fixing security issue
|
||||
### 2. WeKan Security Team:
|
||||
|
||||
## Losing Points
|
||||
- **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 does not facilitate **CVE Coordination** due to
|
||||
our rapid release cycle (new versions are released almost daily).
|
||||
|
||||
- If you ask about [bounty](CONTRIBUTING.md). There is no bounty. WeKan is NOT Big Tech. WeKan is FLOSS.
|
||||
- If you forget to include vulnerability details.
|
||||
- If you send info about security issue to somewhere else than security@wekan.fi
|
||||
### 3. Post-Release and Public Disclosure:
|
||||
|
||||
## How should reports be formatted?
|
||||
- **Independent CVE Assignment**: After the fix is public, you are welcome to coordinate
|
||||
**CVE assignment** independently. Please send the assigned CVE IDs to **security@wekan.fi**,
|
||||
and we will update your entry in the [Hall of Fame](https://wekan.fi/hall-of-fame/).
|
||||
- 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.
|
||||
|
||||
## How Should Reports Be Formatted?
|
||||
|
||||
```
|
||||
Name: %name
|
||||
@@ -57,15 +54,19 @@ added to the WeKan Hall of Fame https://wekan.fi/hall-of-fame/
|
||||
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.
|
||||
|
||||
## Meteor Security
|
||||
|
||||
https://guide.meteor.com/security
|
||||
|
||||
### Standalone Wekan Security
|
||||
|
||||
@@ -74,11 +75,12 @@ 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.
|
||||
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.
|
||||
- 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
|
||||
@@ -90,24 +92,25 @@ and by by companies that have 30k users.
|
||||
|
||||
## 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 .
|
||||
- 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
|
||||
- Dompurify cleans up viewed code, so JavaScript in input fields does not execute
|
||||
- https://wekan.github.io/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://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?
|
||||
|
||||
@@ -115,32 +118,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
|
||||
- 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
|
||||
|
||||
@@ -152,10 +159,10 @@ A:
|
||||
|
||||
## 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()) {`
|
||||
@@ -164,8 +171,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(() => {
|
||||
@@ -176,13 +183,13 @@ 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/
|
||||
|
||||
@@ -249,18 +256,16 @@ Meteor.startup(() => {
|
||||
|
||||
### 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?
|
||||
|
||||
@@ -276,16 +281,11 @@ a security issue, we'd like to know about it, and also how to fix it:
|
||||
|
||||
## 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.fi
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v8.41.0"
|
||||
appVersion: "v9.26.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
+38
-3
@@ -1,7 +1,41 @@
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,7 +44,8 @@ import '/client/config/blazeHelpers';
|
||||
|
||||
// Import board converter for on-demand conversion
|
||||
import '/client/lib/boardConverter';
|
||||
import '/client/components/boardConversionProgress';
|
||||
import '/client/features/boardConversion';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
// Import migration manager and progress UI - COMMENTED OUT
|
||||
// import '/client/lib/attachmentMigrationManager';
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
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;
|
||||
|
||||
Template.activities.onCreated(function () {
|
||||
// Register with sidebar so it can call loadNextPage on us
|
||||
const Sidebar = getSidebarInstance();
|
||||
if (Sidebar) {
|
||||
Sidebar.activitiesInstance = this;
|
||||
}
|
||||
@@ -22,7 +27,7 @@ Template.activities.onCreated(function () {
|
||||
};
|
||||
|
||||
// TODO is sidebar always available? E.g. on small screens/mobile devices
|
||||
const sidebar = Sidebar;
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar && sidebar.infiniteScrolling) {
|
||||
sidebar.infiniteScrolling.resetNextPeak();
|
||||
}
|
||||
@@ -71,7 +76,9 @@ function _showActivities(data) {
|
||||
const currentCard = Utils.getCurrentCard();
|
||||
ret = currentCard.showActivities ?? false;
|
||||
} else if (mode === 'card') {
|
||||
ret = data?.card?.showActivities ?? false;
|
||||
// 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;
|
||||
}
|
||||
@@ -80,8 +87,31 @@ function _showActivities(data) {
|
||||
}
|
||||
|
||||
Template.activities.helpers({
|
||||
showActivities() {
|
||||
const data = Template.currentData();
|
||||
return _showActivities(data);
|
||||
},
|
||||
|
||||
activities() {
|
||||
return this.card.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 || [];
|
||||
},
|
||||
});
|
||||
|
||||
@@ -260,7 +290,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);
|
||||
@@ -272,7 +303,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);
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
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);
|
||||
|
||||
@@ -65,7 +70,7 @@ Template.comment.events({
|
||||
evt.preventDefault();
|
||||
const textarea = tpl.find('.js-edit-comment textarea,input[type=text]');
|
||||
const commentText = textarea && textarea.value ? textarea.value.trim() : '';
|
||||
const commentId = this._id;
|
||||
const commentId = tpl.data._id;
|
||||
if (commentText) {
|
||||
CardComments.update(commentId, {
|
||||
$set: {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import Boards from '/models/boards';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
Template.archivedBoards.onCreated(function () {
|
||||
this.subscribe('archivedBoards');
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import '../gantt/gantt.js';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import dragscroll from '@wekanteam/dragscroll';
|
||||
@@ -7,13 +6,16 @@ import { boardConverter } from '/client/lib/boardConverter';
|
||||
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
|
||||
import Swimlanes from '/models/swimlanes';
|
||||
import Lists from '/models/lists';
|
||||
import TableVisibilityModeSettings from '/models/tableVisibilityModeSettings';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
// SubsManager removed for Meteor 3 migration
|
||||
const { calculateIndex } = Utils;
|
||||
const swimlaneWhileSortingHeight = 150;
|
||||
|
||||
// Global reference so child components (swimlanes, cards) can access boardBody instance
|
||||
BoardBody = null;
|
||||
let BoardBody = null;
|
||||
|
||||
Template.board.onCreated(function () {
|
||||
this.isBoardReady = new ReactiveVar(false);
|
||||
@@ -22,7 +24,7 @@ Template.board.onCreated(function () {
|
||||
this._boardProcessed = false; // Track if board has been processed
|
||||
this._lastProcessedBoardId = null; // Track last processed board ID
|
||||
|
||||
this.ensureDefaultSwimlane = (boardId) => {
|
||||
this.ensureDefaultSwimlane = async (boardId) => {
|
||||
// Only create swimlane once per board
|
||||
if (this._swimlaneCreated.has(boardId)) {
|
||||
return;
|
||||
@@ -33,25 +35,15 @@ Template.board.onCreated(function () {
|
||||
if (!board) return;
|
||||
|
||||
const swimlanes = board.swimlanes();
|
||||
|
||||
if (swimlanes.length === 0) {
|
||||
// Check if any swimlane exists in the database to avoid race conditions
|
||||
const existingSwimlanes = ReactiveCache.getSwimlanes({ boardId });
|
||||
if (existingSwimlanes.length === 0) {
|
||||
const swimlaneId = Swimlanes.insert({
|
||||
title: 'Default',
|
||||
boardId: boardId,
|
||||
});
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(
|
||||
`Created default swimlane ${swimlaneId} for board ${boardId}`,
|
||||
);
|
||||
}
|
||||
const swimlaneId = await Meteor.callAsync('ensureDefaultSwimlane', boardId);
|
||||
if (process.env.DEBUG === 'true' && swimlaneId) {
|
||||
console.log(
|
||||
`Ensured default swimlane ${swimlaneId} for board ${boardId}`,
|
||||
);
|
||||
}
|
||||
this._swimlaneCreated.add(boardId);
|
||||
} else {
|
||||
this._swimlaneCreated.add(boardId);
|
||||
}
|
||||
this._swimlaneCreated.add(boardId);
|
||||
} catch (error) {
|
||||
console.error('Error creating default swimlane:', error);
|
||||
}
|
||||
@@ -81,7 +73,7 @@ Template.board.onCreated(function () {
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
if (!currentBoardId) return;
|
||||
|
||||
const handle = subManager.subscribe('board', currentBoardId, false);
|
||||
const handle = Meteor.subscribe('board', currentBoardId, false);
|
||||
|
||||
// Use a separate autorun for subscription ready state to avoid reactive loops
|
||||
this.subscriptionReadyAutorun = Tracker.autorun(() => {
|
||||
@@ -614,6 +606,17 @@ Template.boardBody.onDestroyed(function () {
|
||||
});
|
||||
|
||||
Template.boardBody.helpers({
|
||||
openCards() {
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (!isMobile) {
|
||||
const openCardIds = Session.get('openCards') || [];
|
||||
return openCardIds
|
||||
.map((id) => ReactiveCache.getCard(id))
|
||||
.filter((card) => card);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
draggingActive() {
|
||||
return Template.instance().draggingActive.get();
|
||||
},
|
||||
@@ -1091,11 +1094,33 @@ Template.calendarView.helpers({
|
||||
const createCardButton = modalElement.querySelector(
|
||||
'#create-card-button',
|
||||
);
|
||||
createCardButton.addEventListener('click', function () {
|
||||
createCardButton.addEventListener('click', async function () {
|
||||
const myTitle = modalElement.querySelector('#card-title-input').value;
|
||||
if (myTitle) {
|
||||
const firstList = currentBoard.draggableLists()[0];
|
||||
const firstSwimlane = currentBoard.swimlanes()[0];
|
||||
let firstSwimlane = currentBoard.swimlanes()[0];
|
||||
if (!firstSwimlane) {
|
||||
const swimlaneId = await Swimlanes.insertAsync({
|
||||
title: 'Default',
|
||||
boardId: currentBoard._id,
|
||||
});
|
||||
firstSwimlane = ReactiveCache.getSwimlane(swimlaneId);
|
||||
}
|
||||
|
||||
let firstList = currentBoard.draggableLists()[0];
|
||||
if (!firstList && firstSwimlane) {
|
||||
const defaultTitle = TAPi18n.__('default');
|
||||
const listId = await Lists.insertAsync({
|
||||
title: typeof defaultTitle === 'string' ? defaultTitle : 'Default',
|
||||
boardId: currentBoard._id,
|
||||
swimlaneId: firstSwimlane._id,
|
||||
});
|
||||
firstList = ReactiveCache.getList(listId);
|
||||
}
|
||||
|
||||
if (!firstList || !firstSwimlane) {
|
||||
return;
|
||||
}
|
||||
|
||||
Meteor.call(
|
||||
'createCardWithDueDate',
|
||||
currentBoard._id,
|
||||
|
||||
@@ -3370,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),
|
||||
@@ -4279,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;
|
||||
|
||||
@@ -1,151 +1,152 @@
|
||||
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(
|
||||
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(
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
else
|
||||
a.board-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
span {{_ 'log-in'}}
|
||||
else
|
||||
a.board-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
|
||||
.board-header-btns.center
|
||||
if isSandstorm
|
||||
if currentUser
|
||||
a.board-header-btn.js-open-archived-board
|
||||
i.fa.fa-archive
|
||||
|
||||
.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
|
||||
//if showSort
|
||||
//
|
||||
a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
|
||||
//
|
||||
i.fa(class="{{directionClass}}")
|
||||
//
|
||||
span {{_ 'sort'}}{{_ listSortShortDesc}}
|
||||
|
||||
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.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
|
||||
|
||||
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}}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
|
||||
|
||||
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'}}")
|
||||
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-bars
|
||||
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
|
||||
|
||||
@@ -2,6 +2,14 @@ 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';
|
||||
@@ -11,6 +19,7 @@ const sortCardsBy = new ReactiveVar('');
|
||||
|
||||
Template.boardChangeTitlePopup.events({
|
||||
async submit(event, templateInstance) {
|
||||
event.preventDefault();
|
||||
const newTitle = templateInstance
|
||||
.$('.js-board-name')
|
||||
.val()
|
||||
@@ -27,11 +36,25 @@ Template.boardChangeTitlePopup.events({
|
||||
}
|
||||
Popup.back();
|
||||
}
|
||||
event.preventDefault();
|
||||
},
|
||||
});
|
||||
|
||||
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());
|
||||
@@ -94,30 +117,32 @@ Template.boardHeaderBar.events({
|
||||
console.log('Hamburger menu clicked');
|
||||
}
|
||||
// Use the same approach as keyboard shortcuts
|
||||
if (typeof Sidebar !== 'undefined' && Sidebar && typeof Sidebar.toggle === 'function') {
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar && typeof sidebar.toggle === 'function') {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Using Sidebar.toggle()');
|
||||
}
|
||||
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 sidebar = Blaze._globalHelpers.Sidebar();
|
||||
if (sidebar && typeof sidebar.toggle === 'function') {
|
||||
const blazeSidebar = Blaze._globalHelpers.Sidebar();
|
||||
if (blazeSidebar && typeof blazeSidebar.toggle === 'function') {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Using Blaze helper Sidebar.toggle()');
|
||||
}
|
||||
sidebar.toggle();
|
||||
blazeSidebar.toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-open-filter-view'() {
|
||||
if (Sidebar) {
|
||||
Sidebar.setView('filter');
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar) {
|
||||
sidebar.setView('filter');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
@@ -137,8 +162,9 @@ Template.boardHeaderBar.events({
|
||||
*/
|
||||
'click .js-filter-reset'(event) {
|
||||
event.stopPropagation();
|
||||
if (Sidebar) {
|
||||
Sidebar.setView();
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar) {
|
||||
sidebar.setView();
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
@@ -148,8 +174,9 @@ Template.boardHeaderBar.events({
|
||||
Session.set('sortBy', '');
|
||||
},
|
||||
'click .js-open-search-view'() {
|
||||
if (Sidebar) {
|
||||
Sidebar.setView('search');
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar) {
|
||||
sidebar.setView('search');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
@@ -206,7 +233,7 @@ function createBoardHelpers() {
|
||||
return Template.instance().visibility.get();
|
||||
},
|
||||
notAllowPrivateVisibilityOnly() {
|
||||
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
|
||||
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly')?.booleanValue;
|
||||
},
|
||||
visibilityCheck() {
|
||||
return Template.currentData() === Template.instance().visibility.get();
|
||||
@@ -214,55 +241,26 @@ function createBoardHelpers() {
|
||||
};
|
||||
}
|
||||
|
||||
function createBoardSubmit(tpl, event) {
|
||||
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) {
|
||||
//const templateContainerId = Meteor.call('setCreateTemplateContainer');
|
||||
//Utils.goBoardId(templateContainerId);
|
||||
//alert('niinku template ' + Meteor.call('setCreateTemplateContainer'));
|
||||
|
||||
tpl.boardId.set(
|
||||
Boards.insert({
|
||||
// title: TAPi18n.__('templates'),
|
||||
title: title,
|
||||
permission: 'private',
|
||||
type: 'template-container',
|
||||
migrationVersion: 1, // Latest version - no migration needed
|
||||
}),
|
||||
);
|
||||
|
||||
// Insert the card templates swimlane
|
||||
Swimlanes.insert({
|
||||
// title: TAPi18n.__('card-templates-swimlane'),
|
||||
title: 'Card Templates',
|
||||
boardId: tpl.boardId.get(),
|
||||
sort: 1,
|
||||
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' },
|
||||
],
|
||||
}),
|
||||
|
||||
// Insert the list templates swimlane
|
||||
Swimlanes.insert(
|
||||
{
|
||||
// title: TAPi18n.__('list-templates-swimlane'),
|
||||
title: 'List Templates',
|
||||
boardId: tpl.boardId.get(),
|
||||
sort: 2,
|
||||
type: 'template-container',
|
||||
},
|
||||
);
|
||||
|
||||
// Insert the board templates swimlane
|
||||
Swimlanes.insert(
|
||||
{
|
||||
//title: TAPi18n.__('board-templates-swimlane'),
|
||||
title: 'Board Templates',
|
||||
boardId: tpl.boardId.get(),
|
||||
sort: 3,
|
||||
type: 'template-container',
|
||||
},
|
||||
);
|
||||
|
||||
// Assign to space if one was selected
|
||||
@@ -274,24 +272,21 @@ function createBoardSubmit(tpl, event) {
|
||||
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||
}
|
||||
|
||||
Utils.goBoardId(tpl.boardId.get());
|
||||
FlowRouter.go('board', { id: tpl.boardId.get(), slug });
|
||||
|
||||
} else {
|
||||
const visibility = tpl.visibility.get();
|
||||
|
||||
tpl.boardId.set(
|
||||
Boards.insert({
|
||||
await Meteor.callAsync('createBoardWithInitialSwimlanes', {
|
||||
title,
|
||||
slug,
|
||||
permission: visibility,
|
||||
migrationVersion: 1, // Latest version - no migration needed
|
||||
migrationVersion: 1,
|
||||
swimlanes: [{ title: 'Default' }],
|
||||
}),
|
||||
);
|
||||
|
||||
Swimlanes.insert({
|
||||
title: 'Default',
|
||||
boardId: tpl.boardId.get(),
|
||||
});
|
||||
|
||||
// Assign to space if one was selected
|
||||
const spaceId = Session.get('createBoardInWorkspace');
|
||||
if (spaceId) {
|
||||
@@ -301,22 +296,22 @@ function createBoardSubmit(tpl, event) {
|
||||
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||
}
|
||||
|
||||
Utils.goBoardId(tpl.boardId.get());
|
||||
FlowRouter.go('board', { id: tpl.boardId.get(), slug });
|
||||
}
|
||||
}
|
||||
|
||||
function createBoardEvents() {
|
||||
return {
|
||||
'click .js-select-visibility'(event, tpl) {
|
||||
tpl.visibility.set(Template.currentData());
|
||||
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'),
|
||||
'submit'(event, tpl) {
|
||||
createBoardSubmit(tpl, event);
|
||||
async 'submit'(event, tpl) {
|
||||
await createBoardSubmit(tpl, event);
|
||||
},
|
||||
'click .js-import-board': Popup.open('chooseBoardSource'),
|
||||
'click .js-board-template': Popup.open('searchElement'),
|
||||
@@ -367,7 +362,7 @@ Template.headerBarCreateBoardPopup.helpers(createBoardHelpers());
|
||||
|
||||
Template.headerBarCreateBoardPopup.events({
|
||||
'click .js-select-visibility'(event, tpl) {
|
||||
tpl.visibility.set(Template.currentData());
|
||||
tpl.visibility.set(this);
|
||||
tpl.visibilityMenuIsOpen.set(false);
|
||||
},
|
||||
'click .js-change-visibility'(event, tpl) {
|
||||
@@ -375,7 +370,7 @@ Template.headerBarCreateBoardPopup.events({
|
||||
},
|
||||
'click .js-import': Popup.open('boardImportBoard'),
|
||||
async submit(event, tpl) {
|
||||
createBoardSubmit(tpl, event);
|
||||
await createBoardSubmit(tpl, event);
|
||||
// Immediately star boards created with the headerbar popup.
|
||||
await ReactiveCache.getCurrentUser().toggleBoardStar(tpl.boardId.get());
|
||||
},
|
||||
@@ -386,13 +381,19 @@ Template.headerBarCreateBoardPopup.events({
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly')?.booleanValue;
|
||||
},
|
||||
visibilityCheck() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
@@ -403,9 +404,11 @@ Template.boardChangeVisibilityPopup.helpers({
|
||||
Template.boardChangeVisibilityPopup.events({
|
||||
'click .js-select-visibility'() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const visibility = String(this);
|
||||
currentBoard.setVisibility(visibility);
|
||||
Popup.back();
|
||||
const visibility = this;
|
||||
if (typeof visibility === 'string') {
|
||||
currentBoard.setVisibility(visibility);
|
||||
Popup.back();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -423,16 +426,18 @@ Template.boardChangeWatchPopup.helpers({
|
||||
|
||||
Template.boardChangeWatchPopup.events({
|
||||
'click .js-select-watch'() {
|
||||
const level = String(this);
|
||||
Meteor.call(
|
||||
'watch',
|
||||
'board',
|
||||
Session.get('currentBoard'),
|
||||
level,
|
||||
(err, ret) => {
|
||||
if (!err && ret) Popup.back();
|
||||
},
|
||||
);
|
||||
const level = this;
|
||||
if (typeof level === 'string') {
|
||||
Meteor.call(
|
||||
'watch',
|
||||
'board',
|
||||
Session.get('currentBoard'),
|
||||
level,
|
||||
(err, ret) => {
|
||||
if (!err && ret) 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 {
|
||||
@@ -62,6 +62,17 @@
|
||||
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;
|
||||
|
||||
@@ -130,7 +130,7 @@ template(name="boardList")
|
||||
if BoardMultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
span.board-handle(title="{{_ 'drag-board'}}")
|
||||
span.board-handle(title="{{boardWorkspaceDragHint}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-arrows
|
||||
|
||||
@@ -158,13 +158,13 @@ template(name="boardList")
|
||||
if BoardMultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
span.board-handle(title="{{_ 'drag-board'}}")
|
||||
span.board-handle(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="{{_ 'board-drag-drop-reorder-or-click-open'}}")
|
||||
span.board-list-item-name(title="{{boardOpenAndMoveHint}}")
|
||||
+viewer
|
||||
= title
|
||||
unless currentSetting.hideBoardMemberList
|
||||
|
||||
@@ -2,8 +2,12 @@ import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import getSlug from 'limax';
|
||||
import TableVisibilityModeSettings from '/models/tableVisibilityModeSettings';
|
||||
import { BoardMultiSelection } from '/client/lib/boardMultiSelection';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
// SubsManager removed for Meteor 3 migration
|
||||
|
||||
const DEFAULT_WORKSPACE_ICON = '📁';
|
||||
|
||||
@@ -350,12 +354,6 @@ Template.boardList.helpers({
|
||||
if (FlowRouter.getRouteName() === 'home') {
|
||||
membershipOrs.push({ 'members.userId': Meteor.userId() });
|
||||
|
||||
if (
|
||||
allowPrivateVisibilityOnly !== undefined &&
|
||||
allowPrivateVisibilityOnly.booleanValue
|
||||
) {
|
||||
query.$and.push({ permission: 'private' });
|
||||
}
|
||||
const currUser = ReactiveCache.getCurrentUser();
|
||||
|
||||
let orgIdsUserBelongs = currUser?.orgIdsUserBelongs() || '';
|
||||
@@ -535,6 +533,28 @@ Template.boardList.helpers({
|
||||
hasBoardsSelected() {
|
||||
return BoardMultiSelection.count() > 0;
|
||||
},
|
||||
boardWorkspaceDragHint() {
|
||||
const remaining = TAPi18n.__('allboards.remaining') || 'Remaining';
|
||||
const workspaces = TAPi18n.__('allboards.workspaces') || 'Workspaces';
|
||||
return (
|
||||
TAPi18n.__('drag-board-to-workspace', { remaining, workspaces }) ||
|
||||
`Drag board to assign to ${workspaces} (drop on workspace in left sidebar)`
|
||||
);
|
||||
},
|
||||
boardOpenAndMoveHint() {
|
||||
const remaining = TAPi18n.__('allboards.remaining') || 'Remaining';
|
||||
const workspaces = TAPi18n.__('allboards.workspaces') || 'Workspaces';
|
||||
return (
|
||||
TAPi18n.__(
|
||||
'board-open-and-move-between-remaining-and-workspaces',
|
||||
{
|
||||
remaining,
|
||||
workspaces,
|
||||
},
|
||||
) ||
|
||||
`Click board icon to open board. Drag board between ${remaining} and ${workspaces}.`
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Template.workspaceTree.helpers({
|
||||
@@ -638,6 +658,20 @@ Template.boardList.events({
|
||||
evt.originalEvent.dataTransfer.setData('text/plain', boardId);
|
||||
} catch (e) {}
|
||||
}
|
||||
// Highlight valid drop targets in the sidebar so users know where to drop
|
||||
document.querySelectorAll('.workspace-node').forEach((el) => {
|
||||
el.classList.add('board-drag-hint');
|
||||
});
|
||||
document.querySelectorAll('.js-select-menu').forEach((el) => {
|
||||
if (el.getAttribute('data-type') === 'remaining') {
|
||||
el.classList.add('board-drag-hint');
|
||||
}
|
||||
});
|
||||
},
|
||||
'dragend .js-board'() {
|
||||
document.querySelectorAll('.workspace-node.board-drag-hint, .js-select-menu.board-drag-hint').forEach((el) => {
|
||||
el.classList.remove('board-drag-hint');
|
||||
});
|
||||
},
|
||||
'click .js-clone-board'(evt) {
|
||||
if (confirm(TAPi18n.__('duplicate-board-confirm'))) {
|
||||
@@ -657,7 +691,7 @@ Template.boardList.events({
|
||||
console.error(err);
|
||||
} else {
|
||||
Session.set('fromBoard', null);
|
||||
subManager.subscribe('board', res, false);
|
||||
Meteor.subscribe('board', res, false);
|
||||
FlowRouter.go('board', {
|
||||
id: res,
|
||||
slug: title,
|
||||
@@ -671,7 +705,9 @@ Template.boardList.events({
|
||||
'click .js-archive-board'(evt) {
|
||||
if (confirm(TAPi18n.__('archive-board-confirm'))) {
|
||||
const boardId = this._id;
|
||||
Meteor.call('archiveBoard', boardId);
|
||||
Meteor.call('archiveBoard', boardId, (err) => {
|
||||
if (err) alert(err?.reason || err?.message || 'Failed to archive board');
|
||||
});
|
||||
evt.preventDefault();
|
||||
}
|
||||
},
|
||||
@@ -714,7 +750,9 @@ Template.boardList.events({
|
||||
confirm(TAPi18n.__('archive-board-confirm'))
|
||||
) {
|
||||
selectedBoards.forEach((boardId) => {
|
||||
Meteor.call('archiveBoard', boardId);
|
||||
Meteor.call('archiveBoard', boardId, (err) => {
|
||||
if (err) alert(err?.reason || err?.message || 'Failed to archive board');
|
||||
});
|
||||
});
|
||||
BoardMultiSelection.reset();
|
||||
}
|
||||
|
||||
@@ -86,6 +86,10 @@ 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}}", title="{{_ 'download'}}")
|
||||
i.fa.fa-arrow-down
|
||||
|
||||
@@ -1,12 +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
|
||||
@@ -19,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) {
|
||||
@@ -40,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();
|
||||
}),
|
||||
});
|
||||
@@ -196,8 +204,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();
|
||||
@@ -217,14 +223,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
|
||||
@@ -263,6 +267,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();
|
||||
},
|
||||
@@ -273,6 +295,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() {
|
||||
@@ -294,12 +336,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() {
|
||||
@@ -317,6 +360,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();
|
||||
},
|
||||
@@ -328,7 +375,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 [];
|
||||
}
|
||||
@@ -361,7 +408,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,
|
||||
@@ -381,11 +428,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,
|
||||
);
|
||||
@@ -460,21 +512,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,
|
||||
);
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
} 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() {
|
||||
@@ -49,7 +51,10 @@ Template.cardCustomFieldsPopup.events({
|
||||
},
|
||||
'click .js-settings'(event) {
|
||||
EscapeActions.executeUpTo('detailsPane');
|
||||
Sidebar.setView('customFields');
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar) {
|
||||
sidebar.setView('customFields');
|
||||
}
|
||||
event.preventDefault();
|
||||
},
|
||||
});
|
||||
@@ -165,7 +170,7 @@ Template['cardCustomField-date'].helpers({
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
showISODate() {
|
||||
|
||||
@@ -48,7 +48,7 @@ Template.editCardReceivedDatePopup.helpers(datePickerHelpers());
|
||||
|
||||
Template.editCardReceivedDatePopup.events(datePickerEvents({
|
||||
storeDate(date) {
|
||||
this.datePicker.card.setReceived(formatDateTime(date));
|
||||
this.datePicker.card.setReceived(date);
|
||||
},
|
||||
deleteDate() {
|
||||
this.datePicker.card.unsetReceived();
|
||||
@@ -72,7 +72,7 @@ Template.editCardStartDatePopup.helpers(datePickerHelpers());
|
||||
|
||||
Template.editCardStartDatePopup.events(datePickerEvents({
|
||||
storeDate(date) {
|
||||
this.datePicker.card.setStart(formatDateTime(date));
|
||||
this.datePicker.card.setStart(date);
|
||||
},
|
||||
deleteDate() {
|
||||
this.datePicker.card.unsetStart();
|
||||
@@ -96,7 +96,7 @@ Template.editCardDueDatePopup.helpers(datePickerHelpers());
|
||||
|
||||
Template.editCardDueDatePopup.events(datePickerEvents({
|
||||
storeDate(date) {
|
||||
this.datePicker.card.setDue(formatDateTime(date));
|
||||
this.datePicker.card.setDue(date);
|
||||
},
|
||||
deleteDate() {
|
||||
this.datePicker.card.unsetDue();
|
||||
@@ -120,7 +120,7 @@ Template.editCardEndDatePopup.helpers(datePickerHelpers());
|
||||
|
||||
Template.editCardEndDatePopup.events(datePickerEvents({
|
||||
storeDate(date) {
|
||||
this.datePicker.card.setEnd(formatDateTime(date));
|
||||
this.datePicker.card.setEnd(date);
|
||||
},
|
||||
deleteDate() {
|
||||
this.datePicker.card.unsetEnd();
|
||||
@@ -146,14 +146,12 @@ function cardDateHelpers(extraHelpers) {
|
||||
},
|
||||
showWeekOfYear() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
if (!user) return window.localStorage.getItem('showWeekOfYear') === 'true';
|
||||
return user.isShowWeekOfYear();
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
showISODate() {
|
||||
@@ -196,7 +194,7 @@ Template.cardReceivedDate.helpers(cardDateHelpers({
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
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}`;
|
||||
},
|
||||
@@ -237,7 +235,7 @@ Template.cardStartDate.helpers(cardDateHelpers({
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
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}`;
|
||||
},
|
||||
@@ -286,7 +284,7 @@ Template.cardDueDate.helpers(cardDateHelpers({
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
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}`;
|
||||
},
|
||||
@@ -356,7 +354,7 @@ Template.cardCustomFieldDate.helpers(cardDateHelpers({
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${formattedDate}`;
|
||||
},
|
||||
@@ -400,13 +398,13 @@ Template.minicardReceivedDate.helpers(cardDateHelpers({
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
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() : 'YYYY-MM-DD';
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
}));
|
||||
@@ -446,13 +444,13 @@ Template.minicardStartDate.helpers(cardDateHelpers({
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
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() : 'YYYY-MM-DD';
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
}));
|
||||
@@ -500,13 +498,13 @@ Template.minicardDueDate.helpers(cardDateHelpers({
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
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() : 'YYYY-MM-DD';
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
}));
|
||||
@@ -549,7 +547,7 @@ Template.minicardEndDate.helpers(cardDateHelpers({
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
}));
|
||||
@@ -570,13 +568,13 @@ Template.minicardCustomFieldDate.onCreated(function () {
|
||||
Template.minicardCustomFieldDate.helpers(cardDateHelpers({
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
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() : 'YYYY-MM-DD';
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${formattedDate}`;
|
||||
},
|
||||
@@ -602,7 +600,7 @@ Template.voteEndDate.helpers(cardDateHelpers({
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
showTitle() {
|
||||
@@ -630,7 +628,7 @@ Template.pokerEndDate.helpers(cardDateHelpers({
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : (window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD');
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
showTitle() {
|
||||
|
||||
@@ -11,31 +11,30 @@ template(name="cardDetails")
|
||||
+editCardTitleForm
|
||||
else
|
||||
unless isMiniScreen
|
||||
unless isPopup
|
||||
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'}}")
|
||||
span.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
|
||||
if cardCollapsed
|
||||
i.fa.fa-caret-right
|
||||
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
|
||||
span.card-drag-handle.js-card-drag-handle(title="Drag card")
|
||||
i.fa.fa-arrows
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
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
|
||||
span.card-drag-handle.js-card-drag-handle(title="Drag card")
|
||||
i.fa.fa-arrows
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
else
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
i.fa.fa-times-thin
|
||||
@@ -67,7 +66,7 @@ template(name="cardDetails")
|
||||
+viewer
|
||||
if currentBoard.allowsCardNumber
|
||||
span.card-number
|
||||
| ##{getCardNumber}
|
||||
| ##{getCardNumber}
|
||||
= getTitle
|
||||
if isWatching
|
||||
i.card-details-watch
|
||||
@@ -298,7 +297,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
|
||||
@@ -625,19 +624,18 @@ 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
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
+commentForm
|
||||
+comments
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isNoComments
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
+commentForm
|
||||
+comments
|
||||
hr
|
||||
|
||||
.card-details-right
|
||||
@@ -692,154 +690,156 @@ template(name="editCardSortOrderForm")
|
||||
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
|
||||
| {{_ '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
|
||||
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
|
||||
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
|
||||
else
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
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
|
||||
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-upload
|
||||
| {{_ 'export-card'}}
|
||||
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'}}
|
||||
if currentUser
|
||||
unless canModifyCard
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
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-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'}}
|
||||
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-more
|
||||
span.emoji-icon
|
||||
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
|
||||
|
||||
@@ -37,6 +37,7 @@ import CardComments from '/models/cardComments';
|
||||
import { ALLOWED_COLORS } from '/config/const';
|
||||
import { uniqBy } from '/imports/lib/collectionHelpers';
|
||||
import { UserAvatar } from '../users/userAvatar';
|
||||
import { Filter } from '/client/lib/filter';
|
||||
import { BoardSwimlaneListCardDialog } from '/client/lib/dialogWithBoardSwimlaneListCard';
|
||||
import { handleFileUpload } from './attachments';
|
||||
import { InfiniteScrolling } from '/client/lib/infiniteScrolling';
|
||||
@@ -45,8 +46,14 @@ import {
|
||||
getCurrentCardFromContext,
|
||||
} from '/client/lib/currentCard';
|
||||
import uploadProgressManager from '../../lib/uploadProgressManager';
|
||||
import { CSSEvents } from '/client/lib/cssEvents';
|
||||
import { UnsavedEdits } from '/client/lib/unsavedEdits';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { MultiSelection } from '/client/lib/multiSelection';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import autosize from 'autosize';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
// SubsManager removed for Meteor 3 migration
|
||||
const { calculateIndexData } = Utils;
|
||||
|
||||
function getCardId() {
|
||||
@@ -200,22 +207,22 @@ Template.cardDetails.onRendered(function () {
|
||||
EscapeActions.executeUpTo('popup-close');
|
||||
},
|
||||
stop(evt, ui) {
|
||||
let prevChecklist = ui.item.prev('.js-subtasks').get(0);
|
||||
if (prevChecklist) {
|
||||
prevChecklist = Blaze.getData(prevChecklist).subtask;
|
||||
let prevSubtask = ui.item.prev('.js-subtasks').get(0);
|
||||
if (prevSubtask) {
|
||||
prevSubtask = Blaze.getData(prevSubtask).subtask;
|
||||
}
|
||||
let nextChecklist = ui.item.next('.js-subtasks').get(0);
|
||||
if (nextChecklist) {
|
||||
nextChecklist = Blaze.getData(nextChecklist).subtask;
|
||||
let nextSubtask = ui.item.next('.js-subtasks').get(0);
|
||||
if (nextSubtask) {
|
||||
nextSubtask = Blaze.getData(nextSubtask).subtask;
|
||||
}
|
||||
const sortIndex = calculateIndexData(prevChecklist, nextChecklist, 1);
|
||||
const sortIndex = calculateIndexData(prevSubtask, nextSubtask, 1);
|
||||
|
||||
$subtasksDom.sortable('cancel');
|
||||
const subtask = Blaze.getData(ui.item.get(0)).subtask;
|
||||
|
||||
Subtasks.update(subtask._id, {
|
||||
Cards.updateAsync(subtask._id, {
|
||||
$set: {
|
||||
subtaskSort: sortIndex.base,
|
||||
sort: sortIndex.base,
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -261,12 +268,16 @@ Template.cardDetails.helpers({
|
||||
},
|
||||
|
||||
cardMaximized() {
|
||||
return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const maximized = currentUser
|
||||
? currentUser.hasCardMaximized()
|
||||
: window.localStorage.getItem('cardMaximized') === 'true';
|
||||
return !Utils.getPopupCardId() && maximized;
|
||||
},
|
||||
|
||||
showActivities() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
return user && user.hasShowActivities();
|
||||
const card = Template.currentData();
|
||||
return card && card.showActivities;
|
||||
},
|
||||
|
||||
cardCollapsed() {
|
||||
@@ -329,8 +340,29 @@ Template.cardDetails.helpers({
|
||||
return user && user.isVerticalScrollbars();
|
||||
},
|
||||
|
||||
currentSwimlaneListsSorted() {
|
||||
const card = Template.currentData();
|
||||
if (!card || !card.boardId) return [];
|
||||
const board = ReactiveCache.getBoard(card.boardId);
|
||||
if (!board) return [];
|
||||
const swimlaneId = card.swimlaneId;
|
||||
const selector = { boardId: card.boardId, archived: false };
|
||||
if (swimlaneId) {
|
||||
const defaultSwimlane = board.getDefaultSwimline && board.getDefaultSwimline();
|
||||
if (defaultSwimlane && defaultSwimlane._id === swimlaneId) {
|
||||
selector.swimlaneId = { $in: [swimlaneId, null, ''] };
|
||||
} else {
|
||||
selector.swimlaneId = swimlaneId;
|
||||
}
|
||||
}
|
||||
return ReactiveCache.getLists(selector, { sort: { sort: 1 } });
|
||||
},
|
||||
|
||||
isCurrentListId(listId) {
|
||||
const data = Template.currentData();
|
||||
let data = Template.currentData();
|
||||
if (!data || typeof data.listId === 'undefined') {
|
||||
data = Template.parentData(1);
|
||||
}
|
||||
if (!data || typeof data.listId === 'undefined') return false;
|
||||
return data.listId == listId;
|
||||
},
|
||||
@@ -468,7 +500,11 @@ Template.cardDetails.events({
|
||||
},
|
||||
'change .js-date-format-selector'(event) {
|
||||
const dateFormat = event.target.value;
|
||||
Meteor.call('changeDateFormat', dateFormat);
|
||||
if (Meteor.userId()) {
|
||||
Meteor.call('changeDateFormat', dateFormat);
|
||||
} else {
|
||||
window.localStorage.setItem('dateFormat', dateFormat);
|
||||
}
|
||||
},
|
||||
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
|
||||
// Mobile: switch to desktop popup view (maximize)
|
||||
@@ -549,9 +585,8 @@ Template.cardDetails.events({
|
||||
}
|
||||
},
|
||||
async 'change .js-select-card-details-lists'(event, tpl) {
|
||||
const listId = event.target.value;
|
||||
let card = Template.currentData();
|
||||
const listSelect = tpl.$('.js-select-card-details-lists')[0];
|
||||
const listId = listSelect.options[listSelect.selectedIndex].value;
|
||||
|
||||
const minOrder = await card.getMinSort(listId, card.swimlaneId);
|
||||
await card.move(card.boardId, card.swimlaneId, listId, minOrder - 1);
|
||||
@@ -595,15 +630,27 @@ Template.cardDetails.events({
|
||||
const card = Template.currentData();
|
||||
await card.toggleHideCheckedChecklistItems();
|
||||
},
|
||||
async 'change #toggleShowActivitiesCard'() {
|
||||
const card = Template.currentData();
|
||||
await card.toggleShowActivities();
|
||||
},
|
||||
'click #toggleCustomFieldsGridButton'() {
|
||||
Meteor.call('toggleCustomFieldsGrid');
|
||||
},
|
||||
'click .js-maximize-card-details'() {
|
||||
Meteor.call('toggleCardMaximized');
|
||||
if (Meteor.userId()) {
|
||||
Meteor.call('toggleCardMaximized');
|
||||
} else {
|
||||
window.localStorage.setItem('cardMaximized', 'true');
|
||||
}
|
||||
autosize($('.card-details'));
|
||||
},
|
||||
'click .js-minimize-card-details'() {
|
||||
Meteor.call('toggleCardMaximized');
|
||||
if (Meteor.userId()) {
|
||||
Meteor.call('toggleCardMaximized');
|
||||
} else {
|
||||
window.localStorage.setItem('cardMaximized', 'false');
|
||||
}
|
||||
autosize($('.card-details'));
|
||||
},
|
||||
'click .js-vote'(e) {
|
||||
@@ -760,7 +807,10 @@ Template.cardDetails.helpers({
|
||||
},
|
||||
isDateFormat(format) {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (!currentUser) return format === 'YYYY-MM-DD';
|
||||
if (!currentUser) {
|
||||
const stored = window.localStorage.getItem('dateFormat') || 'YYYY-MM-DD';
|
||||
return format === stored;
|
||||
}
|
||||
return currentUser.getDateFormat() === format;
|
||||
},
|
||||
// Upload progress helpers
|
||||
@@ -1089,14 +1139,21 @@ const filterMembers = (filterTerm) => {
|
||||
let members = currBoard.activeMembers();
|
||||
|
||||
if (filterTerm) {
|
||||
const searchTerm = filterTerm.toLowerCase();
|
||||
members = members
|
||||
.map(member => ({
|
||||
member,
|
||||
user: ReactiveCache.getUser(member.userId)
|
||||
}))
|
||||
.filter(({ user }) =>
|
||||
(user.profile.fullname !== undefined && user.profile.fullname.toLowerCase().indexOf(filterTerm.toLowerCase()) !== -1)
|
||||
|| user.profile.fullname === undefined && user.profile.username !== undefined && user.profile.username.toLowerCase().indexOf(filterTerm.toLowerCase()) !== -1)
|
||||
.filter(({ user }) => {
|
||||
// Check if user data is available
|
||||
if (!user || !user.profile) {
|
||||
return false;
|
||||
}
|
||||
const fullname = (user.profile.fullname || '').toLowerCase();
|
||||
const username = (user.username || '').toLowerCase();
|
||||
return fullname.indexOf(searchTerm) !== -1 || username.indexOf(searchTerm) !== -1;
|
||||
})
|
||||
.map(({ member }) => member);
|
||||
}
|
||||
return members;
|
||||
@@ -1427,7 +1484,8 @@ Template.setCardColorPopup.helpers({
|
||||
|
||||
Template.setCardColorPopup.events({
|
||||
'click .js-palette-color'(event, tpl) {
|
||||
tpl.currentColor.set(Template.currentData().color);
|
||||
const paletteData = Blaze.getData(event.currentTarget);
|
||||
tpl.currentColor.set(paletteData?.color);
|
||||
},
|
||||
async 'click .js-submit'(event, tpl) {
|
||||
event.preventDefault();
|
||||
@@ -1446,7 +1504,9 @@ Template.setCardColorPopup.events({
|
||||
});
|
||||
|
||||
Template.setSelectionColorPopup.onCreated(function () {
|
||||
this.currentColor = new ReactiveVar(null);
|
||||
const selectedCards = ReactiveCache.getCards(MultiSelection.getMongoSelector());
|
||||
const uniqueColors = [...new Set(selectedCards.map(card => card.color || null))];
|
||||
this.currentColor = new ReactiveVar(uniqueColors.length === 1 ? uniqueColors[0] : null);
|
||||
});
|
||||
|
||||
Template.setSelectionColorPopup.helpers({
|
||||
@@ -1467,22 +1527,32 @@ Template.setSelectionColorPopup.events({
|
||||
const color = colorClass ? colorClass.replace('card-details-', '') : null;
|
||||
tpl.currentColor.set(color);
|
||||
},
|
||||
async 'click .js-submit'(event, tpl) {
|
||||
async 'submit form.edit-label'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const color = tpl.currentColor.get();
|
||||
// Use MultiSelection to get selected cards and set color on each
|
||||
for (const card of ReactiveCache.getCards(MultiSelection.getMongoSelector())) {
|
||||
await card.setColor(color);
|
||||
try {
|
||||
for (const card of ReactiveCache.getCards(MultiSelection.getMongoSelector())) {
|
||||
await card.setColor(color);
|
||||
}
|
||||
Popup.back();
|
||||
} catch (error) {
|
||||
alert(error?.reason || error?.message || 'Failed to save selection color');
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
async 'click .js-submit'(event, tpl) {
|
||||
event.preventDefault();
|
||||
await tpl.$('form.edit-label').trigger('submit');
|
||||
},
|
||||
async 'click .js-remove-color'(event, tpl) {
|
||||
event.preventDefault();
|
||||
// Use MultiSelection to get selected cards and remove color from each
|
||||
for (const card of ReactiveCache.getCards(MultiSelection.getMongoSelector())) {
|
||||
await card.setColor(null);
|
||||
try {
|
||||
for (const card of ReactiveCache.getCards(MultiSelection.getMongoSelector())) {
|
||||
await card.setColor(null);
|
||||
}
|
||||
Popup.back();
|
||||
} catch (error) {
|
||||
alert(error?.reason || error?.message || 'Failed to unset selection color');
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1593,7 +1663,7 @@ Template.cardMorePopup.events({
|
||||
if (selection === 'none') {
|
||||
tpl.parentBoard.set(null);
|
||||
} else {
|
||||
subManager.subscribe('board', $(event.currentTarget).val(), false);
|
||||
Meteor.subscribe('board', $(event.currentTarget).val(), false);
|
||||
tpl.parentBoard.set(selection);
|
||||
list.prop('disabled', false);
|
||||
}
|
||||
@@ -1823,8 +1893,8 @@ EscapeActions.register(
|
||||
Session.set('cardDetailsIsMouseDown', false);
|
||||
|
||||
} else {
|
||||
// Prevent close card when the user is selecting text and moves the mouse cursor outside the card detail area
|
||||
Utils.goBoardId(Session.get('currentBoard'));
|
||||
// Trigger the close button so all close logic (openCards, routing, mini-screen) runs consistently
|
||||
$('.js-close-card-details').first().trigger('click');
|
||||
}
|
||||
},
|
||||
() => {
|
||||
|
||||
@@ -34,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
|
||||
@@ -85,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'}}
|
||||
@@ -171,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,9 +2,14 @@ import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import Cards from '/models/cards';
|
||||
import Boards from '/models/boards';
|
||||
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();
|
||||
// SubsManager removed for Meteor 3 migration
|
||||
const { calculateIndexData } = Utils;
|
||||
|
||||
function initSorting(items) {
|
||||
@@ -130,8 +135,11 @@ Template.checklists.events({
|
||||
event.preventDefault();
|
||||
const textarea = tpl.find('textarea.js-edit-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
const checklist = Template.currentData().checklist;
|
||||
checklist.setTitle(title);
|
||||
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();
|
||||
@@ -218,7 +226,7 @@ Template.checklists.events({
|
||||
event.preventDefault();
|
||||
Template.currentData().card.toggleHideFinishedChecklist();
|
||||
},
|
||||
keydown(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) {
|
||||
@@ -266,6 +274,11 @@ Template.checklistActionsPopup.events({
|
||||
Template.currentData().checklist.toggleHideAllChecklistItems();
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-show-checklist-at-minicard'(event) {
|
||||
event.preventDefault();
|
||||
Template.currentData().checklist.toggleShowChecklistAtMinicard();
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
Template.editChecklistItemForm.onRendered(function () {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
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;
|
||||
});
|
||||
|
||||
const getFallbackLabelColor = () => {
|
||||
|
||||
@@ -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;
|
||||
@@ -30,13 +30,13 @@
|
||||
}
|
||||
@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;
|
||||
@@ -77,7 +77,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,
|
||||
@@ -91,9 +91,9 @@
|
||||
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 {
|
||||
@@ -105,7 +105,7 @@
|
||||
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;
|
||||
@@ -137,7 +137,7 @@
|
||||
height: clamp(20px, 2.5vw, 28px);
|
||||
position: absolute;
|
||||
right: 0vw;
|
||||
top: 4vh;
|
||||
top: 28px;
|
||||
display: none;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -313,7 +313,7 @@
|
||||
|
||||
.minicard .badges {
|
||||
float: left;
|
||||
margin-top: 1vh;
|
||||
margin-top: 8px;
|
||||
color: #808080;
|
||||
}
|
||||
.minicard .badges:empty {
|
||||
@@ -322,7 +322,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 {
|
||||
@@ -356,7 +356,7 @@
|
||||
.minicard .minicard-creator {
|
||||
float: right;
|
||||
margin-left: 0.7vw;
|
||||
margin-bottom: 0.5vh;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.minicard .minicard-members .member,
|
||||
.minicard .minicard-assignees .member,
|
||||
@@ -365,7 +365,7 @@
|
||||
border-radius: 50%;
|
||||
height: clamp(24px, 3.5vw, 32px);
|
||||
width: clamp(24px, 3.5vw, 32px);
|
||||
margin-bottom: 0.5vh;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.minicard .minicard-members .assignee,
|
||||
.minicard .minicard-assignees .assignee,
|
||||
@@ -378,7 +378,7 @@
|
||||
.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;
|
||||
@@ -391,11 +391,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;
|
||||
@@ -405,7 +405,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 {
|
||||
@@ -416,9 +416,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 {
|
||||
@@ -736,7 +736,7 @@
|
||||
.minicard-list-name {
|
||||
font-size: 0.75em;
|
||||
color: #8c8c8c;
|
||||
margin-top: 0.2vh;
|
||||
margin-top: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3vw;
|
||||
@@ -745,9 +745,11 @@
|
||||
/* Checklist display on minicard */
|
||||
.minicard-checklist {
|
||||
width: 100%;
|
||||
margin-top: 0.5vh;
|
||||
margin-bottom: 0.5vh;
|
||||
padding: 0.3vh 0.5vw;
|
||||
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;
|
||||
@@ -757,7 +759,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.3vh;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-title {
|
||||
@@ -765,6 +767,7 @@
|
||||
font-weight: bold;
|
||||
color: #4d4d4d;
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-menu {
|
||||
@@ -783,13 +786,13 @@
|
||||
.minicard-checklist .checklist-item {
|
||||
font-size: 0.75em;
|
||||
color: #666;
|
||||
margin-bottom: 0.2vh;
|
||||
margin-bottom: 2px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.3vw;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
padding: 0.2vh 0;
|
||||
padding: 2px 0;
|
||||
border-radius: 0.2vw;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
@@ -806,7 +809,7 @@
|
||||
.minicard-checklist .checklist-item .check-box-unicode {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.8em;
|
||||
margin-top: 0.1vh;
|
||||
margin-top: 1px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,17 +11,21 @@ template(name="minicard")
|
||||
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
.dates
|
||||
if getReceived
|
||||
.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
|
||||
+minicardDueDate
|
||||
if showEnd
|
||||
if getEnd
|
||||
+minicardEndDate
|
||||
if getSpentTime
|
||||
.date
|
||||
+cardSpentTime
|
||||
@@ -68,19 +72,20 @@ template(name="minicard")
|
||||
span.linked-icon.linked-archived
|
||||
i.fa.fa-archive
|
||||
+viewer
|
||||
if currentBoard.allowsCardNumber
|
||||
if showCardNumber
|
||||
span.card-number
|
||||
| ##{getCardNumber}
|
||||
| ##{getCardNumber}
|
||||
= getTitle
|
||||
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
|
||||
@@ -143,14 +148,16 @@ 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.badge-comment.badge-text
|
||||
i.fa.fa-comment-o
|
||||
= ' '
|
||||
= comments.length
|
||||
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'}}
|
||||
@@ -179,21 +186,19 @@ template(name="minicard")
|
||||
span.badge-icon
|
||||
i.fa.fa-paperclip
|
||||
span.badge-text= attachments.length
|
||||
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 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
|
||||
i.fa.fa-sort-numeric-asc
|
||||
span.badge-text.check-list-sort {{ sort }}
|
||||
if shouldShowChecklistAtMinicard
|
||||
each shouldShowChecklistAtMinicard
|
||||
+minicardChecklist(checklist=. card=..)
|
||||
if currentBoard.allowsDescriptionTextOnMinicard
|
||||
if getDescription
|
||||
.minicard-description
|
||||
@@ -218,10 +223,26 @@ template(name="editCardSortOrderPopup")
|
||||
template(name="minicardChecklist")
|
||||
.minicard-checklist
|
||||
.checklist-header
|
||||
.checklist-title= checklist.title
|
||||
if canModifyCard
|
||||
+inlinedForm(classNames="js-edit-checklist-title" checklist=checklist)
|
||||
+editChecklistForm(checklist=checklist)
|
||||
else
|
||||
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
|
||||
each visibleItems
|
||||
+checklistItemDetail(item = . checklist = checklist card = card)
|
||||
.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
|
||||
|
||||
@@ -3,6 +3,19 @@ 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')
|
||||
@@ -49,31 +62,41 @@ Template.minicard.helpers({
|
||||
},
|
||||
|
||||
showMembers() {
|
||||
// cache "board" to reduce the mini-mongodb access
|
||||
const board = this.board();
|
||||
let ret = false;
|
||||
if (board) {
|
||||
ret =
|
||||
board.allowsMembers === null ||
|
||||
board.allowsMembers === undefined ||
|
||||
board.allowsMembers
|
||||
;
|
||||
}
|
||||
return ret;
|
||||
return getMinicardFlag(board, 'allowsMembersOnMinicard', 'allowsMembers', true);
|
||||
},
|
||||
|
||||
showAssignee() {
|
||||
// cache "board" to reduce the mini-mongodb access
|
||||
const board = this.board();
|
||||
let ret = false;
|
||||
if (board) {
|
||||
ret =
|
||||
board.allowsAssignee === null ||
|
||||
board.allowsAssignee === undefined ||
|
||||
board.allowsAssignee
|
||||
;
|
||||
}
|
||||
return ret;
|
||||
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);
|
||||
},
|
||||
|
||||
hiddenMinicardLabelText() {
|
||||
@@ -251,6 +274,13 @@ Template.minicardChecklist.helpers({
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -258,6 +288,66 @@ Template.minicardChecklist.events({
|
||||
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({
|
||||
|
||||
@@ -1,7 +1,52 @@
|
||||
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 '#';
|
||||
},
|
||||
});
|
||||
|
||||
Template.resultCard.events({
|
||||
|
||||
@@ -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;
|
||||
@@ -175,6 +185,7 @@ body.grey-icons-enabled .subtasks-item .check-box-unicode {
|
||||
display: inline-block;
|
||||
}
|
||||
.subtask-details-menu {
|
||||
float: right;
|
||||
flex-shrink: 0;
|
||||
padding: 6px 10px 6px 10px;
|
||||
order: 2;
|
||||
}
|
||||
@@ -1,8 +1,17 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import Cards from '/models/cards';
|
||||
import { Filter } from '/client/lib/filter';
|
||||
|
||||
Template.subtasks.events({
|
||||
'click .js-open-subtask-details-menu': Popup.open('subtaskActions'),
|
||||
'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 = tpl.find('textarea.js-add-subtask-item');
|
||||
@@ -11,8 +20,16 @@ Template.subtasks.events({
|
||||
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({
|
||||
@@ -20,10 +37,12 @@ Template.subtasks.events({
|
||||
_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
|
||||
@@ -33,7 +52,7 @@ Template.subtasks.events({
|
||||
const nextCardNumber = await targetBoard.getNextCardNumber();
|
||||
|
||||
if (title) {
|
||||
const _id = Cards.insert({
|
||||
const _id = await Cards.insertAsync({
|
||||
title,
|
||||
parentId: cardId,
|
||||
members: [],
|
||||
@@ -44,7 +63,7 @@ Template.subtasks.events({
|
||||
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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import Cards from '/models/cards';
|
||||
|
||||
// Add click handler to ganttView for card titles
|
||||
Template.ganttView.events({
|
||||
@@ -14,6 +15,7 @@ Template.ganttView.events({
|
||||
},
|
||||
});
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
// Blaze template helpers for ganttView
|
||||
function getISOWeekInfo(d) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import { wekanGetMembersToMap } from './wekanMembersMapper';
|
||||
import { csvGetMembersToMap } from './csvMembersMapper';
|
||||
import getSlug from 'limax';
|
||||
import { UserSearchIndex } from '/models/users';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
const Papa = require('papaparse');
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { ReactiveVar } = require('meteor/reactive-var');
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
|
||||
// Creation-time stack tracking which basicTabs is currently being rendered.
|
||||
// Blaze creates parent views before children, so when tabContent.onCreated fires,
|
||||
|
||||
@@ -152,12 +152,12 @@ body.list-resizing-active * {
|
||||
/* Ensure consistent header height for all lists */
|
||||
.list-header {
|
||||
/* Maintain consistent height and padding for all lists */
|
||||
min-height: 2.5vh !important;
|
||||
min-height: 30px !important;
|
||||
height: auto !important;
|
||||
padding: 2.5vh 1.5vw 0.5vh !important;
|
||||
padding: 28px 1.5vw 6px !important;
|
||||
/* Make sure the background covers the full height */
|
||||
background-color: #e4e4e4 !important;
|
||||
border-bottom: 0.8vh solid #e4e4e4 !important;
|
||||
border-bottom: 8px solid #e4e4e4 !important;
|
||||
/* Use original display for consistent button positioning */
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
@@ -401,24 +401,24 @@ form.inlined-form .list-composer {
|
||||
.list.list-composer .list-name-input,
|
||||
.list .list-composer .list-name-input {
|
||||
background: #fff;
|
||||
margin: -0.4vh 0 1vh;
|
||||
margin: -4px 0 8px;
|
||||
}
|
||||
.list-header-add {
|
||||
flex: 0 0 auto;
|
||||
padding: 1.5vh 1.5vw;
|
||||
padding: 14px 1.5vw;
|
||||
position: relative;
|
||||
min-height: 2.5vh;
|
||||
min-height: 30px;
|
||||
}
|
||||
.list-header {
|
||||
flex: 0 0 auto;
|
||||
padding: 2.5vh 1.5vw 0.5vh;
|
||||
padding: 28px 1.5vw 6px;
|
||||
position: relative;
|
||||
min-height: 2.5vh;
|
||||
min-height: 30px;
|
||||
background-color: #e4e4e4;
|
||||
border-bottom: 0.8vh solid #e4e4e4;
|
||||
border-bottom: 8px solid #e4e4e4;
|
||||
}
|
||||
.list-header.list-header-card-count {
|
||||
min-height: 4.5vh;
|
||||
min-height: 45px;
|
||||
height: auto;
|
||||
}
|
||||
.list-header.ui-sortable-handle {
|
||||
@@ -433,7 +433,7 @@ form.inlined-form .list-composer {
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
min-height: 1.2vh;
|
||||
min-height: 14px;
|
||||
min-width: 4vw;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
@@ -1196,7 +1196,7 @@ form.inlined-form .list-composer {
|
||||
|
||||
/* Allow long list titles to expand on desktop (non-mobile, non-collapsed) */
|
||||
.list:not(.mobile-view):not(.list-collapsed) .list-header {
|
||||
padding-top: calc(2.5vh - 6px) !important;
|
||||
padding-top: 22px !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
@@ -1226,52 +1226,52 @@ form.inlined-form .list-composer {
|
||||
margin-top: unset !important;
|
||||
}
|
||||
.list-header-white {
|
||||
border-bottom: 6px solid #fff;
|
||||
border-bottom: 6px solid #fff !important;
|
||||
}
|
||||
.list-header-green {
|
||||
border-bottom: 6px solid #3cb500;
|
||||
border-bottom: 6px solid #3cb500 !important;
|
||||
}
|
||||
.list-header-yellow {
|
||||
border-bottom: 6px solid #fad900;
|
||||
border-bottom: 6px solid #fad900 !important;
|
||||
}
|
||||
.list-header-orange {
|
||||
border-bottom: 6px solid #ff9f19;
|
||||
border-bottom: 6px solid #ff9f19 !important;
|
||||
}
|
||||
.list-header-red {
|
||||
border-bottom: 6px solid #eb4646;
|
||||
border-bottom: 6px solid #eb4646 !important;
|
||||
}
|
||||
.list-header-purple {
|
||||
border-bottom: 6px solid #a632db;
|
||||
border-bottom: 6px solid #a632db !important;
|
||||
}
|
||||
.list-header-blue {
|
||||
border-bottom: 6px solid #0079bf;
|
||||
border-bottom: 6px solid #0079bf !important;
|
||||
}
|
||||
.list-header-pink {
|
||||
border-bottom: 6px solid #ff78cb;
|
||||
border-bottom: 6px solid #ff78cb !important;
|
||||
}
|
||||
.list-header-sky {
|
||||
border-bottom: 6px solid #00c2e0;
|
||||
border-bottom: 6px solid #00c2e0 !important;
|
||||
}
|
||||
.list-header-black {
|
||||
border-bottom: 6px solid #4d4d4d;
|
||||
border-bottom: 6px solid #4d4d4d !important;
|
||||
}
|
||||
.list-header-lime {
|
||||
border-bottom: 6px solid #51e898;
|
||||
border-bottom: 6px solid #51e898 !important;
|
||||
}
|
||||
.list-header-silver {
|
||||
border-bottom: 6px solid #e4e4e4;
|
||||
border-bottom: 6px solid #e4e4e4 !important;
|
||||
}
|
||||
.list-header-peachpuff {
|
||||
border-bottom: 6px solid #ffdab9;
|
||||
border-bottom: 6px solid #ffdab9 !important;
|
||||
}
|
||||
.list-header-crimson {
|
||||
border-bottom: 6px solid #dc143c;
|
||||
border-bottom: 6px solid #dc143c !important;
|
||||
}
|
||||
.list-header-plum {
|
||||
border-bottom: 6px solid #dda0dd;
|
||||
border-bottom: 6px solid #dda0dd !important;
|
||||
}
|
||||
.list-header-darkgreen {
|
||||
border-bottom: 6px solid #006400;
|
||||
border-bottom: 6px solid #006400 !important;
|
||||
}
|
||||
|
||||
/* Final collapsed desktop overrides:
|
||||
@@ -1379,31 +1379,31 @@ form.inlined-form .list-composer {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.list-header-slateblue {
|
||||
border-bottom: 6px solid #6a5acd;
|
||||
border-bottom: 6px solid #6a5acd !important;
|
||||
}
|
||||
.list-header-magenta {
|
||||
border-bottom: 6px solid #f0f;
|
||||
border-bottom: 6px solid #f0f !important;
|
||||
}
|
||||
.list-header-gold {
|
||||
border-bottom: 6px solid #ffd700;
|
||||
border-bottom: 6px solid #ffd700 !important;
|
||||
}
|
||||
.list-header-navy {
|
||||
border-bottom: 6px solid #000080;
|
||||
border-bottom: 6px solid #000080 !important;
|
||||
}
|
||||
.list-header-gray {
|
||||
border-bottom: 6px solid #808080;
|
||||
border-bottom: 6px solid #808080 !important;
|
||||
}
|
||||
.list-header-saddlebrown {
|
||||
border-bottom: 6px solid #8b4513;
|
||||
border-bottom: 6px solid #8b4513 !important;
|
||||
}
|
||||
.list-header-paleturquoise {
|
||||
border-bottom: 6px solid #afeeee;
|
||||
border-bottom: 6px solid #afeeee !important;
|
||||
}
|
||||
.list-header-mistyrose {
|
||||
border-bottom: 6px solid #ffe4e1;
|
||||
border-bottom: 6px solid #ffe4e1 !important;
|
||||
}
|
||||
.list-header-indigo {
|
||||
border-bottom: 6px solid #4b0082;
|
||||
border-bottom: 6px solid #4b0082 !important;
|
||||
}
|
||||
|
||||
.list.list-collapsed .collapsed-list-drag-area {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { MultiSelection } from '/client/lib/multiSelection';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
require('/client/lib/jquery-ui.js')
|
||||
|
||||
const { calculateIndex } = Utils;
|
||||
|
||||
@@ -1,39 +1,38 @@
|
||||
template(name="listBody")
|
||||
unless collapsed
|
||||
.list-body(class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}")
|
||||
.minicards.clearfix.js-minicards(class="{{#if reachedWipLimit}}js-list-full{{/if}}")
|
||||
+inlinedForm(autoclose=false position="top")
|
||||
+addCardForm(listId=_id position="top")
|
||||
ul.sidebar-list
|
||||
each customFieldsSum
|
||||
li
|
||||
.list-body(class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}")
|
||||
.minicards.clearfix.js-minicards(class="{{#if reachedWipLimit}}js-list-full{{/if}}")
|
||||
+inlinedForm(autoclose=false position="top")
|
||||
+addCardForm(listId=_id position="top")
|
||||
ul.sidebar-list
|
||||
each customFieldsSum
|
||||
li
|
||||
+viewer
|
||||
= name
|
||||
if $eq customFieldsSum.type "number"
|
||||
+viewer
|
||||
= name
|
||||
if $eq customFieldsSum.type "number"
|
||||
+viewer
|
||||
= value
|
||||
if $eq customFieldsSum.type "currency"
|
||||
+viewer
|
||||
= formattedCurrencyCustomFieldValue(value)
|
||||
each (cardsWithLimit (idOrNull ../../_id))
|
||||
a.minicard-wrapper.js-minicard(href=originRelativeUrl
|
||||
class="{{#if cardIsSelected}}is-selected{{/if}}"
|
||||
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
if MultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-multi-selection(
|
||||
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
+minicard(this)
|
||||
if (showSpinner (idOrNull ../../_id))
|
||||
+spinnerList
|
||||
if canSeeAddCard
|
||||
+inlinedForm(autoclose=false position="bottom")
|
||||
+addCardForm(listId=_id position="bottom")
|
||||
else
|
||||
a.open-minicard-composer.js-card-composer.js-open-inlined-form(title="{{_ 'add-card-to-bottom-of-list'}}")
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-card'}}
|
||||
= value
|
||||
if $eq customFieldsSum.type "currency"
|
||||
+viewer
|
||||
= formattedCurrencyCustomFieldValue(value)
|
||||
each (cardsWithLimit (idOrNull ../../_id))
|
||||
a.minicard-wrapper.js-minicard(href=originRelativeUrl
|
||||
class="{{#if cardIsSelected}}is-selected{{/if}}"
|
||||
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
if MultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-multi-selection(
|
||||
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
+minicard(this)
|
||||
if (showSpinner (idOrNull ../../_id))
|
||||
+spinnerList
|
||||
if canSeeAddCard
|
||||
+inlinedForm(autoclose=false position="bottom")
|
||||
+addCardForm(listId=_id position="bottom")
|
||||
else
|
||||
a.open-minicard-composer.js-card-composer.js-open-inlined-form(title="{{_ 'add-card-to-bottom-of-list'}}")
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-card'}}
|
||||
+inlinedForm(autoclose=false position="bottom")
|
||||
+addCardForm(listId=_id position="bottom")
|
||||
|
||||
template(name="spinnerList")
|
||||
.sk-spinner.sk-spinner-list(
|
||||
|
||||
@@ -3,8 +3,15 @@ import { TAPi18n } from '/imports/i18n';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import { getSpinnerName, getSpinnerTemplate } from '/client/lib/spinner';
|
||||
import getSlug from 'limax';
|
||||
import Boards from '/models/boards';
|
||||
import Cards from '/models/cards';
|
||||
import Swimlanes from '/models/swimlanes';
|
||||
import { Filter } from '/client/lib/filter';
|
||||
import { MultiSelection } from '/client/lib/multiSelection';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import autosize from 'autosize';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
// SubsManager removed for Meteor 3 migration
|
||||
const InfiniteScrollIter = 10;
|
||||
|
||||
Template.listBody.onCreated(function () {
|
||||
@@ -59,7 +66,10 @@ Template.listBody.onCreated(function () {
|
||||
};
|
||||
|
||||
this.idOrNull = (swimlaneId) => {
|
||||
const data = Template.currentData();
|
||||
const data = this.data;
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
Utils.boardView() === 'board-view-swimlanes' ||
|
||||
data.board().isTemplatesBoard()
|
||||
@@ -88,7 +98,10 @@ Template.listBody.onCreated(function () {
|
||||
const labelIds = formComponent.labels.get();
|
||||
const customFields = formComponent.customFields.get();
|
||||
|
||||
const data = Template.currentData();
|
||||
const data = this.data;
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const board = data.board();
|
||||
let linkedId = '';
|
||||
let swimlaneId = '';
|
||||
@@ -107,13 +120,15 @@ Template.listBody.onCreated(function () {
|
||||
if (swimlane.isCardTemplatesSwimlane()) cardType = 'template-card';
|
||||
// If this is the board templates swimlane, insert a board template and a linked card
|
||||
else if (swimlane.isBoardTemplatesSwimlane()) {
|
||||
linkedId = Boards.insert({
|
||||
linkedId = await Boards.insertAsync({
|
||||
title,
|
||||
slug: getSlug(title) || 'board',
|
||||
permission: 'private',
|
||||
type: 'template-board',
|
||||
});
|
||||
Swimlanes.insert({
|
||||
title: TAPi18n.__('default'),
|
||||
const defaultTitle = TAPi18n.__('default');
|
||||
await Swimlanes.insertAsync({
|
||||
title: typeof defaultTitle === 'string' ? defaultTitle : 'Default',
|
||||
boardId: linkedId,
|
||||
});
|
||||
cardType = 'cardType-linkedBoard';
|
||||
@@ -177,6 +192,13 @@ Template.listBody.onCreated(function () {
|
||||
}
|
||||
const clickedTitle = $target.closest('.minicard-title').length > 0;
|
||||
const clickedLinkedReference = $target.closest('.js-linked-link').length > 0;
|
||||
const clickedChecklist = $target.closest('.minicard-checklist').length > 0;
|
||||
|
||||
if (clickedChecklist) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Title clicks should open the regular board card details view.
|
||||
if (clickedTitle && !clickedLinkedReference) {
|
||||
@@ -294,7 +316,15 @@ Template.listBody.helpers({
|
||||
listId: Template.currentData()._id,
|
||||
archived: false,
|
||||
};
|
||||
if (swimlaneId) selector.swimlaneId = swimlaneId;
|
||||
if (swimlaneId) {
|
||||
// Fallback: also show cards with no swimlaneId (null/empty) so that
|
||||
// pre-migration / orphaned cards are always visible without a DB migration.
|
||||
selector.$or = [
|
||||
{ swimlaneId },
|
||||
{ swimlaneId: null }, // null covers null AND missing field
|
||||
{ swimlaneId: '' }, // empty string from shared-lists era
|
||||
];
|
||||
}
|
||||
const ret = ReactiveCache.getCards(Filter.mongoSelector(selector), {
|
||||
// sort: ['sort'],
|
||||
sort: sortBy,
|
||||
@@ -554,7 +584,7 @@ Template.linkCardPopup.onCreated(function () {
|
||||
|
||||
this.boardId = Session.get('currentBoard');
|
||||
// In order to get current board info
|
||||
subManager.subscribe('board', this.boardId, false);
|
||||
Meteor.subscribe('board', this.boardId, false);
|
||||
this.board = ReactiveCache.getBoard(this.boardId);
|
||||
// List where to insert card
|
||||
this.list = $(Popup._getTopStack().openerElement).closest('.js-list');
|
||||
@@ -685,7 +715,7 @@ Template.linkCardPopup.helpers({
|
||||
Template.linkCardPopup.events({
|
||||
'change .js-select-boards'(evt, tpl) {
|
||||
const val = $(evt.currentTarget).val();
|
||||
subManager.subscribe('board', val, false);
|
||||
Meteor.subscribe('board', val, false);
|
||||
// Clear selections to allow linking only board or re-choose swimlane/list
|
||||
tpl.selectedSwimlaneId.set('');
|
||||
tpl.selectedListId.set('');
|
||||
@@ -773,7 +803,7 @@ Template.searchElementPopup.onCreated(function () {
|
||||
if (this.isTemplateSearch) {
|
||||
const boardId = (ReactiveCache.getCurrentUser().profile || {}).templatesBoardId;
|
||||
if (boardId) {
|
||||
subManager.subscribe('board', boardId, false);
|
||||
Meteor.subscribe('board', boardId, false);
|
||||
this.board = ReactiveCache.getBoard(boardId);
|
||||
}
|
||||
} else {
|
||||
@@ -785,7 +815,7 @@ Template.searchElementPopup.onCreated(function () {
|
||||
}
|
||||
this.boardId = this.board._id;
|
||||
// Subscribe to this board
|
||||
subManager.subscribe('board', this.boardId, false);
|
||||
Meteor.subscribe('board', this.boardId, false);
|
||||
this.selectedBoardId = new ReactiveVar(this.boardId);
|
||||
this.list = $(Popup._getTopStack().openerElement).closest('.js-list');
|
||||
|
||||
@@ -849,7 +879,7 @@ Template.searchElementPopup.helpers({
|
||||
} else if (tpl.isBoardTemplateSearch) {
|
||||
const boards = board.searchBoards(tpl.term.get());
|
||||
boards.forEach(board => {
|
||||
subManager.subscribe('board', board.linkedId, false);
|
||||
Meteor.subscribe('board', board.linkedId, false);
|
||||
});
|
||||
return boards;
|
||||
} else {
|
||||
@@ -860,7 +890,7 @@ Template.searchElementPopup.helpers({
|
||||
|
||||
Template.searchElementPopup.events({
|
||||
'change .js-select-boards'(evt, tpl) {
|
||||
subManager.subscribe('board', $(evt.currentTarget).val(), false);
|
||||
Meteor.subscribe('board', $(evt.currentTarget).val(), false);
|
||||
tpl.selectedBoardId.set($(evt.currentTarget).val());
|
||||
},
|
||||
'submit .js-search-term-form'(evt, tpl) {
|
||||
@@ -915,7 +945,7 @@ Template.searchElementPopup.events({
|
||||
},
|
||||
(err, data) => {
|
||||
_id = data;
|
||||
subManager.subscribe('board', _id, false);
|
||||
Meteor.subscribe('board', _id, false);
|
||||
FlowRouter.go('board', {
|
||||
id: _id,
|
||||
slug: getSlug(element.title),
|
||||
|
||||
@@ -3,10 +3,14 @@ import Lists from '../../../models/lists';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import dragscroll from '@wekanteam/dragscroll';
|
||||
import { BoardSwimlaneListDialog } from '/client/lib/dialogWithBoardSwimlaneList';
|
||||
import Cards from '/models/cards';
|
||||
import { LIST_COLORS } from '/models/metadata/colors';
|
||||
import { MultiSelection } from '/client/lib/multiSelection';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
let listsColors;
|
||||
Meteor.startup(() => {
|
||||
listsColors = Lists.simpleSchema()._schema.color.allowedValues;
|
||||
listsColors = LIST_COLORS;
|
||||
});
|
||||
|
||||
Template.listHeader.helpers({
|
||||
@@ -439,14 +443,39 @@ Template.setListColorPopup.helpers({
|
||||
|
||||
Template.setListColorPopup.events({
|
||||
'click .js-palette-color'(event, tpl) {
|
||||
tpl.currentColor.set(Template.currentData().color);
|
||||
const paletteData = Blaze.getData(event.currentTarget);
|
||||
tpl.currentColor.set(paletteData?.color);
|
||||
},
|
||||
async 'submit form'(event, tpl) {
|
||||
event.preventDefault();
|
||||
console.log('[ListColor] submit form, color:', tpl.currentColor.get(), 'listId:', tpl.currentList?._id, 'setColor type:', typeof tpl.currentList?.setColor);
|
||||
try {
|
||||
const result = await tpl.currentList.setColor(tpl.currentColor.get());
|
||||
console.log('[ListColor] submit form setColor result:', result);
|
||||
} catch (err) {
|
||||
console.error('[ListColor] submit form setColor error:', err);
|
||||
}
|
||||
Popup.close();
|
||||
},
|
||||
async 'click .js-submit'(event, tpl) {
|
||||
await tpl.currentList.setColor(tpl.currentColor.get());
|
||||
event.preventDefault();
|
||||
console.log('[ListColor] click submit, color:', tpl.currentColor.get(), 'listId:', tpl.currentList?._id, 'setColor type:', typeof tpl.currentList?.setColor);
|
||||
try {
|
||||
const result = await tpl.currentList.setColor(tpl.currentColor.get());
|
||||
console.log('[ListColor] click submit setColor result:', result);
|
||||
} catch (err) {
|
||||
console.error('[ListColor] click submit setColor error:', err);
|
||||
}
|
||||
Popup.close();
|
||||
},
|
||||
async 'click .js-remove-color'(event, tpl) {
|
||||
await tpl.currentList.setColor(null);
|
||||
event.preventDefault();
|
||||
console.log('[ListColor] remove color, listId:', tpl.currentList?._id);
|
||||
try {
|
||||
await tpl.currentList.setColor(null);
|
||||
} catch (err) {
|
||||
console.error('[ListColor] remove color error:', err);
|
||||
}
|
||||
Popup.close();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import AccessibilitySettings from '/models/accessibilitySettings';
|
||||
|
||||
// Shared helpers for both accessibility templates
|
||||
const accessibilityHelpers = {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Boards from '/models/boards';
|
||||
|
||||
Template.bookmarks.helpers({
|
||||
hasStarredBoards() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
// const subManager = new SubsManager();
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { findWhere } from '/imports/lib/collectionHelpers';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import Attachments from '/models/attachments';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import autosize from 'autosize';
|
||||
var converter = require('@wekanteam/html-to-markdown');
|
||||
|
||||
const specialHandles = [
|
||||
@@ -207,11 +210,11 @@ Template.editor.onRendered(function () {
|
||||
const currentCard = Utils.getCurrentCard();
|
||||
const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
|
||||
const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
|
||||
const processUpload = function(file) {
|
||||
const uploader = Attachments.insert(
|
||||
const processUpload = async function(file) {
|
||||
const uploader = await Attachments.insertAsync(
|
||||
{
|
||||
file,
|
||||
meta: Utils.getCommonAttachmentMetaFrom(card),
|
||||
meta: Utils.getCommonAttachmentMetaFrom(currentCard),
|
||||
chunkSize: 'dynamic',
|
||||
},
|
||||
false,
|
||||
|
||||
@@ -84,6 +84,21 @@
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
.js-new-search {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.board-color-belize.js-new-search {
|
||||
background-color: #216694 !important;
|
||||
color: #fff !important;
|
||||
border: none !important;
|
||||
}
|
||||
.board-color-belize.js-new-search:hover {
|
||||
background-color: #1a4d6a !important;
|
||||
}
|
||||
.board-color-belize.js-new-search:active {
|
||||
background-color: #0d334d !important;
|
||||
}
|
||||
.global-search-operator {
|
||||
font-family: Courier;
|
||||
}
|
||||
|
||||
@@ -11,68 +11,47 @@ template(name="globalSearchModalTitle")
|
||||
| {{_ 'globalSearch-title'}}
|
||||
|
||||
template(name="resultsPaged")
|
||||
if resultsHeading.get
|
||||
h1
|
||||
= resultsHeading.get
|
||||
a(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}")
|
||||
| 🔗
|
||||
each card in results.get
|
||||
if resultsHeading
|
||||
h1= resultsHeading
|
||||
each card in results
|
||||
+resultCard(card)
|
||||
table.global-search-footer
|
||||
tr
|
||||
td.global-search-previous-page
|
||||
if hasPreviousPage.get
|
||||
if hasPreviousPage
|
||||
button.js-previous-page
|
||||
| {{_ 'previous-page' }}
|
||||
td.global-search-next-page(align="right")
|
||||
if hasNextPage.get
|
||||
if hasNextPage
|
||||
button.js-next-page
|
||||
| {{_ 'next-page' }}
|
||||
|
||||
template(name="globalSearch")
|
||||
if currentUser
|
||||
.wrapper
|
||||
form.global-search-page.js-search-query-form
|
||||
.wrapper(class=boardColorClass)
|
||||
form.global-search-page.js-search-query-form(onsubmit="return false;")
|
||||
input.global-search-query-input(
|
||||
style="{# if hasResults.get #}display: inline-block;{#/if#}"
|
||||
style="display: inline-block;"
|
||||
id="global-search-input"
|
||||
type="text"
|
||||
name="searchQuery"
|
||||
placeholder="{{_ 'search-example'}}"
|
||||
value="{{ query.get }}"
|
||||
value="{{ queryValue }}"
|
||||
autofocus dir="auto"
|
||||
)
|
||||
a.js-new-search
|
||||
button.js-new-search.board-color-belize(type="button" style="background: #216694 !important; background-color: #216694 !important; color: #fff !important; border: none !important;")
|
||||
| 🧹
|
||||
if debug.get.show
|
||||
h1 Debug
|
||||
if debug.get.showSelector
|
||||
h2 Selector
|
||||
button.js-copy-debug-selector
|
||||
= 'Copy'
|
||||
pre(
|
||||
id="debug-selector"
|
||||
)
|
||||
= sessionData.selector
|
||||
if debug.get.showProjection
|
||||
h2 Projection
|
||||
button.js-copy-debug-projection
|
||||
= 'Copy'
|
||||
pre(
|
||||
id="debug-projection"
|
||||
)
|
||||
= sessionData.projection
|
||||
if searching.get
|
||||
if searching
|
||||
+spinner
|
||||
else if hasResults.get
|
||||
else if hasResults
|
||||
.global-search-results-list-wrapper
|
||||
if hasQueryErrors.get
|
||||
if hasQueryErrors
|
||||
ul
|
||||
each msg in errorMessages
|
||||
li.global-search-error-messages
|
||||
= msg
|
||||
+resultsPaged(this)
|
||||
else if serverError.get
|
||||
+resultsPaged getSearchData
|
||||
else if serverError
|
||||
.global-search-page
|
||||
.global-search-help
|
||||
h1 {{_ 'server-error' }}
|
||||
@@ -83,22 +62,22 @@ template(name="globalSearch")
|
||||
.global-search-help
|
||||
h2 {{_ 'boards' }}
|
||||
.lists-wrapper
|
||||
each title in myBoardNames.get
|
||||
each title in myBoardNames
|
||||
span.card-label.list-title.js-board-title
|
||||
= title
|
||||
h2 {{_ 'lists' }}
|
||||
.lists-wrapper
|
||||
each title in myLists.get
|
||||
each title in myLists
|
||||
span.card-label.list-title.js-list-title
|
||||
= title
|
||||
h2 {{_ 'label-colors' }}
|
||||
.palette-colors: each label in labelColors
|
||||
span.card-label.palette-color.js-label-color(class="card-label-{{label.color}}")
|
||||
= label.name
|
||||
if myLabelNames.get.length
|
||||
if myLabelNames
|
||||
h2 {{_ 'label-names' }}
|
||||
.lists-wrapper
|
||||
each name in myLabelNames.get
|
||||
each name in myLabelNames
|
||||
span.card-label.list-title.js-label-name
|
||||
= name
|
||||
.global-search-instructions
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { CardSearchPaged } from '../../lib/cardSearch';
|
||||
import Boards from '../../../models/boards';
|
||||
import { LABEL_COLORS } from '/models/metadata/colors';
|
||||
import { Query, QueryErrors } from '../../../config/query-classes';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
|
||||
// const subManager = new SubsManager();
|
||||
|
||||
@@ -98,36 +99,45 @@ Template.globalSearch.helpers({
|
||||
return Meteor.userId();
|
||||
},
|
||||
|
||||
// Return ReactiveVars so jade can use .get pattern
|
||||
// Return ReactiveVar values so Blaze can use them
|
||||
searching() {
|
||||
return Template.instance().search.searching;
|
||||
const val = Template.instance().search.searching.get();
|
||||
return val;
|
||||
},
|
||||
hasResults() {
|
||||
return Template.instance().search.hasResults;
|
||||
const val = Template.instance().search.hasResults.get();
|
||||
return val;
|
||||
},
|
||||
hasQueryErrors() {
|
||||
return Template.instance().search.hasQueryErrors;
|
||||
const val = Template.instance().search.hasQueryErrors.get();
|
||||
return val;
|
||||
},
|
||||
serverError() {
|
||||
return Template.instance().search.serverError;
|
||||
const val = Template.instance().search.serverError.get();
|
||||
return val;
|
||||
},
|
||||
query() {
|
||||
return Template.instance().search.query;
|
||||
},
|
||||
debug() {
|
||||
return Template.instance().search.debug;
|
||||
const val = Template.instance().search.debug.get();
|
||||
return val;
|
||||
},
|
||||
resultsHeading() {
|
||||
return Template.instance().search.resultsHeading;
|
||||
const rv = Template.instance().search.resultsHeading;
|
||||
return rv ? rv.get() : '';
|
||||
},
|
||||
results() {
|
||||
return Template.instance().search.results;
|
||||
const rv = Template.instance().search.results;
|
||||
return rv ? rv.get() : [];
|
||||
},
|
||||
hasNextPage() {
|
||||
return Template.instance().search.hasNextPage;
|
||||
const rv = Template.instance().search.hasNextPage;
|
||||
return rv ? rv.get() : false;
|
||||
},
|
||||
hasPreviousPage() {
|
||||
return Template.instance().search.hasPreviousPage;
|
||||
const rv = Template.instance().search.hasPreviousPage;
|
||||
return rv ? rv.get() : false;
|
||||
},
|
||||
sessionData() {
|
||||
return Template.instance().search.sessionData;
|
||||
@@ -137,124 +147,170 @@ Template.globalSearch.helpers({
|
||||
},
|
||||
|
||||
myLists() {
|
||||
return Template.instance().myLists;
|
||||
const rv = Template.instance().myLists;
|
||||
return rv ? rv.get() : [];
|
||||
},
|
||||
myLabelNames() {
|
||||
return Template.instance().myLabelNames;
|
||||
const rv = Template.instance().myLabelNames;
|
||||
return rv ? rv.get() : [];
|
||||
},
|
||||
myBoardNames() {
|
||||
return Template.instance().myBoardNames;
|
||||
const rv = Template.instance().myBoardNames;
|
||||
return rv ? rv.get() : [];
|
||||
},
|
||||
|
||||
errorMessages() {
|
||||
return errorMessages(Template.instance());
|
||||
},
|
||||
|
||||
searchInstructions() {
|
||||
const tags = {
|
||||
operator_board: TAPi18n.__('operator-board'),
|
||||
operator_list: TAPi18n.__('operator-list'),
|
||||
operator_swimlane: TAPi18n.__('operator-swimlane'),
|
||||
operator_comment: TAPi18n.__('operator-comment'),
|
||||
operator_label: TAPi18n.__('operator-label'),
|
||||
operator_label_abbrev: TAPi18n.__('operator-label-abbrev'),
|
||||
operator_user: TAPi18n.__('operator-user'),
|
||||
operator_user_abbrev: TAPi18n.__('operator-user-abbrev'),
|
||||
operator_member: TAPi18n.__('operator-member'),
|
||||
operator_member_abbrev: TAPi18n.__('operator-member-abbrev'),
|
||||
operator_assignee: TAPi18n.__('operator-assignee'),
|
||||
operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'),
|
||||
operator_creator: TAPi18n.__('operator-creator'),
|
||||
operator_due: TAPi18n.__('operator-due'),
|
||||
operator_created: TAPi18n.__('operator-created'),
|
||||
operator_modified: TAPi18n.__('operator-modified'),
|
||||
operator_status: TAPi18n.__('operator-status'),
|
||||
operator_has: TAPi18n.__('operator-has'),
|
||||
operator_sort: TAPi18n.__('operator-sort'),
|
||||
operator_limit: TAPi18n.__('operator-limit'),
|
||||
operator_debug: TAPi18n.__('operator-debug'),
|
||||
operator_org: TAPi18n.__('operator-org'),
|
||||
operator_team: TAPi18n.__('operator-team'),
|
||||
predicate_overdue: TAPi18n.__('predicate-overdue'),
|
||||
predicate_archived: TAPi18n.__('predicate-archived'),
|
||||
predicate_all: TAPi18n.__('predicate-all'),
|
||||
predicate_ended: TAPi18n.__('predicate-ended'),
|
||||
predicate_week: TAPi18n.__('predicate-week'),
|
||||
predicate_month: TAPi18n.__('predicate-month'),
|
||||
predicate_quarter: TAPi18n.__('predicate-quarter'),
|
||||
predicate_year: TAPi18n.__('predicate-year'),
|
||||
predicate_attachment: TAPi18n.__('predicate-attachment'),
|
||||
predicate_description: TAPi18n.__('predicate-description'),
|
||||
predicate_checklist: TAPi18n.__('predicate-checklist'),
|
||||
predicate_public: TAPi18n.__('predicate-public'),
|
||||
predicate_private: TAPi18n.__('predicate-private'),
|
||||
predicate_due: TAPi18n.__('predicate-due'),
|
||||
predicate_created: TAPi18n.__('predicate-created'),
|
||||
predicate_modified: TAPi18n.__('predicate-modified'),
|
||||
predicate_start: TAPi18n.__('predicate-start'),
|
||||
predicate_end: TAPi18n.__('predicate-end'),
|
||||
predicate_assignee: TAPi18n.__('predicate-assignee'),
|
||||
predicate_member: TAPi18n.__('predicate-member'),
|
||||
predicate_selector: TAPi18n.__('predicate-selector'),
|
||||
predicate_projection: TAPi18n.__('predicate-projection'),
|
||||
};
|
||||
getSearchData() {
|
||||
return Template.instance().search;
|
||||
},
|
||||
|
||||
let text = '';
|
||||
[
|
||||
['# ', 'globalSearch-instructions-heading'],
|
||||
['\n', 'globalSearch-instructions-description'],
|
||||
['\n\n', 'globalSearch-instructions-operators'],
|
||||
['\n- ', 'globalSearch-instructions-operator-board'],
|
||||
['\n- ', 'globalSearch-instructions-operator-list'],
|
||||
['\n- ', 'globalSearch-instructions-operator-swimlane'],
|
||||
['\n- ', 'globalSearch-instructions-operator-comment'],
|
||||
['\n- ', 'globalSearch-instructions-operator-label'],
|
||||
['\n- ', 'globalSearch-instructions-operator-hash'],
|
||||
['\n- ', 'globalSearch-instructions-operator-user'],
|
||||
['\n- ', 'globalSearch-instructions-operator-at'],
|
||||
['\n- ', 'globalSearch-instructions-operator-member'],
|
||||
['\n- ', 'globalSearch-instructions-operator-assignee'],
|
||||
['\n- ', 'globalSearch-instructions-operator-creator'],
|
||||
['\n- ', 'globalSearch-instructions-operator-org'],
|
||||
['\n- ', 'globalSearch-instructions-operator-team'],
|
||||
['\n- ', 'globalSearch-instructions-operator-due'],
|
||||
['\n- ', 'globalSearch-instructions-operator-created'],
|
||||
['\n- ', 'globalSearch-instructions-operator-modified'],
|
||||
['\n- ', 'globalSearch-instructions-operator-status'],
|
||||
['\n - ', 'globalSearch-instructions-status-archived'],
|
||||
['\n - ', 'globalSearch-instructions-status-public'],
|
||||
['\n - ', 'globalSearch-instructions-status-private'],
|
||||
['\n - ', 'globalSearch-instructions-status-all'],
|
||||
['\n - ', 'globalSearch-instructions-status-ended'],
|
||||
['\n- ', 'globalSearch-instructions-operator-has'],
|
||||
['\n- ', 'globalSearch-instructions-operator-sort'],
|
||||
['\n- ', 'globalSearch-instructions-operator-limit'],
|
||||
['\n## ', 'heading-notes'],
|
||||
['\n- ', 'globalSearch-instructions-notes-1'],
|
||||
['\n- ', 'globalSearch-instructions-notes-2'],
|
||||
['\n- ', 'globalSearch-instructions-notes-3'],
|
||||
['\n- ', 'globalSearch-instructions-notes-3-2'],
|
||||
['\n- ', 'globalSearch-instructions-notes-4'],
|
||||
['\n- ', 'globalSearch-instructions-notes-5'],
|
||||
].forEach(([prefix, instruction]) => {
|
||||
text += `${prefix}${TAPi18n.__(instruction, tags)}`
|
||||
// Replace *<text>* with `<text>` so markdown shows correctly
|
||||
.replace(/\*\</, '`<')
|
||||
.replace(/\>\*/, '\>\`')
|
||||
});
|
||||
return text;
|
||||
boardColorClass() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const board = boardId ? ReactiveCache.getBoard(boardId) : null;
|
||||
return board ? board.colorClass() : 'board-color-belize';
|
||||
},
|
||||
|
||||
searchInstructions() {
|
||||
try {
|
||||
const tags = {
|
||||
operator_board: TAPi18n.__('operator-board'),
|
||||
operator_list: TAPi18n.__('operator-list'),
|
||||
operator_swimlane: TAPi18n.__('operator-swimlane'),
|
||||
operator_comment: TAPi18n.__('operator-comment'),
|
||||
operator_label: TAPi18n.__('operator-label'),
|
||||
operator_label_abbrev: TAPi18n.__('operator-label-abbrev'),
|
||||
operator_user: TAPi18n.__('operator-user'),
|
||||
operator_user_abbrev: TAPi18n.__('operator-user-abbrev'),
|
||||
operator_member: TAPi18n.__('operator-member'),
|
||||
operator_member_abbrev: TAPi18n.__('operator-member-abbrev'),
|
||||
operator_assignee: TAPi18n.__('operator-assignee'),
|
||||
operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'),
|
||||
operator_creator: TAPi18n.__('operator-creator'),
|
||||
operator_due: TAPi18n.__('operator-due'),
|
||||
operator_created: TAPi18n.__('operator-created'),
|
||||
operator_modified: TAPi18n.__('operator-modified'),
|
||||
operator_status: TAPi18n.__('operator-status'),
|
||||
operator_has: TAPi18n.__('operator-has'),
|
||||
operator_sort: TAPi18n.__('operator-sort'),
|
||||
operator_limit: TAPi18n.__('operator-limit'),
|
||||
operator_debug: TAPi18n.__('operator-debug'),
|
||||
operator_org: TAPi18n.__('operator-org'),
|
||||
operator_team: TAPi18n.__('operator-team'),
|
||||
predicate_overdue: TAPi18n.__('predicate-overdue'),
|
||||
predicate_archived: TAPi18n.__('predicate-archived'),
|
||||
predicate_all: TAPi18n.__('predicate-all'),
|
||||
predicate_ended: TAPi18n.__('predicate-ended'),
|
||||
predicate_week: TAPi18n.__('predicate-week'),
|
||||
predicate_month: TAPi18n.__('predicate-month'),
|
||||
predicate_quarter: TAPi18n.__('predicate-quarter'),
|
||||
predicate_year: TAPi18n.__('predicate-year'),
|
||||
predicate_attachment: TAPi18n.__('predicate-attachment'),
|
||||
predicate_description: TAPi18n.__('predicate-description'),
|
||||
predicate_checklist: TAPi18n.__('predicate-checklist'),
|
||||
predicate_public: TAPi18n.__('predicate-public'),
|
||||
predicate_private: TAPi18n.__('predicate-private'),
|
||||
predicate_due: TAPi18n.__('predicate-due'),
|
||||
predicate_created: TAPi18n.__('predicate-created'),
|
||||
predicate_modified: TAPi18n.__('predicate-modified'),
|
||||
predicate_start: TAPi18n.__('predicate-start'),
|
||||
predicate_end: TAPi18n.__('predicate-end'),
|
||||
predicate_assignee: TAPi18n.__('predicate-assignee'),
|
||||
predicate_member: TAPi18n.__('predicate-member'),
|
||||
predicate_selector: TAPi18n.__('predicate-selector'),
|
||||
predicate_projection: TAPi18n.__('predicate-projection'),
|
||||
};
|
||||
|
||||
let text = '';
|
||||
[
|
||||
['# ', 'globalSearch-instructions-heading'],
|
||||
['\n', 'globalSearch-instructions-description'],
|
||||
['\n\n', 'globalSearch-instructions-operators'],
|
||||
['\n- ', 'globalSearch-instructions-operator-board'],
|
||||
['\n- ', 'globalSearch-instructions-operator-list'],
|
||||
['\n- ', 'globalSearch-instructions-operator-swimlane'],
|
||||
['\n- ', 'globalSearch-instructions-operator-comment'],
|
||||
['\n- ', 'globalSearch-instructions-operator-label'],
|
||||
['\n- ', 'globalSearch-instructions-operator-hash'],
|
||||
['\n- ', 'globalSearch-instructions-operator-user'],
|
||||
['\n- ', 'globalSearch-instructions-operator-at'],
|
||||
['\n- ', 'globalSearch-instructions-operator-member'],
|
||||
['\n- ', 'globalSearch-instructions-operator-assignee'],
|
||||
['\n- ', 'globalSearch-instructions-operator-creator'],
|
||||
['\n- ', 'globalSearch-instructions-operator-org'],
|
||||
['\n- ', 'globalSearch-instructions-operator-team'],
|
||||
['\n- ', 'globalSearch-instructions-operator-due'],
|
||||
['\n- ', 'globalSearch-instructions-operator-created'],
|
||||
['\n- ', 'globalSearch-instructions-operator-modified'],
|
||||
['\n- ', 'globalSearch-instructions-operator-status'],
|
||||
['\n - ', 'globalSearch-instructions-status-archived'],
|
||||
['\n - ', 'globalSearch-instructions-status-public'],
|
||||
['\n - ', 'globalSearch-instructions-status-private'],
|
||||
['\n - ', 'globalSearch-instructions-status-all'],
|
||||
['\n - ', 'globalSearch-instructions-status-ended'],
|
||||
['\n- ', 'globalSearch-instructions-operator-has'],
|
||||
['\n- ', 'globalSearch-instructions-operator-sort'],
|
||||
['\n- ', 'globalSearch-instructions-operator-limit'],
|
||||
['\n## ', 'heading-notes'],
|
||||
['\n- ', 'globalSearch-instructions-notes-1'],
|
||||
['\n- ', 'globalSearch-instructions-notes-2'],
|
||||
['\n- ', 'globalSearch-instructions-notes-3'],
|
||||
['\n- ', 'globalSearch-instructions-notes-3-2'],
|
||||
['\n- ', 'globalSearch-instructions-notes-4'],
|
||||
['\n- ', 'globalSearch-instructions-notes-5'],
|
||||
].forEach(([prefix, instruction]) => {
|
||||
text += `${prefix}${TAPi18n.__(instruction, tags)}`
|
||||
// Replace *<text>* with `<text>` so markdown shows correctly
|
||||
.replace(/\*\</, '`<')
|
||||
.replace(/\>\*/, '\>\`');
|
||||
});
|
||||
return text;
|
||||
} catch (e) {
|
||||
console.error('Error building searchInstructions:', e);
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
labelColors() {
|
||||
return Boards.simpleSchema()._schema['labels.$.color'].allowedValues.map(
|
||||
color => {
|
||||
return { color, name: TAPi18n.__(`color-${color}`) };
|
||||
},
|
||||
);
|
||||
try {
|
||||
if (!LABEL_COLORS) return [];
|
||||
return LABEL_COLORS.map(
|
||||
color => {
|
||||
return { color, name: TAPi18n.__(`color-${color}`) };
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Error building labelColors:', e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.globalSearch.events({
|
||||
'click input.global-search-query-input'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
},
|
||||
'focus input.global-search-query-input'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
},
|
||||
'keyup input.global-search-query-input'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
// Store the query value
|
||||
tpl.search.query.set(evt.target.value);
|
||||
},
|
||||
'keydown input.global-search-query-input'(evt, tpl) {
|
||||
// Allow normal key behavior but capture Enter key
|
||||
if (evt.key === 'Enter') {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
searchAllBoards(tpl, evt.target.value);
|
||||
}
|
||||
},
|
||||
'submit .js-search-query-form'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
searchAllBoards(tpl, evt.target.searchQuery.value);
|
||||
@@ -269,30 +325,6 @@ Template.globalSearch.events({
|
||||
);
|
||||
document.getElementById('global-search-input').focus();
|
||||
},
|
||||
'click .js-copy-debug-selector'(evt) {
|
||||
/* Get the text field */
|
||||
const selector = document.getElementById("debug-selector");
|
||||
|
||||
try {
|
||||
navigator.clipboard.writeText(selector.textContent);
|
||||
alert("Selector copied to clipboard");
|
||||
} catch(err) {
|
||||
alert("Error copying text: " + err);
|
||||
}
|
||||
|
||||
},
|
||||
'click .js-copy-debug-projection'(evt) {
|
||||
/* Get the text field */
|
||||
const projection = document.getElementById("debug-projection");
|
||||
|
||||
try {
|
||||
navigator.clipboard.writeText(projection.textContent);
|
||||
alert("Projection copied to clipboard");
|
||||
} catch(err) {
|
||||
alert("Error copying text: " + err);
|
||||
}
|
||||
|
||||
},
|
||||
'click .js-board-title'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const input = document.getElementById('global-search-input');
|
||||
@@ -340,47 +372,46 @@ Template.globalSearch.events({
|
||||
},
|
||||
});
|
||||
|
||||
// resultsPaged template helpers - this template is used by globalSearch, brokenCards, and brokenCardsReport.
|
||||
// It receives the parent's data context (which includes reactive vars) via +resultsPaged(this).
|
||||
// The jade accesses resultsHeading.get, results.get, getSearchHref, hasPreviousPage.get, hasNextPage.get
|
||||
// directly on the data context, so we don't need helpers for those when the parent passes
|
||||
// the right data context. However, we need to ensure the helpers exist for the template.
|
||||
// resultsPaged template helpers - receives search data context from parent template.
|
||||
Template.resultsPaged.helpers({
|
||||
resultsHeading() {
|
||||
const data = Template.currentData();
|
||||
if (data && data.resultsHeading) return data.resultsHeading;
|
||||
// fallback: check parent template
|
||||
const parentTpl = Template.instance().view.parentView.templateInstance && Template.instance().view.parentView.templateInstance();
|
||||
if (parentTpl && parentTpl.search) return parentTpl.search.resultsHeading;
|
||||
return new ReactiveVar('');
|
||||
if (data && data.resultsHeading) {
|
||||
const rv = data.resultsHeading;
|
||||
return rv && typeof rv.get === 'function' ? rv.get() : rv;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
results() {
|
||||
const data = Template.currentData();
|
||||
if (data && data.results) return data.results;
|
||||
const parentTpl = Template.instance().view.parentView.templateInstance && Template.instance().view.parentView.templateInstance();
|
||||
if (parentTpl && parentTpl.search) return parentTpl.search.results;
|
||||
return new ReactiveVar([]);
|
||||
if (data && data.results) {
|
||||
const rv = data.results;
|
||||
return rv && typeof rv.get === 'function' ? rv.get() : rv;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
getSearchHref() {
|
||||
const data = Template.currentData();
|
||||
if (data && data.getSearchHref) return data.getSearchHref();
|
||||
const parentTpl = Template.instance().view.parentView.templateInstance && Template.instance().view.parentView.templateInstance();
|
||||
if (parentTpl && parentTpl.search) return parentTpl.search.getSearchHref();
|
||||
if (data && typeof data.getSearchHref === 'function') {
|
||||
return data.getSearchHref();
|
||||
}
|
||||
return '';
|
||||
},
|
||||
hasPreviousPage() {
|
||||
const data = Template.currentData();
|
||||
if (data && data.hasPreviousPage) return data.hasPreviousPage;
|
||||
const parentTpl = Template.instance().view.parentView.templateInstance && Template.instance().view.parentView.templateInstance();
|
||||
if (parentTpl && parentTpl.search) return parentTpl.search.hasPreviousPage;
|
||||
return new ReactiveVar(false);
|
||||
if (data && data.hasPreviousPage) {
|
||||
const rv = data.hasPreviousPage;
|
||||
return rv && typeof rv.get === 'function' ? rv.get() : rv;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
hasNextPage() {
|
||||
const data = Template.currentData();
|
||||
if (data && data.hasNextPage) return data.hasNextPage;
|
||||
const parentTpl = Template.instance().view.parentView.templateInstance && Template.instance().view.parentView.templateInstance();
|
||||
if (parentTpl && parentTpl.search) return parentTpl.search.hasNextPage;
|
||||
return new ReactiveVar(false);
|
||||
if (data && data.hasNextPage) {
|
||||
const rv = data.hasNextPage;
|
||||
return rv && typeof rv.get === 'function' ? rv.get() : rv;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -177,24 +177,25 @@
|
||||
}
|
||||
#header-quick-access ul.header-quick-access-list {
|
||||
transition: opacity 0.2s;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding: 10px;
|
||||
margin: -10px;
|
||||
margin: 10px;
|
||||
flex: 1; /* Take up available space */
|
||||
min-width: 0; /* Allow shrinking below content size */
|
||||
display: flex; /* Use flexbox for better control */
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
scrollbar-color: white #2573a7;
|
||||
}
|
||||
|
||||
/* Hide scrollbar completely */
|
||||
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar {
|
||||
display: none;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#header-quick-access ul.header-quick-access-list {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: thin; /* IE and Edge */
|
||||
scrollbar-width: thin; /* Firefox */
|
||||
}
|
||||
#header-quick-access ul.header-quick-access-list li {
|
||||
display: inline-block; /* Keep inline-block for proper spacing */
|
||||
|
||||
@@ -68,12 +68,8 @@ template(name="header")
|
||||
+viewer
|
||||
= title
|
||||
else
|
||||
li.current.empty(title="{{_ 'quick-access-description'}}")
|
||||
li.current(title="{{_ 'quick-access-description'}}")
|
||||
| {{_ 'quick-access-description'}}
|
||||
#header-new-board-icon
|
||||
// Next line is used only for spacing at header,
|
||||
// there is no visible clickable icon.
|
||||
#header-new-board-icon
|
||||
//
|
||||
Hide duplicate create board button,
|
||||
//
|
||||
@@ -126,5 +122,9 @@ template(name="offlineWarning")
|
||||
p
|
||||
i.fa.fa-warning
|
||||
| {{_ 'app-is-offline'}}
|
||||
|
||||
a.app-try-reconnect {{_ 'app-try-reconnect'}}
|
||||
if appOfflineMessage
|
||||
br
|
||||
span= appOfflineMessage
|
||||
if canReconnectNow
|
||||
br
|
||||
a.app-try-reconnect {{_ 'app-try-reconnect'}}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Session } from 'meteor/session';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import Announcements from '/models/announcements';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
Meteor.subscribe('user-admin');
|
||||
Meteor.subscribe('boards');
|
||||
@@ -43,7 +47,27 @@ Template.header.helpers({
|
||||
},
|
||||
|
||||
appIsOffline() {
|
||||
return !Meteor.status().connected;
|
||||
const status = Meteor.status();
|
||||
return ['waiting', 'failed', 'offline'].includes(status.status);
|
||||
},
|
||||
|
||||
appOfflineMessage() {
|
||||
const status = Meteor.status();
|
||||
|
||||
if (status.status === 'failed' && status.reason) {
|
||||
return status.reason;
|
||||
}
|
||||
|
||||
if (status.status === 'offline') {
|
||||
return 'Connection is paused.';
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
canReconnectNow() {
|
||||
const status = Meteor.status();
|
||||
return ['waiting', 'failed', 'offline'].includes(status.status);
|
||||
},
|
||||
|
||||
hasAnnouncement() {
|
||||
@@ -132,7 +156,7 @@ Template.header.events({
|
||||
Session.set('currentCard', null);
|
||||
},
|
||||
'click .js-toggle-desktop-drag-handles'() {
|
||||
currentUser = Meteor.user();
|
||||
const currentUser = Meteor.user();
|
||||
if (currentUser) {
|
||||
Meteor.call('toggleDesktopDragHandles');
|
||||
} else if (window.localStorage.getItem('showDesktopDragHandles')) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import Users from '/models/users';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
|
||||
let alreadyCheck = 1;
|
||||
let isCheckDone = false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CardSearchPaged } from '../../lib/cardSearch';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
Template.myCardsHeaderBar.helpers({
|
||||
myCardsSort() {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
border-radius: 0.4vw;
|
||||
border: 1px solid #dbdbdb;
|
||||
border-bottom-color: #c2c2c2;
|
||||
box-shadow: 0 0.2vh 0.8vh rgba(0,0,0,0.3);
|
||||
box-shadow: 0 0.2vh 0.8vh rgba(0, 0, 0, 0.3);
|
||||
position: absolute;
|
||||
/* Wider default to fit full color palette */
|
||||
width: min(380px, 55vw);
|
||||
@@ -15,10 +15,10 @@
|
||||
}
|
||||
.pop-over p,
|
||||
.pop-over textarea,
|
||||
.pop-over input[type="text"],
|
||||
.pop-over input[type="email"],
|
||||
.pop-over input[type="password"],
|
||||
.pop-over input[type="file"] {
|
||||
.pop-over input[type='text'],
|
||||
.pop-over input[type='email'],
|
||||
.pop-over input[type='password'],
|
||||
.pop-over input[type='file'] {
|
||||
width: 100%;
|
||||
}
|
||||
.pop-over select {
|
||||
@@ -78,9 +78,32 @@
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Allow dynamic max-height to override default constraint */
|
||||
.pop-over[style*="max-height"] .content-wrapper {
|
||||
max-height: inherit;
|
||||
/* Bounded height: flex column + clip so inline max-height cannot spill past the shell.
|
||||
Class pop-over--bounded matches popup.tpl.jade when max-height is set (more reliable
|
||||
than attribute substring matching on the style attribute alone). */
|
||||
.pop-over.pop-over--bounded,
|
||||
.pop-over[style*='max-height'] {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pop-over.pop-over--bounded .header,
|
||||
.pop-over[style*='max-height'] .header {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.pop-over.pop-over--bounded .content-wrapper,
|
||||
.pop-over[style*='max-height'] .content-wrapper {
|
||||
/* 0% basis: use remaining space under header, scroll inside (not past white bg) */
|
||||
flex: 1 1 0%;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: inherit;
|
||||
}
|
||||
.pop-over .content-container {
|
||||
width: 100%;
|
||||
@@ -89,17 +112,18 @@
|
||||
}
|
||||
|
||||
/* Allow dynamic max-height to override default constraint for content-container */
|
||||
.pop-over[style*="max-height"] .content-container {
|
||||
max-height: inherit;
|
||||
.pop-over.pop-over--bounded .content-container,
|
||||
.pop-over[style*='max-height'] .content-container {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
/* Fix overflow in the Member Settings (member menu) popup:
|
||||
the popup itself gets a max-height inline style, but the header consumes space.
|
||||
Make the header overlay the scrollable area so the list can't spill out. */
|
||||
.pop-over[data-popup="memberMenuPopup"] {
|
||||
.pop-over[data-popup='memberMenuPopup'] {
|
||||
overflow: hidden;
|
||||
}
|
||||
.pop-over[data-popup="memberMenuPopup"] > .header {
|
||||
.pop-over[data-popup='memberMenuPopup'] > .header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -107,30 +131,30 @@
|
||||
margin-bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.pop-over[data-popup="memberMenuPopup"] > .content-wrapper {
|
||||
.pop-over[data-popup='memberMenuPopup'] > .content-wrapper {
|
||||
padding-top: calc(4.5vh + 1vh);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Admin edit popups: use full height */
|
||||
.pop-over[data-popup="editUserPopup"],
|
||||
.pop-over[data-popup="editOrgPopup"],
|
||||
.pop-over[data-popup="editTeamPopup"] {
|
||||
.pop-over[data-popup='editUserPopup'],
|
||||
.pop-over[data-popup='editOrgPopup'],
|
||||
.pop-over[data-popup='editTeamPopup'] {
|
||||
height: calc(100vh - 20px) !important;
|
||||
max-height: calc(100vh - 20px) !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editUserPopup"] .content-wrapper,
|
||||
.pop-over[data-popup="editOrgPopup"] .content-wrapper,
|
||||
.pop-over[data-popup="editTeamPopup"] .content-wrapper {
|
||||
.pop-over[data-popup='editUserPopup'] .content-wrapper,
|
||||
.pop-over[data-popup='editOrgPopup'] .content-wrapper,
|
||||
.pop-over[data-popup='editTeamPopup'] .content-wrapper {
|
||||
max-height: calc(100vh - 80px) !important; /* Subtract header height */
|
||||
height: calc(100vh - 80px) !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editUserPopup"] .content-container,
|
||||
.pop-over[data-popup="editOrgPopup"] .content-container,
|
||||
.pop-over[data-popup="editTeamPopup"] .content-container {
|
||||
.pop-over[data-popup='editUserPopup'] .content-container,
|
||||
.pop-over[data-popup='editOrgPopup'] .content-container,
|
||||
.pop-over[data-popup='editTeamPopup'] .content-container {
|
||||
max-height: calc(100vh - 80px) !important; /* Subtract header height */
|
||||
height: calc(100vh - 80px) !important;
|
||||
}
|
||||
@@ -142,7 +166,7 @@
|
||||
}
|
||||
|
||||
/* Specific styling for language popup list */
|
||||
.pop-over[data-popup="changeLanguagePopup"] .pop-over-list {
|
||||
.pop-over[data-popup='changeLanguagePopup'] .pop-over-list {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
height: auto;
|
||||
@@ -150,7 +174,7 @@
|
||||
}
|
||||
|
||||
/* Ensure content div in language popup contains all items */
|
||||
.pop-over[data-popup="changeLanguagePopup"] .content {
|
||||
.pop-over[data-popup='changeLanguagePopup'] .content {
|
||||
height: auto;
|
||||
/* Remove forced min-height to avoid top gap */
|
||||
display: flex;
|
||||
@@ -158,7 +182,7 @@
|
||||
}
|
||||
|
||||
/* Ensure hidden stack pages truly take no space */
|
||||
.pop-over[data-popup="changeLanguagePopup"] .content.no-height {
|
||||
.pop-over[data-popup='changeLanguagePopup'] .content.no-height {
|
||||
min-height: 0 !important;
|
||||
height: 0 !important;
|
||||
padding: 0 !important;
|
||||
@@ -167,7 +191,7 @@
|
||||
}
|
||||
|
||||
/* Make language popup extend to bottom of browser window */
|
||||
.pop-over[data-popup="changeLanguagePopup"] {
|
||||
.pop-over[data-popup='changeLanguagePopup'] {
|
||||
position: fixed !important;
|
||||
bottom: 0 !important;
|
||||
top: auto !important;
|
||||
@@ -184,12 +208,12 @@
|
||||
}
|
||||
|
||||
/* Allow dynamic height for Change Language popup */
|
||||
.pop-over[data-popup="changeLanguagePopup"] .header {
|
||||
.pop-over[data-popup='changeLanguagePopup'] .header {
|
||||
flex-shrink: 0 !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="changeLanguagePopup"] .content-wrapper {
|
||||
.pop-over[data-popup='changeLanguagePopup'] .content-wrapper {
|
||||
flex: 1 !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
@@ -199,7 +223,7 @@
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="changeLanguagePopup"] .content-container {
|
||||
.pop-over[data-popup='changeLanguagePopup'] .content-container {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
flex: 1 !important;
|
||||
@@ -208,7 +232,7 @@
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="changeLanguagePopup"] .content {
|
||||
.pop-over[data-popup='changeLanguagePopup'] .content {
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
padding-bottom: 50px !important;
|
||||
@@ -216,59 +240,59 @@
|
||||
}
|
||||
|
||||
/* Date popup sizing for native HTML inputs */
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"],
|
||||
.pop-over[data-popup="editCardStartDatePopup"],
|
||||
.pop-over[data-popup="editCardDueDatePopup"],
|
||||
.pop-over[data-popup="editCardEndDatePopup"],
|
||||
.pop-over[data-popup*="Date"] {
|
||||
.pop-over[data-popup='editCardReceivedDatePopup'],
|
||||
.pop-over[data-popup='editCardStartDatePopup'],
|
||||
.pop-over[data-popup='editCardDueDatePopup'],
|
||||
.pop-over[data-popup='editCardEndDatePopup'],
|
||||
.pop-over[data-popup*='Date'] {
|
||||
width: min(400px, 90vw) !important; /* Smaller width for native inputs */
|
||||
min-width: 350px !important;
|
||||
max-height: 80vh !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .content-wrapper,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .content-wrapper,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .content-wrapper,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .content-wrapper,
|
||||
.pop-over[data-popup*="Date"] .content-wrapper {
|
||||
.pop-over[data-popup='editCardReceivedDatePopup'] .content-wrapper,
|
||||
.pop-over[data-popup='editCardStartDatePopup'] .content-wrapper,
|
||||
.pop-over[data-popup='editCardDueDatePopup'] .content-wrapper,
|
||||
.pop-over[data-popup='editCardEndDatePopup'] .content-wrapper,
|
||||
.pop-over[data-popup*='Date'] .content-wrapper {
|
||||
max-height: 60vh !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .content-container,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .content-container,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .content-container,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .content-container,
|
||||
.pop-over[data-popup*="Date"] .content-container {
|
||||
.pop-over[data-popup='editCardReceivedDatePopup'] .content-container,
|
||||
.pop-over[data-popup='editCardStartDatePopup'] .content-container,
|
||||
.pop-over[data-popup='editCardDueDatePopup'] .content-container,
|
||||
.pop-over[data-popup='editCardEndDatePopup'] .content-container,
|
||||
.pop-over[data-popup*='Date'] .content-container {
|
||||
max-height: 60vh !important;
|
||||
}
|
||||
|
||||
/* Native HTML input styling */
|
||||
.pop-over[data-popup*="Date"] .datepicker-container {
|
||||
.pop-over[data-popup*='Date'] .datepicker-container {
|
||||
width: 100% !important;
|
||||
padding: 15px !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup*="Date"] .datepicker-container .fields {
|
||||
.pop-over[data-popup*='Date'] .datepicker-container .fields {
|
||||
display: flex !important;
|
||||
gap: 15px !important;
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup*="Date"] .datepicker-container .fields .left,
|
||||
.pop-over[data-popup*="Date"] .datepicker-container .fields .right {
|
||||
.pop-over[data-popup*='Date'] .datepicker-container .fields .left,
|
||||
.pop-over[data-popup*='Date'] .datepicker-container .fields .right {
|
||||
flex: 1 !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup*="Date"] .datepicker-container label {
|
||||
.pop-over[data-popup*='Date'] .datepicker-container label {
|
||||
display: block !important;
|
||||
margin-bottom: 5px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup*="Date"] .datepicker-container input[type="date"],
|
||||
.pop-over[data-popup*="Date"] .datepicker-container input[type="time"] {
|
||||
.pop-over[data-popup*='Date'] .datepicker-container input[type='date'],
|
||||
.pop-over[data-popup*='Date'] .datepicker-container input[type='time'] {
|
||||
width: 100% !important;
|
||||
padding: 8px !important;
|
||||
border: 1px solid #ccc !important;
|
||||
@@ -277,71 +301,109 @@
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup*="Date"] .datepicker-container input[type="date"]:focus,
|
||||
.pop-over[data-popup*="Date"] .datepicker-container input[type="time"]:focus {
|
||||
.pop-over[data-popup*='Date'] .datepicker-container input[type='date']:focus,
|
||||
.pop-over[data-popup*='Date'] .datepicker-container input[type='time']:focus {
|
||||
outline: none !important;
|
||||
border-color: #007cba !important;
|
||||
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2) !important;
|
||||
}
|
||||
|
||||
/* Ensure date popup buttons stay within popup boundaries */
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .content,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .content,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .content,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .content,
|
||||
.pop-over[data-popup*="Date"] .content {
|
||||
.pop-over[data-popup='editCardReceivedDatePopup'] .content,
|
||||
.pop-over[data-popup='editCardStartDatePopup'] .content,
|
||||
.pop-over[data-popup='editCardDueDatePopup'] .content,
|
||||
.pop-over[data-popup='editCardEndDatePopup'] .content,
|
||||
.pop-over[data-popup*='Date'] .content {
|
||||
max-height: 60vh !important; /* Leave space for buttons */
|
||||
overflow-y: auto !important;
|
||||
padding-bottom: 100px !important; /* More space for buttons */
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .datepicker-container,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .datepicker-container,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .datepicker-container,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .datepicker-container,
|
||||
.pop-over[data-popup*="Date"] .datepicker-container {
|
||||
/* Workspace Settings popup: ensure modal doesn't overflow viewport */
|
||||
.pop-over[data-popup='workspaceActionsPopup'] {
|
||||
max-height: 80vh !important;
|
||||
width: min(420px, 85vw) !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup='workspaceActionsPopup'] .content-wrapper {
|
||||
max-height: calc(80vh - 70px) !important; /* Subtract header height */
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup='workspaceActionsPopup'] .content-container {
|
||||
max-height: calc(80vh - 70px) !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup='workspaceActionsPopup'] .content {
|
||||
max-height: none !important; /* Allow content to scroll within container */
|
||||
overflow: visible !important; /* Overflow handled by parent */
|
||||
}
|
||||
|
||||
/* Card Details Actions popup: ensure menu doesn't overflow viewport */
|
||||
.pop-over[data-popup="cardDetailsActionsPopup"] {
|
||||
max-height: 80vh !important;
|
||||
width: min(380px, 85vw) !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="cardDetailsActionsPopup"] .content-wrapper {
|
||||
max-height: calc(80vh - 70px) !important; /* Subtract header height */
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="cardDetailsActionsPopup"] .content-container {
|
||||
max-height: calc(80vh - 70px) !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="cardDetailsActionsPopup"] .content {
|
||||
max-height: none !important; /* Allow content to scroll within container */
|
||||
overflow: visible !important; /* Overflow handled by parent */
|
||||
}
|
||||
|
||||
.pop-over[data-popup='editCardReceivedDatePopup'] .datepicker-container,
|
||||
.pop-over[data-popup='editCardStartDatePopup'] .datepicker-container,
|
||||
.pop-over[data-popup='editCardDueDatePopup'] .datepicker-container,
|
||||
.pop-over[data-popup='editCardEndDatePopup'] .datepicker-container,
|
||||
.pop-over[data-popup*='Date'] .datepicker-container {
|
||||
max-height: 50vh !important; /* Limit calendar height */
|
||||
overflow-y: auto !important;
|
||||
margin-bottom: 20px !important; /* Space before buttons */
|
||||
}
|
||||
|
||||
/* Ensure buttons are properly positioned */
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .edit-date,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .edit-date,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .edit-date,
|
||||
.pop-over[data-popup*="Date"] .edit-date {
|
||||
.pop-over[data-popup='editCardReceivedDatePopup'] .edit-date,
|
||||
.pop-over[data-popup='editCardStartDatePopup'] .edit-date,
|
||||
.pop-over[data-popup='editCardDueDatePopup'] .edit-date,
|
||||
.pop-over[data-popup='editCardEndDatePopup'] .edit-date,
|
||||
.pop-over[data-popup*='Date'] .edit-date {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date .fields,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .edit-date .fields,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .edit-date .fields,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .edit-date .fields,
|
||||
.pop-over[data-popup*="Date"] .edit-date .fields {
|
||||
.pop-over[data-popup='editCardReceivedDatePopup'] .edit-date .fields,
|
||||
.pop-over[data-popup='editCardStartDatePopup'] .edit-date .fields,
|
||||
.pop-over[data-popup='editCardDueDatePopup'] .edit-date .fields,
|
||||
.pop-over[data-popup='editCardEndDatePopup'] .edit-date .fields,
|
||||
.pop-over[data-popup*='Date'] .edit-date .fields {
|
||||
flex-shrink: 0 !important;
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date .js-datepicker,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .edit-date .js-datepicker,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .edit-date .js-datepicker,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .edit-date .js-datepicker,
|
||||
.pop-over[data-popup*="Date"] .edit-date .js-datepicker {
|
||||
.pop-over[data-popup='editCardReceivedDatePopup'] .edit-date .js-datepicker,
|
||||
.pop-over[data-popup='editCardStartDatePopup'] .edit-date .js-datepicker,
|
||||
.pop-over[data-popup='editCardDueDatePopup'] .edit-date .js-datepicker,
|
||||
.pop-over[data-popup='editCardEndDatePopup'] .edit-date .js-datepicker,
|
||||
.pop-over[data-popup*='Date'] .edit-date .js-datepicker {
|
||||
flex: 1 !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date button,
|
||||
.pop-over[data-popup="editCardStartDatePopup"] .edit-date button,
|
||||
.pop-over[data-popup="editCardDueDatePopup"] .edit-date button,
|
||||
.pop-over[data-popup="editCardEndDatePopup"] .edit-date button,
|
||||
.pop-over[data-popup*="Date"] .edit-date button {
|
||||
.pop-over[data-popup='editCardReceivedDatePopup'] .edit-date button,
|
||||
.pop-over[data-popup='editCardStartDatePopup'] .edit-date button,
|
||||
.pop-over[data-popup='editCardDueDatePopup'] .edit-date button,
|
||||
.pop-over[data-popup='editCardEndDatePopup'] .edit-date button,
|
||||
.pop-over[data-popup*='Date'] .edit-date button {
|
||||
flex-shrink: 0 !important;
|
||||
margin-top: 15px !important;
|
||||
position: relative !important;
|
||||
@@ -350,7 +412,7 @@
|
||||
.pop-over .content-container .content {
|
||||
/* Match wider popover, leave padding */
|
||||
width: 100%;
|
||||
padding: 0 1.3vw 1.3vh;
|
||||
padding: 1.3vh 1.3vw 1.3vh;
|
||||
box-sizing: border-box;
|
||||
/* Ensure content is not shifted left */
|
||||
margin-left: 0 !important;
|
||||
@@ -387,8 +449,6 @@
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Fix popup depth containers - don't block transforms which are needed for popup navigation */
|
||||
.pop-over .popup-container-depth-1,
|
||||
.pop-over .popup-container-depth-2,
|
||||
@@ -477,15 +537,15 @@
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
line-height: 33px;
|
||||
display:flex;
|
||||
/* flex-wrap:wrap;*/
|
||||
gap:5px;
|
||||
display: flex;
|
||||
/* flex-wrap:wrap;*/
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
color: #000 !important;
|
||||
}
|
||||
.pop-over-list li > a > .member{
|
||||
.pop-over-list li > a > .member {
|
||||
align-self: flex-start;
|
||||
flex:0 0 auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.pop-over-list li > a .item-name {
|
||||
display: block;
|
||||
@@ -657,10 +717,10 @@ body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
|
||||
}
|
||||
.pop-over .content-container .content p,
|
||||
.pop-over .content-container .content textarea,
|
||||
.pop-over .content-container .content input[type="text"],
|
||||
.pop-over .content-container .content input[type="email"],
|
||||
.pop-over .content-container .content input[type="password"],
|
||||
.pop-over .content-container .content input[type="file"] {
|
||||
.pop-over .content-container .content input[type='text'],
|
||||
.pop-over .content-container .content input[type='email'],
|
||||
.pop-over .content-container .content input[type='password'],
|
||||
.pop-over .content-container .content input[type='file'] {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -707,3 +767,60 @@ body.mobile-mode .pop-over .content-wrapper {
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FIX: Popup overflow (Issues #6200, #6221, #6223)
|
||||
Prevents menus from overflowing outside viewport
|
||||
============================================ */
|
||||
|
||||
/* Specific fix for list actions menu */
|
||||
.pop-over[data-popup="listActionsPopup"] {
|
||||
max-height: 85vh !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="listActionsPopup"] .content-wrapper {
|
||||
max-height: calc(85vh - 50px) !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
/* Prevent any popup from overflowing viewport */
|
||||
.pop-over {
|
||||
max-height: 90vh !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.pop-over .content-wrapper {
|
||||
flex: 1 1 auto !important;
|
||||
overflow-y: auto !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
.pop-over .content-container {
|
||||
flex: 1 1 auto !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
.pop-over .content {
|
||||
flex: 1 1 auto !important;
|
||||
overflow-y: auto !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
/* Keep header fixed while content scrolls */
|
||||
.pop-over .header {
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
/* Mobile-specific adjustments */
|
||||
@media screen and (max-width: 800px) {
|
||||
.pop-over[data-popup="listActionsPopup"] {
|
||||
max-height: 100vh !important;
|
||||
}
|
||||
|
||||
.pop-over[data-popup="listActionsPopup"] .content-wrapper {
|
||||
max-height: calc(100vh - 48px) !important;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { CSSEvents } from '/client/lib/cssEvents';
|
||||
|
||||
Popup.template.events({
|
||||
'click .js-back-view'() {
|
||||
Popup.back();
|
||||
@@ -26,8 +28,11 @@ Popup.template.events({
|
||||
// When a popup content is removed (ie, when the user press the "back" button),
|
||||
// we need to wait for the container translation to end before removing the
|
||||
// actual DOM element. For that purpose we use the undocumented `_uihooks` API.
|
||||
Popup.template.onRendered(() => {
|
||||
Popup.template.onRendered(function () {
|
||||
const container = this.find('.content-container');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
container._uihooks = {
|
||||
removeElement(node) {
|
||||
$(node).addClass('no-height');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
class="{{#unless title}}miniprofile{{/unless}}"
|
||||
class=currentBoard.colorClass
|
||||
class="{{#unless title}}no-title{{/unless}}"
|
||||
class="{{#if offset.maxHeight}}pop-over--bounded{{/if}}"
|
||||
data-popup="{{popupName}}"
|
||||
style="left:{{offset.left}}px; top:{{offset.top}}px;{{#if offset.maxHeight}} max-height:{{offset.maxHeight}}px;{{/if}}")
|
||||
.header
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
|
||||
import Users from '/models/users';
|
||||
|
||||
Template.notification.events({
|
||||
'click .read-status .materialCheckBox'() {
|
||||
async 'click .read-status .materialCheckBox'() {
|
||||
const update = {};
|
||||
const newReadValue = this.read ? null : Date.now();
|
||||
const newReadValue = this.read ? null : new Date();
|
||||
update[`profile.notifications.${this.index}.read`] = newReadValue;
|
||||
|
||||
Users.update(Meteor.userId(), { $set: update }, (error, result) => {
|
||||
await Users.updateAsync(Meteor.userId(), { $set: update }).catch((error) => {
|
||||
if (error) {
|
||||
console.error('Error updating notification:', error);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { toggleNotificationsDrawer } from './notifications.js';
|
||||
import Users from '/models/users';
|
||||
|
||||
Template.notificationsDrawer.onCreated(function() {
|
||||
Meteor.subscribe('notificationActivities');
|
||||
@@ -34,7 +36,7 @@ Template.notificationsDrawer.events({
|
||||
event.stopPropagation();
|
||||
Session.set('showNotificationMenu', !Session.get('showNotificationMenu'));
|
||||
},
|
||||
'click .notification-menu .menu-item'(event) {
|
||||
async 'click .notification-menu .menu-item'(event) {
|
||||
const target = event.currentTarget;
|
||||
|
||||
if (target.classList.contains('mark-all-read')) {
|
||||
@@ -42,8 +44,10 @@ Template.notificationsDrawer.events({
|
||||
for (const index in notifications) {
|
||||
if (notifications.hasOwnProperty(index) && !notifications[index].read) {
|
||||
const update = {};
|
||||
update[`profile.notifications.${index}.read`] = Date.now();
|
||||
Users.update(Meteor.userId(), { $set: update });
|
||||
update[`profile.notifications.${index}.read`] = new Date();
|
||||
await Users.updateAsync(Meteor.userId(), { $set: update }).catch((error) => {
|
||||
console.error('Error marking notification as read:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
Session.set('showNotificationMenu', false);
|
||||
@@ -53,7 +57,9 @@ Template.notificationsDrawer.events({
|
||||
if (notifications.hasOwnProperty(index) && notifications[index].read) {
|
||||
const update = {};
|
||||
update[`profile.notifications.${index}.read`] = null;
|
||||
Users.update(Meteor.userId(), { $set: update });
|
||||
await Users.updateAsync(Meteor.userId(), { $set: update }).catch((error) => {
|
||||
console.error('Error marking notification as unread:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
Session.set('showNotificationMenu', false);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import Actions from '/models/actions';
|
||||
import Rules from '/models/rules';
|
||||
import Triggers from '/models/triggers';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
Template.boardActions.onCreated(function () {
|
||||
this.subscribe('boards');
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import Actions from '/models/actions';
|
||||
import { CARD_COLORS } from '/models/metadata/colors';
|
||||
import Rules from '/models/rules';
|
||||
import Triggers from '/models/triggers';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
let cardColors;
|
||||
Meteor.startup(() => {
|
||||
cardColors = Cards.simpleSchema()._schema.color.allowedValues;
|
||||
cardColors = CARD_COLORS;
|
||||
});
|
||||
|
||||
// Module-level shared state so the color popup can read/write the
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import Actions from '/models/actions';
|
||||
import Rules from '/models/rules';
|
||||
import Triggers from '/models/triggers';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
Template.checklistActions.onCreated(function () {
|
||||
this.subscribe('allRules');
|
||||
});
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import Actions from '/models/actions';
|
||||
import Rules from '/models/rules';
|
||||
import Triggers from '/models/triggers';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
Template.mailActions.events({
|
||||
'click .js-mail-action'(event, tpl) {
|
||||
const emailTo = tpl.find('#email-to').value;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import Actions from '/models/actions';
|
||||
import Rules from '/models/rules';
|
||||
import Triggers from '/models/triggers';
|
||||
|
||||
Template.rulesMain.onCreated(function () {
|
||||
this.rulesCurrentTab = new ReactiveVar('rulesList');
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
Template.boardTriggers.onCreated(function () {
|
||||
this.provaVar = new ReactiveVar('');
|
||||
this.currentPopupTriggerId = 'def';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
Template.cardTriggers.onCreated(function () {
|
||||
this.subscribe('allRules');
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
Template.checklistTriggers.onCreated(function () {
|
||||
this.subscribe('allRules');
|
||||
});
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { AttachmentStorage } from '/models/attachments';
|
||||
import Attachments, { AttachmentStorage } from '/models/attachments';
|
||||
import { CardSearchPaged } from '/client/lib/cardSearch';
|
||||
import SessionData from '/models/usersessiondata';
|
||||
import { QueryParams } from '/config/query-classes';
|
||||
import { OPERATOR_LIMIT } from '/config/search-const';
|
||||
const filesize = require('filesize');
|
||||
import Boards from '/models/boards';
|
||||
import Cards from '/models/cards';
|
||||
import Rules from '/models/rules';
|
||||
const { filesize } = require('filesize');
|
||||
|
||||
// --- Shared helper functions (formerly AdminReport base class methods) ---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { groupBy } from '/imports/lib/collectionHelpers';
|
||||
import Attachments, { fileStoreStrategyFactory } from '/models/attachments';
|
||||
const filesize = require('filesize');
|
||||
import Attachments from '/models/attachments';
|
||||
const { filesize } = require('filesize');
|
||||
|
||||
Template.attachments.onCreated(function () {
|
||||
this.subscription = null;
|
||||
@@ -62,7 +62,10 @@ Template.moveAttachments.helpers({
|
||||
_attachment.flatVersion = Object.keys(_attachment.versions)
|
||||
.map(_versionName => {
|
||||
const _version = Object.assign(_attachment.versions[_versionName], {"versionName": _versionName});
|
||||
_version.storageName = fileStoreStrategyFactory.getFileStrategy(_attachment, _versionName).getStorageName();
|
||||
// Read storage directly from the document (set by onAfterUpload on server)
|
||||
_version.storageName = _version.storage || (
|
||||
(_attachment.meta?.source === 'import' || _version.meta?.gridFsFileId) ? 'gridfs' : 'fs'
|
||||
);
|
||||
return _version;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ template(name='statistics')
|
||||
td {{statistics.mongo.mongoStorageEngine}}
|
||||
tr
|
||||
th {{_ 'MongoDB_Oplog_enabled'}}
|
||||
td {{statistics.mongo.mongoOplogEnabled}}
|
||||
td {{formatBoolean statistics.mongo.mongoOplogEnabled}}
|
||||
tr
|
||||
th {{_ 'OS_Type'}}
|
||||
td {{statistics.os.type}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
const filesize = require('filesize');
|
||||
const { filesize } = require('filesize');
|
||||
|
||||
Template.statistics.onCreated(function () {
|
||||
this.info = new ReactiveVar({});
|
||||
@@ -47,4 +47,8 @@ Template.statistics.helpers({
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
formatBoolean(value) {
|
||||
return value ? TAPi18n.__('yes') : TAPi18n.__('no');
|
||||
},
|
||||
});
|
||||
@@ -311,3 +311,46 @@ table td:first-child {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Avatar styling - removed people-avatar column */
|
||||
|
||||
/* Username column styling - displays avatar inline with username */
|
||||
.username a.js-edit-people-avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.username a.js-edit-people-avatar:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.username .avatar {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.username .avatar.avatar-initials {
|
||||
background-color: #dbdbdb;
|
||||
color: #444;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.username .avatar img.avatar-image {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -114,7 +114,7 @@ template(name="orgGeneral")
|
||||
th {{_ 'shortName'}}
|
||||
th {{_ 'website'}}
|
||||
th {{_ 'createdAt'}}
|
||||
th {{_ 'active'}}
|
||||
th {{_ 'active-org'}}
|
||||
tbody
|
||||
tr
|
||||
each org in orgList
|
||||
@@ -137,7 +137,7 @@ template(name="teamGeneral")
|
||||
th {{_ 'shortName'}}
|
||||
th {{_ 'website'}}
|
||||
th {{_ 'createdAt'}}
|
||||
th {{_ 'active'}}
|
||||
th {{_ 'active-team'}}
|
||||
tbody
|
||||
tr
|
||||
each team in teamList
|
||||
@@ -160,7 +160,7 @@ template(name="peopleGeneral")
|
||||
th {{_ 'username'}}
|
||||
th {{_ 'email'}}
|
||||
th {{_ 'admin'}}
|
||||
th {{_ 'admin-people-active-status'}}
|
||||
th {{_ 'active-person'}}
|
||||
th {{_ 'accounts-lockout-status'}}
|
||||
th {{_ 'createdAt'}}
|
||||
th
|
||||
@@ -260,17 +260,38 @@ template(name="teamRow")
|
||||
template(name="peopleRow")
|
||||
tr
|
||||
td
|
||||
a.edit-user
|
||||
a.edit-user(data-user-id=userData._id)
|
||||
i.fa.fa-pencil-square-o
|
||||
| {{_ 'edit'}}
|
||||
a.more-settings-user
|
||||
a.more-settings-user(data-user-id=userData._id)
|
||||
| ⋯
|
||||
if userData.loginDisabled
|
||||
td.username <s>{{ userData.username }}</s>
|
||||
td.username
|
||||
a.js-edit-people-avatar(data-user-id=userData._id)
|
||||
if hasAvatarUrl
|
||||
.avatar
|
||||
img.avatar-image(src="{{userData.profile.avatarUrl}}", alt="{{userData.username}}", title="{{_ 'change-avatar'}}")
|
||||
else
|
||||
+userAvatarInitials(userId=userData._id)
|
||||
<s>{{ userData.username }}</s>
|
||||
else if isUserLocked
|
||||
td.username {{ userData.username }}
|
||||
td.username
|
||||
a.js-edit-people-avatar(data-user-id=userData._id)
|
||||
if hasAvatarUrl
|
||||
.avatar
|
||||
img.avatar-image(src="{{userData.profile.avatarUrl}}", alt="{{userData.username}}", title="{{_ 'change-avatar'}}")
|
||||
else
|
||||
+userAvatarInitials(userId=userData._id)
|
||||
| {{ userData.username }}
|
||||
else
|
||||
td.username {{ userData.username }}
|
||||
td.username
|
||||
a.js-edit-people-avatar(data-user-id=userData._id)
|
||||
if hasAvatarUrl
|
||||
.avatar
|
||||
img.avatar-image(src="{{userData.profile.avatarUrl}}", alt="{{userData.username}}", title="{{_ 'change-avatar'}}")
|
||||
else
|
||||
+userAvatarInitials(userId=userData._id)
|
||||
| {{ userData.username }}
|
||||
if userData.loginDisabled
|
||||
td <s>{{ userData.emails.[0].address }}</s>
|
||||
else
|
||||
@@ -333,7 +354,7 @@ template(name="editOrgPopup")
|
||||
| {{_ 'website'}}
|
||||
input.js-orgWebsite(type="text" value=org.orgWebsite)
|
||||
label
|
||||
| {{_ 'active'}}
|
||||
| {{_ 'active-org'}}
|
||||
select.select-active.js-org-isactive
|
||||
option(value="false") {{_ 'no'}}
|
||||
option(value="true" selected="{{org.orgIsActive}}") {{_ 'yes'}}
|
||||
@@ -359,7 +380,7 @@ template(name="editTeamPopup")
|
||||
| {{_ 'website'}}
|
||||
input.js-teamWebsite(type="text" value=team.teamWebsite)
|
||||
label
|
||||
| {{_ 'active'}}
|
||||
| {{_ 'active-team'}}
|
||||
select.select-active.js-team-isactive
|
||||
option(value="false") {{_ 'no'}}
|
||||
option(value="true" selected="{{team.teamIsActive}}") {{_ 'yes'}}
|
||||
@@ -406,7 +427,7 @@ template(name="editUserPopup")
|
||||
option(value="false") {{_ 'no'}}
|
||||
option(value="true" selected="{{userData.emails.[0].verified}}") {{_ 'yes'}}
|
||||
label
|
||||
| {{_ 'active'}}
|
||||
| {{_ 'active-person'}}
|
||||
select.select-active.js-profile-isactive
|
||||
option(value="false") {{_ 'yes'}}
|
||||
option(value="true" selected="{{user.loginDisabled}}") {{_ 'no'}}
|
||||
@@ -469,7 +490,7 @@ template(name="newOrgPopup")
|
||||
| {{_ 'website'}}
|
||||
input.js-orgWebsite(type="text" value="" required)
|
||||
label
|
||||
| {{_ 'active'}}
|
||||
| {{_ 'active-org'}}
|
||||
select.select-active.js-org-isactive
|
||||
option(value="false" selected="selected") {{_ 'no'}}
|
||||
option(value="true") {{_ 'yes'}}
|
||||
@@ -493,7 +514,7 @@ template(name="newTeamPopup")
|
||||
| {{_ 'website'}}
|
||||
input.js-teamWebsite(type="text" value="")
|
||||
label
|
||||
| {{_ 'active'}}
|
||||
| {{_ 'active-team'}}
|
||||
select.select-active.js-team-isactive
|
||||
option(value="false" selected="selected") {{_ 'no'}}
|
||||
option(value="true") {{_ 'yes'}}
|
||||
@@ -555,7 +576,7 @@ template(name="newUserPopup")
|
||||
option(value="false" selected="selected") {{_ 'no'}}
|
||||
option(value="true") {{_ 'yes'}}
|
||||
label
|
||||
| {{_ 'active'}}
|
||||
| {{_ 'active-person'}}
|
||||
select.select-active.js-profile-isactive
|
||||
option(value="false" selected="selected") {{_ 'yes'}}
|
||||
option(value="true") {{_ 'no'}}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { avatarUpdateCounter } from '/client/components/users/avatarUpdateCounter';
|
||||
import { InfiniteScrolling } from '/client/lib/infiniteScrolling';
|
||||
import LockoutSettings from '/models/lockoutSettings';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import Org from '/models/org';
|
||||
import Team from '/models/team';
|
||||
import Users from '/models/users';
|
||||
|
||||
const orgsPerPage = 25;
|
||||
const teamsPerPage = 25;
|
||||
@@ -533,7 +537,39 @@ Template.teamRow.helpers({
|
||||
|
||||
Template.peopleRow.helpers({
|
||||
userData() {
|
||||
return this.user || ReactiveCache.getUser(this.userId);
|
||||
// Depend on global avatar update counter to reactively update when avatars change
|
||||
avatarUpdateCounter.get();
|
||||
// Get the user ID from either this._id or this.user._id
|
||||
let userId;
|
||||
if (this.user && this.user._id) {
|
||||
userId = this.user._id;
|
||||
} else if (this._id) {
|
||||
userId = this._id;
|
||||
}
|
||||
// Always fetch from ReactiveCache to ensure latest data
|
||||
if (userId) {
|
||||
return ReactiveCache.getUser(userId);
|
||||
}
|
||||
return this.user || this;
|
||||
},
|
||||
hasAvatarUrl() {
|
||||
// Depend on global avatar update counter to reactively update when avatars change
|
||||
avatarUpdateCounter.get();
|
||||
// Get the user ID from either this._id or this.user._id
|
||||
let userId;
|
||||
if (this.user && this.user._id) {
|
||||
userId = this.user._id;
|
||||
} else if (this._id) {
|
||||
userId = this._id;
|
||||
}
|
||||
let user;
|
||||
if (userId) {
|
||||
user = ReactiveCache.getUser(userId);
|
||||
} else {
|
||||
user = this.user || this;
|
||||
}
|
||||
if (!user || !user.profile) return false;
|
||||
return !!user.profile.avatarUrl;
|
||||
},
|
||||
isUserLocked() {
|
||||
const user = this.user || ReactiveCache.getUser(this.userId);
|
||||
@@ -723,8 +759,20 @@ Template.teamRow.events({
|
||||
});
|
||||
|
||||
Template.peopleRow.events({
|
||||
'click a.edit-user': Popup.open('editUser'),
|
||||
'click a.more-settings-user': Popup.open('settingsUser'),
|
||||
'click a.edit-user'(event) {
|
||||
// Get the user ID from the data attribute
|
||||
const userId = event.currentTarget.getAttribute('data-user-id');
|
||||
if (userId) {
|
||||
Popup.open('editUser').call({ userId: userId }, event);
|
||||
}
|
||||
},
|
||||
'click a.more-settings-user'(event) {
|
||||
// Get the user ID from the data attribute
|
||||
const userId = event.currentTarget.getAttribute('data-user-id');
|
||||
if (userId) {
|
||||
Popup.open('settingsUser').call({ userId: userId }, event);
|
||||
}
|
||||
},
|
||||
'click .selectUserChkBox': function(ev){
|
||||
if(ev.currentTarget){
|
||||
if(ev.currentTarget.checked){
|
||||
@@ -788,6 +836,18 @@ Template.peopleRow.events({
|
||||
// For now, we'll leave this as a no-op
|
||||
}
|
||||
},
|
||||
'click a.js-edit-people-avatar'(event) {
|
||||
// Extract the user ID from the data attribute
|
||||
const userId = event.currentTarget.getAttribute('data-user-id');
|
||||
if (userId) {
|
||||
// Get the user from cache to pass correct context
|
||||
const user = ReactiveCache.getUser(userId);
|
||||
if (user) {
|
||||
// Call Popup.open with the correct user data context
|
||||
Popup.open('adminChangeAvatar').call({ _id: userId, user: user }, event);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.modifyTeamsUsers.helpers({
|
||||
@@ -1180,7 +1240,7 @@ Template.editUserPopup.events({
|
||||
},
|
||||
});
|
||||
|
||||
UpdateUserOrgsOrTeamsElement = function(isNewUser = false){
|
||||
const UpdateUserOrgsOrTeamsElement = function(isNewUser = false){
|
||||
let selectedElt;
|
||||
let selectedEltValue;
|
||||
let selectedEltValueId;
|
||||
@@ -1459,19 +1519,23 @@ Template.settingsTeamPopup.events({
|
||||
Template.settingsUserPopup.events({
|
||||
'click .impersonate-user'(event) {
|
||||
event.preventDefault();
|
||||
const userId = this.userId || this.user?._id;
|
||||
|
||||
Meteor.call('impersonate', this.userId, (err) => {
|
||||
Meteor.call('impersonate', userId, (err) => {
|
||||
if (!err) {
|
||||
// Meteor.connection.setUserId() triggers automatic cache invalidation
|
||||
// No need to manually invalidate - let Meteor handle the user data refresh
|
||||
Meteor.connection.setUserId(userId);
|
||||
FlowRouter.go('/');
|
||||
Meteor.connection.setUserId(this.userId);
|
||||
}
|
||||
});
|
||||
},
|
||||
'click #deleteButton'(event) {
|
||||
event.preventDefault();
|
||||
const userId = this.userId || this.user?._id;
|
||||
|
||||
// Use secure server method instead of direct client-side removal
|
||||
Meteor.call('removeUser', this.userId, (error, result) => {
|
||||
Meteor.call('removeUser', userId, (error, result) => {
|
||||
if (error) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.error('Error removing user:', error);
|
||||
@@ -1498,19 +1562,24 @@ Template.settingsUserPopup.events({
|
||||
|
||||
Template.settingsUserPopup.helpers({
|
||||
user() {
|
||||
return ReactiveCache.getUser(this.userId);
|
||||
const userId = this.userId || this.user?._id;
|
||||
return ReactiveCache.getUser(userId);
|
||||
},
|
||||
authentications() {
|
||||
return Template.instance().authenticationMethods.get();
|
||||
},
|
||||
isSelected(match) {
|
||||
const userId = Template.instance().data.userId;
|
||||
const selected = ReactiveCache.getUser(userId).authenticationMethod;
|
||||
const userId = Template.instance().data.userId || Template.instance().data.user?._id;
|
||||
const user = ReactiveCache.getUser(userId);
|
||||
if (!user) return false;
|
||||
const selected = user.authenticationMethod;
|
||||
return selected === match;
|
||||
},
|
||||
isLdap() {
|
||||
const userId = Template.instance().data.userId;
|
||||
const selected = ReactiveCache.getUser(userId).authenticationMethod;
|
||||
const userId = Template.instance().data.userId || Template.instance().data.user?._id;
|
||||
const user = ReactiveCache.getUser(userId);
|
||||
if (!user) return false;
|
||||
const selected = user.authenticationMethod;
|
||||
return selected === 'ldap';
|
||||
},
|
||||
errorMessage() {
|
||||
|
||||
@@ -2,6 +2,11 @@ import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { ALLOWED_WAIT_SPINNERS } from '/config/const';
|
||||
import LockoutSettings from '/models/lockoutSettings';
|
||||
import AccessibilitySettings from '/models/accessibilitySettings';
|
||||
import AccountSettings from '/models/accountSettings';
|
||||
import Announcements from '/models/announcements';
|
||||
import Settings from '/models/settings';
|
||||
import TableVisibilityModeSettings from '/models/tableVisibilityModeSettings';
|
||||
// import {
|
||||
// cronMigrationProgress,
|
||||
// cronMigrationStatus,
|
||||
@@ -492,7 +497,7 @@ Template.setting.events({
|
||||
});
|
||||
const validEmails = [];
|
||||
emails.forEach((email) => {
|
||||
if (email && SimpleSchema.RegEx.Email.test(email.trim())) {
|
||||
if (email && /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(email.trim())) {
|
||||
validEmails.push(email.trim());
|
||||
}
|
||||
});
|
||||
@@ -1083,25 +1088,44 @@ Template.announcementSettings.helpers({
|
||||
});
|
||||
|
||||
Template.announcementSettings.events({
|
||||
'click a.js-toggle-activemessage'(event, tpl) {
|
||||
async 'click a.js-toggle-activemessage'(event, tpl) {
|
||||
event.preventDefault();
|
||||
tpl.loading.set(true);
|
||||
const announcements = Announcements.findOne();
|
||||
if (!announcements) {
|
||||
tpl.loading.set(false);
|
||||
return;
|
||||
}
|
||||
const isActive = announcements.enabled;
|
||||
Announcements.update(announcements._id, {
|
||||
$set: { enabled: !isActive },
|
||||
});
|
||||
tpl.loading.set(false);
|
||||
if (isActive) {
|
||||
$('.admin-announcement').slideUp();
|
||||
} else {
|
||||
$('.admin-announcement').slideDown();
|
||||
try {
|
||||
await Announcements.updateAsync(announcements._id, {
|
||||
$set: { enabled: !isActive },
|
||||
});
|
||||
if (isActive) {
|
||||
$('.admin-announcement').slideUp();
|
||||
} else {
|
||||
$('.admin-announcement').slideDown();
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error?.reason || error?.message || 'Failed to update announcement setting');
|
||||
} finally {
|
||||
tpl.loading.set(false);
|
||||
}
|
||||
},
|
||||
'click button.js-announcement-save'() {
|
||||
async 'click button.js-announcement-save'(event) {
|
||||
event.preventDefault();
|
||||
const message = $('#admin-announcement').val().trim();
|
||||
Announcements.update(Announcements.findOne()._id, {
|
||||
$set: { body: message },
|
||||
});
|
||||
const announcement = Announcements.findOne();
|
||||
if (!announcement) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await Announcements.updateAsync(announcement._id, {
|
||||
$set: { body: message },
|
||||
});
|
||||
} catch (error) {
|
||||
alert(error?.reason || error?.message || 'Failed to save announcement');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -177,8 +177,8 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.js-field-has-receiveddate(title="{{_ 'card-received'}}" class="{{#if allowsReceivedDate}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsReceivedDate}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-receiveddate(title="{{_ 'card-received'}}" class="{{#if allowsReceivedDate}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsReceivedDate}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
a.flex.js-field-has-receiveddate-on-minicard(title="{{_ 'card-received'}}" class="{{#if allowsReceivedDateOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsReceivedDateOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-sign-in
|
||||
@@ -188,8 +188,8 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.js-field-has-startdate(title="{{_ 'card-start'}}" class="{{#if allowsStartDate}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsStartDate}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-startdate(title="{{_ 'card-start'}}" class="{{#if allowsStartDate}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsStartDate}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
a.flex.js-field-has-startdate-on-minicard(title="{{_ 'card-start'}}" class="{{#if allowsStartDateOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsStartDateOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-hourglass
|
||||
@@ -199,8 +199,8 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.js-field-has-duedate(title="{{_ 'card-due'}}" class="{{#if allowsDueDate}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsDueDate}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-duedate(title="{{_ 'card-due'}}" class="{{#if allowsDueDate}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsDueDate}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
a.flex.js-field-has-duedate-on-minicard(title="{{_ 'card-due'}}" class="{{#if allowsDueDateOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsDueDateOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-sign-in
|
||||
@@ -210,8 +210,8 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.js-field-has-enddate(title="{{_ 'card-end'}}" class="{{#if allowsEndDate}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsEndDate}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-enddate(title="{{_ 'card-end'}}" class="{{#if allowsEndDate}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsEndDate}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
a.flex.js-field-has-enddate-on-minicard(title="{{_ 'card-end'}}" class="{{#if allowsEndDateOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsEndDateOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-clock-o
|
||||
@@ -221,8 +221,8 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.js-field-has-members(title="{{_ 'members'}}" class="{{#if allowsMembers}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsMembers}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-members(title="{{_ 'members'}}" class="{{#if allowsMembers}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsMembers}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
a.flex.js-field-has-members-on-minicard(title="{{_ 'members'}}" class="{{#if allowsMembersOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsMembersOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-users
|
||||
@@ -252,8 +252,8 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.js-field-has-assignee(title="{{_ 'assignee'}}" class="{{#if allowsAssignee}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsAssignee}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-assignee(title="{{_ 'assignee'}}" class="{{#if allowsAssignee}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsAssignee}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
a.flex.js-field-has-assignee-on-minicard(title="{{_ 'assignee'}}" class="{{#if allowsAssigneeOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsAssigneeOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-user
|
||||
@@ -263,8 +263,8 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.js-field-has-assigned-by(title="{{_ 'assigned-by'}}" class="{{#if allowsAssignedBy}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsAssignedBy}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-assigned-by(title="{{_ 'assigned-by'}}" class="{{#if allowsAssignedBy}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsAssignedBy}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
a.flex.js-field-has-assigned-by-on-minicard(title="{{_ 'assigned-by'}}" class="{{#if allowsAssignedByOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsAssignedByOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-shopping-cart
|
||||
@@ -274,8 +274,8 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.js-field-has-requested-by(title="{{_ 'requested-by'}}" class="{{#if allowsRequestedBy}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsRequestedBy}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-requested-by(title="{{_ 'requested-by'}}" class="{{#if allowsRequestedBy}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsRequestedBy}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
a.flex.js-field-has-requested-by-on-minicard(title="{{_ 'requested-by'}}" class="{{#if allowsRequestedByOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsRequestedByOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-user
|
||||
@@ -316,8 +316,8 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.js-field-has-labels(title="{{_ 'labels'}}" class="{{#if allowsLabels}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsLabels}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-labels(title="{{_ 'labels'}}" class="{{#if allowsLabels}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsLabels}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
a.flex.js-field-has-labels-on-minicard(title="{{_ 'labels'}}" class="{{#if allowsLabelsOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsLabelsOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-tag
|
||||
@@ -337,8 +337,8 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.js-field-has-card-number(title="{{_ 'card'}} {{_ 'number'}}" class="{{#if allowsCardNumber}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsCardNumber}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-card-number(title="{{_ 'card'}} {{_ 'number'}}" class="{{#if allowsCardNumber}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsCardNumber}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
a.flex.js-field-has-card-number-on-minicard(title="{{_ 'card'}} {{_ 'number'}}" class="{{#if allowsCardNumberOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsCardNumberOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
| #️⃣
|
||||
@@ -349,8 +349,8 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.js-field-has-description-title(title="{{_ 'description'}} {{_ 'title'}}" class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsDescriptionTitle}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-description-title(title="{{_ 'description'}} {{_ 'title'}}" class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsDescriptionTitle}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
a.flex.js-field-has-description-title-on-minicard(title="{{_ 'description'}} {{_ 'title'}}" class="{{#if allowsDescriptionTitleOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsDescriptionTitleOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-file-text-o
|
||||
@@ -361,30 +361,20 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.js-field-has-description-text(title="{{_ 'description'}} {{_ 'custom-field-text'}}" class="{{#if allowsDescriptionText}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsDescriptionText}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-description-text(title="{{_ 'description'}} {{_ 'custom-field-text'}}" class="{{#if allowsDescriptionText}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsDescriptionText}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
a.flex.js-field-has-description-text-on-minicard(title="{{_ 'description'}} {{_ 'custom-field-text'}}" class="{{#if allowsDescriptionTextOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsDescriptionTextOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-file-text-o
|
||||
| {{_ 'description'}}
|
||||
| {{_ 'custom-field-text'}}
|
||||
.card-settings-row
|
||||
.card-settings-column
|
||||
span
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-description-text-on-minicard(title="{{_ 'description-on-minicard'}}" class="{{#if allowsDescriptionTextOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsDescriptionTextOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-file-text-o
|
||||
| {{_ 'description-on-minicard'}}
|
||||
.card-settings-row
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-checklists(title="{{_ 'checklists'}}" class="{{#if allowsChecklists}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsChecklists}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-checklists(title="{{_ 'checklists'}}" class="{{#if allowsChecklists}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsChecklists}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
a.flex.js-field-has-checklists-on-minicard(title="{{_ 'checklists'}}" class="{{#if allowsChecklistsOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsChecklistsOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-check
|
||||
@@ -394,8 +384,8 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.js-field-has-subtasks(title="{{_ 'subtasks'}}" class="{{#if allowsSubtasks}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsSubtasks}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-subtasks(title="{{_ 'subtasks'}}" class="{{#if allowsSubtasks}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsSubtasks}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
a.flex.js-field-has-subtasks-on-minicard(title="{{_ 'subtasks'}}" class="{{#if allowsSubtasksOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsSubtasksOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-globe
|
||||
@@ -405,8 +395,8 @@ template(name="boardCardSettingsPopup")
|
||||
a.flex.js-field-has-attachments(title="{{_ 'attachments'}}" class="{{#if allowsAttachments}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsAttachments}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-attachments(title="{{_ 'attachments'}}" class="{{#if allowsAttachments}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsAttachments}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
a.flex.js-field-has-attachments-on-minicard(title="{{_ 'attachments'}}" class="{{#if allowsAttachmentsOnMinicard}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsAttachmentsOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-paperclip
|
||||
@@ -504,7 +494,7 @@ template(name="boardSubtaskSettingsPopup")
|
||||
else
|
||||
option(value=_id) {{title}}
|
||||
|
||||
template(name="chooseBoardSource")
|
||||
template(name="chooseBoardSourcePopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a(href="{{pathFor '/import/trello'}}") {{_ 'from-trello'}}
|
||||
@@ -528,8 +518,8 @@ template(name="deleteDuplicateListsPopup")
|
||||
template(name="outgoingWebhooksPopup")
|
||||
each integrations
|
||||
form.integration-form
|
||||
a.flex
|
||||
i.fa(class="{{#unless enabled}}fa-check{{else}}fa-square-o{{/unless}}")
|
||||
a.flex.js-toggle-webhook-enabled
|
||||
.materialCheckBox(class="{{#unless enabled}}is-checked{{/unless}}")
|
||||
span {{_ 'disable-webhook'}}
|
||||
input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" value=title)
|
||||
input.js-outgoing-webhooks-url(type="text" name="url" value=url)
|
||||
@@ -640,11 +630,11 @@ template(name="exportBoardPopup")
|
||||
li
|
||||
a(href="{{exportCsvUrl}}", download="{{exportCsvFilename}}")
|
||||
i.fa.fa-upload
|
||||
| {{_ 'export-board-csv'}} ,
|
||||
| {{_ 'export-board-csv-comma'}}
|
||||
li
|
||||
a(href="{{exportScsvUrl}}", download="{{exportCsvFilename}}")
|
||||
i.fa.fa-upload
|
||||
| {{_ 'export-board-csv'}} ;
|
||||
| {{_ 'export-board-csv-semicolon'}}
|
||||
li
|
||||
a(href="{{exportTsvUrl}}", download="{{exportTsvFilename}}")
|
||||
i.fa.fa-upload
|
||||
|
||||
@@ -1,14 +1,40 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import { InfiniteScrolling } from '/client/lib/infiniteScrolling';
|
||||
import AccessibilitySettings from '/models/accessibilitySettings';
|
||||
import Boards from '/models/boards';
|
||||
import Integrations from '/models/integrations';
|
||||
import Lists from '/models/lists';
|
||||
import { BOARD_COLORS } from '/models/metadata/colors';
|
||||
import { Filter } from '/client/lib/filter';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import {
|
||||
clearSidebarInstance,
|
||||
setSidebarInstance,
|
||||
} from '/client/features/sidebar/service';
|
||||
|
||||
Sidebar = null;
|
||||
export let Sidebar = null;
|
||||
|
||||
const defaultView = 'home';
|
||||
const MCB = '.materialCheckBox';
|
||||
const CKCLS = 'is-checked';
|
||||
|
||||
function getMinicardSetting(board, onMinicardField, cardField, defaultValue) {
|
||||
if (!board) return false;
|
||||
if (board[onMinicardField] !== null && board[onMinicardField] !== undefined) {
|
||||
return board[onMinicardField];
|
||||
}
|
||||
if (cardField && board[cardField] !== null && board[cardField] !== undefined) {
|
||||
return board[cardField];
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const viewTitles = {
|
||||
filter: 'filter-cards',
|
||||
search: 'search-cards',
|
||||
@@ -25,6 +51,7 @@ Template.sidebar.onCreated(function() {
|
||||
this.infiniteScrolling = new InfiniteScrolling();
|
||||
this.activitiesInstance = null;
|
||||
Sidebar = this;
|
||||
setSidebarInstance(this);
|
||||
|
||||
// Subscribe to accessibility settings
|
||||
Meteor.subscribe('accessibilitySettings');
|
||||
@@ -106,6 +133,7 @@ Template.sidebar.onCreated(function() {
|
||||
});
|
||||
|
||||
Template.sidebar.onDestroyed(function() {
|
||||
clearSidebarInstance(this);
|
||||
Sidebar = null;
|
||||
});
|
||||
|
||||
@@ -153,7 +181,7 @@ Template.sidebar.events({
|
||||
tpl.setView();
|
||||
},
|
||||
'click .js-toggle-minicard-label-text'() {
|
||||
currentUser = ReactiveCache.getCurrentUser();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (currentUser) {
|
||||
Meteor.call('toggleMinicardLabelText');
|
||||
} else if (window.localStorage.getItem('hiddenMinicardLabelText')) {
|
||||
@@ -168,20 +196,27 @@ Template.sidebar.events({
|
||||
FlowRouter.go('shortcuts');
|
||||
},
|
||||
'click .js-keyboard-shortcuts-toggle'() {
|
||||
ReactiveCache.getCurrentUser().toggleKeyboardShortcuts();
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user) user.toggleKeyboardShortcuts();
|
||||
},
|
||||
'click .js-vertical-scrollbars-toggle'() {
|
||||
ReactiveCache.getCurrentUser().toggleVerticalScrollbars();
|
||||
},
|
||||
'click .js-show-week-of-year-toggle'() {
|
||||
ReactiveCache.getCurrentUser().toggleShowWeekOfYear();
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user) {
|
||||
user.toggleShowWeekOfYear();
|
||||
} else {
|
||||
const current = window.localStorage.getItem('showWeekOfYear') === 'true';
|
||||
window.localStorage.setItem('showWeekOfYear', String(!current));
|
||||
}
|
||||
},
|
||||
'click .sidebar-accessibility'() {
|
||||
'click .sidebar-accessibility'(event, tpl) {
|
||||
FlowRouter.go('accessibility');
|
||||
Sidebar.toggle();
|
||||
tpl.toggle();
|
||||
},
|
||||
'click .js-close-sidebar'() {
|
||||
Sidebar.toggle()
|
||||
'click .js-close-sidebar'(event, tpl) {
|
||||
tpl.toggle();
|
||||
},
|
||||
'scroll .js-board-sidebar-content'(event, tpl) {
|
||||
tpl.infiniteScrolling.checkScrollPosition(event.currentTarget, () => {
|
||||
@@ -194,7 +229,7 @@ Blaze.registerHelper('Sidebar', () => Sidebar);
|
||||
|
||||
Template.homeSidebar.helpers({
|
||||
hiddenMinicardLabelText() {
|
||||
currentUser = ReactiveCache.getCurrentUser();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (currentUser) {
|
||||
return (currentUser.profile || {}).hiddenMinicardLabelText;
|
||||
} else if (window.localStorage.getItem('hiddenMinicardLabelText')) {
|
||||
@@ -209,7 +244,8 @@ Template.homeSidebar.helpers({
|
||||
},
|
||||
isShowWeekOfYear() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
return user && user.isShowWeekOfYear();
|
||||
if (!user) return window.localStorage.getItem('showWeekOfYear') === 'true';
|
||||
return user.isShowWeekOfYear();
|
||||
},
|
||||
showActivities() {
|
||||
let ret = Utils.getCurrentBoard().showActivities ?? false;
|
||||
@@ -218,8 +254,8 @@ Template.homeSidebar.helpers({
|
||||
});
|
||||
|
||||
Template.homeSidebar.events({
|
||||
'click .js-toggle-show-activities'() {
|
||||
Utils.getCurrentBoard().toggleShowActivities();
|
||||
async 'click .js-toggle-show-activities'() {
|
||||
await Utils.getCurrentBoard().toggleShowActivities();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -237,7 +273,9 @@ Template.boardInfoOnMyBoardsPopup.helpers({
|
||||
EscapeActions.register(
|
||||
'sidebarView',
|
||||
() => {
|
||||
Sidebar.setView(defaultView);
|
||||
if (Sidebar) {
|
||||
Sidebar.setView(defaultView);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
return Sidebar && Sidebar.getView() !== defaultView;
|
||||
@@ -296,11 +334,15 @@ Template.boardMenuPopup.events({
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-custom-fields'() {
|
||||
Sidebar.setView('customFields');
|
||||
if (Sidebar) {
|
||||
Sidebar.setView('customFields');
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-open-archives'() {
|
||||
Sidebar.setView('archives');
|
||||
if (Sidebar) {
|
||||
Sidebar.setView('archives');
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-change-board-color': Popup.open('boardChangeColor'),
|
||||
@@ -348,10 +390,12 @@ Template.boardMenuPopup.events({
|
||||
}),
|
||||
'click .js-archive-board ': Popup.afterConfirm('archiveBoard', async function() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
await currentBoard.archive();
|
||||
// XXX We should have some kind of notification on top of the page to
|
||||
// confirm that the board was successfully archived.
|
||||
FlowRouter.go('home');
|
||||
try {
|
||||
await Meteor.callAsync('archiveBoard', currentBoard._id);
|
||||
FlowRouter.go('home');
|
||||
} catch (err) {
|
||||
alert(err?.reason || err?.message || 'Failed to archive board');
|
||||
}
|
||||
}),
|
||||
'click .js-delete-board': Popup.afterConfirm('deleteBoard', function() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
@@ -544,10 +588,6 @@ Template.membersWidget.events({
|
||||
},
|
||||
});
|
||||
|
||||
Template.outgoingWebhooksPopup.onCreated(function() {
|
||||
this.disabled = new ReactiveVar(false);
|
||||
});
|
||||
|
||||
Template.outgoingWebhooksPopup.helpers({
|
||||
boardId() {
|
||||
return Session.get('currentBoard') || Integrations.Const.GLOBAL_WEBHOOK_ID;
|
||||
@@ -571,20 +611,23 @@ Template.outgoingWebhooksPopup.helpers({
|
||||
});
|
||||
|
||||
Template.outgoingWebhooksPopup.events({
|
||||
'click a.flex'(evt, tpl) {
|
||||
tpl.disabled.set(!tpl.disabled.get());
|
||||
$(evt.target).toggleClass(CKCLS, tpl.disabled.get());
|
||||
},
|
||||
submit(evt, tpl) {
|
||||
'click .js-toggle-webhook-enabled'(evt) {
|
||||
evt.preventDefault();
|
||||
const url = evt.target.url.value;
|
||||
$(evt.currentTarget).find(MCB).toggleClass(CKCLS);
|
||||
},
|
||||
async submit(evt) {
|
||||
evt.preventDefault();
|
||||
const url = evt.target.url.value.trim();
|
||||
const boardId = Session.get('currentBoard') || Integrations.Const.GLOBAL_WEBHOOK_ID;
|
||||
let id = null;
|
||||
let integration = null;
|
||||
const title = evt.target.title.value;
|
||||
const token = evt.target.token.value;
|
||||
const type = evt.target.type.value;
|
||||
const enabled = !tpl.disabled.get();
|
||||
const title = evt.target.title.value.trim();
|
||||
const token = evt.target.token.value.trim();
|
||||
const type = evt.target.type.value.trim();
|
||||
const enabled = !$(evt.target)
|
||||
.find('.js-toggle-webhook-enabled')
|
||||
.find(MCB)
|
||||
.hasClass(CKCLS);
|
||||
let remove = false;
|
||||
const values = {
|
||||
url,
|
||||
@@ -609,22 +652,27 @@ Template.outgoingWebhooksPopup.events({
|
||||
} else if (url) {
|
||||
integration = findIntegration({ url, token });
|
||||
}
|
||||
if (remove) {
|
||||
Integrations.remove(integration._id);
|
||||
} else if (integration && integration._id) {
|
||||
Integrations.update(integration._id, {
|
||||
$set: values,
|
||||
});
|
||||
} else if (url) {
|
||||
Integrations.insert({
|
||||
...values,
|
||||
userId: Meteor.userId(),
|
||||
enabled: true,
|
||||
boardId,
|
||||
activities: ['all'],
|
||||
});
|
||||
|
||||
try {
|
||||
if (remove && integration && integration._id) {
|
||||
await Integrations.removeAsync(integration._id);
|
||||
} else if (integration && integration._id) {
|
||||
await Integrations.updateAsync(integration._id, {
|
||||
$set: values,
|
||||
});
|
||||
} else if (url) {
|
||||
await Integrations.insertAsync({
|
||||
...values,
|
||||
userId: Meteor.userId(),
|
||||
enabled,
|
||||
boardId,
|
||||
activities: ['all'],
|
||||
});
|
||||
}
|
||||
Popup.back();
|
||||
} catch (error) {
|
||||
alert(error?.reason || error?.message || 'Failed to save webhook');
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -778,7 +826,7 @@ Template.labelsWidget.onRendered(draggableMembersLabelsWidgets);
|
||||
|
||||
Template.boardChangeColorPopup.helpers({
|
||||
backgroundColors() {
|
||||
return Boards.simpleSchema()._schema.color.allowedValues;
|
||||
return BOARD_COLORS;
|
||||
},
|
||||
isSelected() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
@@ -1002,26 +1050,51 @@ Template.boardCardSettingsPopup.helpers({
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsReceivedDate : false;
|
||||
},
|
||||
allowsReceivedDateOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return getMinicardSetting(currentBoard, 'allowsReceivedDateOnMinicard', 'allowsReceivedDate', true);
|
||||
},
|
||||
allowsStartDate() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsStartDate : false;
|
||||
},
|
||||
allowsStartDateOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return getMinicardSetting(currentBoard, 'allowsStartDateOnMinicard', 'allowsStartDate', true);
|
||||
},
|
||||
allowsDueDate() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsDueDate : false;
|
||||
},
|
||||
allowsDueDateOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return getMinicardSetting(currentBoard, 'allowsDueDateOnMinicard', 'allowsDueDate', true);
|
||||
},
|
||||
allowsEndDate() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsEndDate : false;
|
||||
},
|
||||
allowsEndDateOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return getMinicardSetting(currentBoard, 'allowsEndDateOnMinicard', 'allowsEndDate', true);
|
||||
},
|
||||
allowsSubtasks() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsSubtasks : false;
|
||||
},
|
||||
allowsSubtasksOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return getMinicardSetting(currentBoard, 'allowsSubtasksOnMinicard', 'allowsSubtasks', true);
|
||||
},
|
||||
allowsCreator() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
@@ -1030,28 +1103,48 @@ Template.boardCardSettingsPopup.helpers({
|
||||
allowsCreatorOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? (currentBoard.allowsCreatorOnMinicard ?? false) : false;
|
||||
return getMinicardSetting(currentBoard, 'allowsCreatorOnMinicard', 'allowsCreator', false);
|
||||
},
|
||||
allowsMembers() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsMembers : false;
|
||||
},
|
||||
allowsMembersOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return getMinicardSetting(currentBoard, 'allowsMembersOnMinicard', 'allowsMembers', true);
|
||||
},
|
||||
allowsAssignee() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsAssignee : false;
|
||||
},
|
||||
allowsAssigneeOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return getMinicardSetting(currentBoard, 'allowsAssigneeOnMinicard', 'allowsAssignee', true);
|
||||
},
|
||||
allowsAssignedBy() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsAssignedBy : false;
|
||||
},
|
||||
allowsAssignedByOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return getMinicardSetting(currentBoard, 'allowsAssignedByOnMinicard', 'allowsAssignedBy', true);
|
||||
},
|
||||
allowsRequestedBy() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsRequestedBy : false;
|
||||
},
|
||||
allowsRequestedByOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return getMinicardSetting(currentBoard, 'allowsRequestedByOnMinicard', 'allowsRequestedBy', true);
|
||||
},
|
||||
allowsCardSortingByNumber() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
@@ -1067,21 +1160,36 @@ Template.boardCardSettingsPopup.helpers({
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsLabels : false;
|
||||
},
|
||||
allowsLabelsOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return getMinicardSetting(currentBoard, 'allowsLabelsOnMinicard', 'allowsLabels', true);
|
||||
},
|
||||
allowsShowListsOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsShowListsOnMinicard : false;
|
||||
return getMinicardSetting(currentBoard, 'allowsShowListsOnMinicard', 'allowsShowLists', false);
|
||||
},
|
||||
allowsChecklists() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsChecklists : false;
|
||||
},
|
||||
allowsChecklistsOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return getMinicardSetting(currentBoard, 'allowsChecklistsOnMinicard', 'allowsChecklists', true);
|
||||
},
|
||||
allowsAttachments() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsAttachments : false;
|
||||
},
|
||||
allowsAttachmentsOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return getMinicardSetting(currentBoard, 'allowsAttachmentsOnMinicard', 'allowsAttachments', true);
|
||||
},
|
||||
allowsComments() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
@@ -1092,11 +1200,21 @@ Template.boardCardSettingsPopup.helpers({
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsCardNumber : false;
|
||||
},
|
||||
allowsCardNumberOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return getMinicardSetting(currentBoard, 'allowsCardNumberOnMinicard', 'allowsCardNumber', false);
|
||||
},
|
||||
allowsDescriptionTitle() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsDescriptionTitle : false;
|
||||
},
|
||||
allowsDescriptionTitleOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return getMinicardSetting(currentBoard, 'allowsDescriptionTitleOnMinicard', 'allowsDescriptionTitle', true);
|
||||
},
|
||||
allowsDescriptionText() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
@@ -1105,7 +1223,7 @@ Template.boardCardSettingsPopup.helpers({
|
||||
isBoardSelected() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.dateSettingsDefaultBoardID : false;
|
||||
return currentBoard ? currentBoard.dateSettingsDefaultBoardId : false;
|
||||
},
|
||||
isNullBoardSelected() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
@@ -1118,7 +1236,7 @@ Template.boardCardSettingsPopup.helpers({
|
||||
allowsDescriptionTextOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsDescriptionTextOnMinicard : false;
|
||||
return getMinicardSetting(currentBoard, 'allowsDescriptionTextOnMinicard', 'allowsDescriptionText', false);
|
||||
},
|
||||
allowsCoverAttachmentOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
@@ -1133,7 +1251,7 @@ Template.boardCardSettingsPopup.helpers({
|
||||
allowsCardSortingByNumberOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsCardSortingByNumberOnMinicard : false;
|
||||
return getMinicardSetting(currentBoard, 'allowsCardSortingByNumberOnMinicard', 'allowsCardSortingByNumber', false);
|
||||
},
|
||||
boards() {
|
||||
const ret = ReactiveCache.getBoards(
|
||||
@@ -1186,26 +1304,51 @@ Template.boardCardSettingsPopup.events({
|
||||
const newValue = !tpl.currentBoard.allowsReceivedDate;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsReceivedDate: newValue } });
|
||||
},
|
||||
'click .js-field-has-receiveddate-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsReceivedDateOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsReceivedDateOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-startdate'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsStartDate;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsStartDate: newValue } });
|
||||
},
|
||||
'click .js-field-has-startdate-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsStartDateOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsStartDateOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-enddate'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsEndDate;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsEndDate: newValue } });
|
||||
},
|
||||
'click .js-field-has-enddate-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsEndDateOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsEndDateOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-duedate'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsDueDate;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsDueDate: newValue } });
|
||||
},
|
||||
'click .js-field-has-duedate-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsDueDateOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsDueDateOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-subtasks'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsSubtasks;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsSubtasks: newValue } });
|
||||
},
|
||||
'click .js-field-has-subtasks-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsSubtasksOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsSubtasksOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-creator'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsCreator;
|
||||
@@ -1221,21 +1364,41 @@ Template.boardCardSettingsPopup.events({
|
||||
const newValue = !tpl.currentBoard.allowsMembers;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsMembers: newValue } });
|
||||
},
|
||||
'click .js-field-has-members-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsMembersOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsMembersOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-assignee'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsAssignee;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsAssignee: newValue } });
|
||||
},
|
||||
'click .js-field-has-assignee-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsAssigneeOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsAssigneeOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-assigned-by'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsAssignedBy;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsAssignedBy: newValue } });
|
||||
},
|
||||
'click .js-field-has-assigned-by-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsAssignedByOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsAssignedByOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-requested-by'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsRequestedBy;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsRequestedBy: newValue } });
|
||||
},
|
||||
'click .js-field-has-requested-by-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsRequestedByOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsRequestedByOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-card-sorting-by-number'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsCardSortingByNumber;
|
||||
@@ -1251,194 +1414,90 @@ Template.boardCardSettingsPopup.events({
|
||||
const newValue = !tpl.currentBoard.allowsLabels;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsLabels: newValue } });
|
||||
},
|
||||
'click .js-field-has-labels-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsLabelsOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsLabelsOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-card-show-lists-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.currentBoard.allowsShowListsOnMinicard = !tpl.currentBoard
|
||||
.allowsShowListsOnMinicard;
|
||||
tpl.currentBoard.setAllowsShowListsOnMinicard(
|
||||
tpl.currentBoard.allowsShowListsOnMinicard,
|
||||
);
|
||||
$(`.js-field-has-card-show-lists-on-minicard ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsShowListsOnMinicard,
|
||||
);
|
||||
$('.js-field-has-card-show-lists-on-minicard').toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsShowListsOnMinicard,
|
||||
);
|
||||
const newValue = !tpl.currentBoard.allowsShowListsOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsShowListsOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-description-title'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.currentBoard.allowsDescriptionTitle = !tpl.currentBoard
|
||||
.allowsDescriptionTitle;
|
||||
tpl.currentBoard.setAllowsDescriptionTitle(
|
||||
tpl.currentBoard.allowsDescriptionTitle,
|
||||
);
|
||||
$(`.js-field-has-description-title ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsDescriptionTitle,
|
||||
);
|
||||
$('.js-field-has-description-title').toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsDescriptionTitle,
|
||||
);
|
||||
const newValue = !tpl.currentBoard.allowsDescriptionTitle;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsDescriptionTitle: newValue } });
|
||||
},
|
||||
'click .js-field-has-description-title-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsDescriptionTitleOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsDescriptionTitleOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-card-number'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.currentBoard.allowsCardNumber = !tpl.currentBoard
|
||||
.allowsCardNumber;
|
||||
tpl.currentBoard.setAllowsCardNumber(
|
||||
tpl.currentBoard.allowsCardNumber,
|
||||
);
|
||||
$(`.js-field-has-card-number ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsCardNumber,
|
||||
);
|
||||
$('.js-field-has-card-number').toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsCardNumber,
|
||||
);
|
||||
const newValue = !tpl.currentBoard.allowsCardNumber;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsCardNumber: newValue } });
|
||||
},
|
||||
'click .js-field-has-card-number-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsCardNumberOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsCardNumberOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-description-text-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.currentBoard.allowsDescriptionTextOnMinicard = !tpl.currentBoard
|
||||
.allowsDescriptionTextOnMinicard;
|
||||
tpl.currentBoard.setallowsDescriptionTextOnMinicard(
|
||||
tpl.currentBoard.allowsDescriptionTextOnMinicard,
|
||||
);
|
||||
$(`.js-field-has-description-text-on-minicard ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsDescriptionTextOnMinicard,
|
||||
);
|
||||
$('.js-field-has-description-text-on-minicard').toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsDescriptionTextOnMinicard,
|
||||
);
|
||||
const newValue = !tpl.currentBoard.allowsDescriptionTextOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsDescriptionTextOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-description-text'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.currentBoard.allowsDescriptionText = !tpl.currentBoard
|
||||
.allowsDescriptionText;
|
||||
tpl.currentBoard.setAllowsDescriptionText(
|
||||
tpl.currentBoard.allowsDescriptionText,
|
||||
);
|
||||
$(`.js-field-has-description-text ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsDescriptionText,
|
||||
);
|
||||
$('.js-field-has-description-text').toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsDescriptionText,
|
||||
);
|
||||
const newValue = !tpl.currentBoard.allowsDescriptionText;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsDescriptionText: newValue } });
|
||||
},
|
||||
'click .js-field-has-checklists'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.currentBoard.allowsChecklists = !tpl.currentBoard
|
||||
.allowsChecklists;
|
||||
tpl.currentBoard.setAllowsChecklists(
|
||||
tpl.currentBoard.allowsChecklists,
|
||||
);
|
||||
$(`.js-field-has-checklists ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsChecklists,
|
||||
);
|
||||
$('.js-field-has-checklists').toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsChecklists,
|
||||
);
|
||||
const newValue = !tpl.currentBoard.allowsChecklists;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsChecklists: newValue } });
|
||||
},
|
||||
'click .js-field-has-checklists-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsChecklistsOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsChecklistsOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-attachments'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.currentBoard.allowsAttachments = !tpl.currentBoard
|
||||
.allowsAttachments;
|
||||
tpl.currentBoard.setAllowsAttachments(
|
||||
tpl.currentBoard.allowsAttachments,
|
||||
);
|
||||
$(`.js-field-has-attachments ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsAttachments,
|
||||
);
|
||||
$('.js-field-has-attachments').toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsAttachments,
|
||||
);
|
||||
const newValue = !tpl.currentBoard.allowsAttachments;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsAttachments: newValue } });
|
||||
},
|
||||
'click .js-field-has-attachments-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !tpl.currentBoard.allowsAttachmentsOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsAttachmentsOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-comments'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.currentBoard.allowsComments = !tpl.currentBoard.allowsComments;
|
||||
tpl.currentBoard.setAllowsComments(tpl.currentBoard.allowsComments);
|
||||
$(`.js-field-has-comments ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsComments,
|
||||
);
|
||||
$('.js-field-has-comments').toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsComments,
|
||||
);
|
||||
const newValue = !tpl.currentBoard.allowsComments;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsComments: newValue } });
|
||||
},
|
||||
'click .js-field-has-activities'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.currentBoard.allowsActivities = !tpl.currentBoard
|
||||
.allowsActivities;
|
||||
tpl.currentBoard.setAllowsActivities(
|
||||
tpl.currentBoard.allowsActivities,
|
||||
);
|
||||
$(`.js-field-has-activities ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsActivities,
|
||||
);
|
||||
$('.js-field-has-activities').toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsActivities,
|
||||
);
|
||||
const newValue = !tpl.currentBoard.allowsActivities;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsActivities: newValue } });
|
||||
},
|
||||
'click .js-field-has-cover-attachment-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.currentBoard.allowsCoverAttachmentOnMinicard = !tpl.currentBoard
|
||||
.allowsCoverAttachmentOnMinicard;
|
||||
tpl.currentBoard.setallowsCoverAttachmentOnMinicard(
|
||||
tpl.currentBoard.allowsCoverAttachmentOnMinicard,
|
||||
);
|
||||
$(`.js-field-has-cover-attachment-on-minicard ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsCoverAttachmentOnMinicard,
|
||||
);
|
||||
$('.js-field-has-cover-attachment-on-minicard').toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsCoverAttachmentOnMinicard,
|
||||
);
|
||||
const newValue = !tpl.currentBoard.allowsCoverAttachmentOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsCoverAttachmentOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-badge-attachment-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.currentBoard.allowsBadgeAttachmentOnMinicard = !tpl.currentBoard
|
||||
.allowsBadgeAttachmentOnMinicard;
|
||||
tpl.currentBoard.setallowsBadgeAttachmentOnMinicard(
|
||||
tpl.currentBoard.allowsBadgeAttachmentOnMinicard,
|
||||
);
|
||||
$(`.js-field-has-badge-attachment-on-minicard ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsBadgeAttachmentOnMinicard,
|
||||
);
|
||||
$('.js-field-has-badge-attachment-on-minicard').toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsBadgeAttachmentOnMinicard,
|
||||
);
|
||||
const newValue = !tpl.currentBoard.allowsBadgeAttachmentOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsBadgeAttachmentOnMinicard: newValue } });
|
||||
},
|
||||
'click .js-field-has-card-sorting-by-number-on-minicard'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.currentBoard.allowsCardSortingByNumberOnMinicard = !tpl.currentBoard
|
||||
.allowsCardSortingByNumberOnMinicard;
|
||||
tpl.currentBoard.setallowsCardSortingByNumberOnMinicard(
|
||||
tpl.currentBoard.allowsCardSortingByNumberOnMinicard,
|
||||
);
|
||||
$(`.js-field-has-card-sorting-by-number-on-minicard ${MCB}`).toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsCardSortingByNumberOnMinicard,
|
||||
);
|
||||
$('.js-field-has-card-sorting-by-number-on-minicard').toggleClass(
|
||||
CKCLS,
|
||||
tpl.currentBoard.allowsCardSortingByNumberOnMinicard,
|
||||
);
|
||||
const newValue = !tpl.currentBoard.allowsCardSortingByNumberOnMinicard;
|
||||
Boards.update(tpl.currentBoard._id, { $set: { allowsCardSortingByNumberOnMinicard: newValue } });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1472,7 +1531,7 @@ Template.addMemberPopup.onCreated(function() {
|
||||
};
|
||||
|
||||
this.isValidEmail = function(email) {
|
||||
return SimpleSchema.RegEx.Email.test(email);
|
||||
return /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(email);
|
||||
};
|
||||
|
||||
this.performSearch = function(query) {
|
||||
|
||||
@@ -6,14 +6,15 @@ template(name="archivesSidebar")
|
||||
li.tab-item(class="{{isArchiveTabActive slug}} {{class}}" data-tab="{{slug}}") {{name}}
|
||||
.tabs-content-container
|
||||
section.tabs-content(class="{{isArchiveTabActive 'cards'}}" data-tab="cards")
|
||||
unless isWorker
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
p.quiet
|
||||
a.js-restore-all-cards {{_ 'restore-all'}}
|
||||
if currentUser.isBoardAdmin
|
||||
| -
|
||||
a.js-delete-all-cards {{_ 'delete-all'}}
|
||||
if currentUser
|
||||
unless isWorker
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
p.quiet
|
||||
a.js-restore-all-cards {{_ 'restore-all'}}
|
||||
if currentUser.isBoardAdmin
|
||||
| -
|
||||
a.js-delete-all-cards {{_ 'delete-all'}}
|
||||
each archivedCards
|
||||
.minicard-wrapper.js-minicard
|
||||
+minicard(this)
|
||||
@@ -36,18 +37,19 @@ template(name="archivesSidebar")
|
||||
p.no-items-message {{_ 'no-archived-cards'}}
|
||||
|
||||
section.tabs-content(class="{{isArchiveTabActive 'lists'}}" data-tab="lists")
|
||||
unless isWorker
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
p.quiet
|
||||
a.js-restore-all-lists {{_ 'restore-all'}}
|
||||
if currentUser.isBoardAdmin
|
||||
| -
|
||||
a.js-delete-all-lists {{_ 'delete-all'}}
|
||||
if currentUser
|
||||
unless isWorker
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
p.quiet
|
||||
a.js-restore-all-lists {{_ 'restore-all'}}
|
||||
if currentUser.isBoardAdmin
|
||||
| -
|
||||
a.js-delete-all-lists {{_ 'delete-all'}}
|
||||
ul.archived-lists
|
||||
each archivedLists
|
||||
li.archived-lists-item
|
||||
= title
|
||||
span.archived-item-title {{title}}
|
||||
if currentUser.isBoardMember
|
||||
unless isWorker
|
||||
unless currentUser.isReadOnly
|
||||
@@ -65,18 +67,19 @@ template(name="archivesSidebar")
|
||||
li.no-items-message {{_ 'no-archived-lists'}}
|
||||
|
||||
section.tabs-content(class="{{isArchiveTabActive 'swimlanes'}}" data-tab="swimlanes")
|
||||
unless isWorker
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
p.quiet
|
||||
a.js-restore-all-swimlanes {{_ 'restore-all'}}
|
||||
if currentUser.isBoardAdmin
|
||||
| -
|
||||
a.js-delete-all-swimlanes {{_ 'delete-all'}}
|
||||
if currentUser
|
||||
unless isWorker
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
p.quiet
|
||||
a.js-restore-all-swimlanes {{_ 'restore-all'}}
|
||||
if currentUser.isBoardAdmin
|
||||
| -
|
||||
a.js-delete-all-swimlanes {{_ 'delete-all'}}
|
||||
ul.archived-lists
|
||||
each archivedSwimlanes
|
||||
li.archived-lists-item
|
||||
= title
|
||||
span.archived-item-title {{title}}
|
||||
if currentUser.isBoardMember
|
||||
unless isWorker
|
||||
unless currentUser.isReadOnly
|
||||
|
||||
@@ -2,10 +2,13 @@ import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import Lists from '/models/lists';
|
||||
import Swimlanes from '/models/swimlanes';
|
||||
import Cards from '/models/cards';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
const ARCHIVE_PAGE_SIZE = 30;
|
||||
const ARCHIVE_SCROLL_THRESHOLD_PX = 120;
|
||||
|
||||
|
||||
function throttle(func, wait) {
|
||||
let timeoutId = null;
|
||||
let lastRan = 0;
|
||||
@@ -148,13 +151,14 @@ Template.archivesSidebar.helpers({
|
||||
return Template.instance().isArchiveReady;
|
||||
},
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
return user && user.isBoardAdmin();
|
||||
},
|
||||
isWorker() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return (
|
||||
!currentBoard.hasAdmin(this.userId) && currentBoard.hasWorker(this.userId)
|
||||
);
|
||||
if (!currentBoard) return false;
|
||||
const userId = Meteor.userId();
|
||||
return !currentBoard.hasAdmin(userId) && currentBoard.hasWorker(userId);
|
||||
},
|
||||
|
||||
tabs() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import CustomFields from '/models/customFields';
|
||||
|
||||
Template.customFieldsSidebar.helpers({
|
||||
customFields() {
|
||||
|
||||
@@ -21,14 +21,14 @@ template(name="filterSidebar")
|
||||
| {{_ 'filter-labels-label'}}
|
||||
ul.sidebar-list
|
||||
li(class="{{#if Filter.labelIds.isSelected undefined}}active{{/if}}")
|
||||
a.name.js-toggle-label-filter
|
||||
a.name.js-toggle-label-filter(data-filter-id="__none__")
|
||||
span.sidebar-list-item-description
|
||||
| {{_ 'filter-no-label'}}
|
||||
if Filter.labelIds.isSelected undefined
|
||||
i.fa.fa-check
|
||||
each currentBoard.labels
|
||||
li
|
||||
a.name.js-toggle-label-filter
|
||||
a.name.js-toggle-label-filter(data-filter-id="{{_id}}")
|
||||
span.card-label.square(class="card-label-{{color}}")
|
||||
span.sidebar-list-item-description
|
||||
if name
|
||||
@@ -43,7 +43,7 @@ template(name="filterSidebar")
|
||||
| {{_ 'filter-member-label'}}
|
||||
ul.sidebar-list
|
||||
li(class="{{#if Filter.members.isSelected undefined}}active{{/if}}")
|
||||
a.name.js-toggle-member-filter
|
||||
a.name.js-toggle-member-filter(data-filter-id="__none__")
|
||||
span.sidebar-list-item-description
|
||||
| {{_ 'filter-no-member'}}
|
||||
if Filter.members.isSelected undefined
|
||||
@@ -51,7 +51,7 @@ template(name="filterSidebar")
|
||||
each currentBoard.activeMembers
|
||||
with getUser userId
|
||||
li(class="{{#if Filter.members.isSelected _id}}active{{/if}}")
|
||||
a.name.js-toggle-member-filter
|
||||
a.name.js-toggle-member-filter(data-filter-id="{{_id}}")
|
||||
+userAvatar(userId=this._id)
|
||||
span.sidebar-list-item-description
|
||||
= profile.fullname
|
||||
@@ -64,7 +64,7 @@ template(name="filterSidebar")
|
||||
| {{_ 'filter-assignee-label'}}
|
||||
ul.sidebar-list
|
||||
li(class="{{#if Filter.assignees.isSelected undefined}}active{{/if}}")
|
||||
a.name.js-toggle-assignee-filter
|
||||
a.name.js-toggle-assignee-filter(data-filter-id="__none__")
|
||||
span.sidebar-list-item-description
|
||||
| {{_ 'filter-no-assignee'}}
|
||||
if Filter.assignees.isSelected undefined
|
||||
@@ -72,7 +72,7 @@ template(name="filterSidebar")
|
||||
each currentBoard.activeMembers
|
||||
with getUser userId
|
||||
li(class="{{#if Filter.assignees.isSelected _id}}active{{/if}}")
|
||||
a.name.js-toggle-assignee-filter
|
||||
a.name.js-toggle-assignee-filter(data-filter-id="{{_id}}")
|
||||
+userAvatar(userId=this._id)
|
||||
span.sidebar-list-item-description
|
||||
= profile.fullname
|
||||
@@ -126,14 +126,14 @@ template(name="filterSidebar")
|
||||
| {{_ 'filter-custom-fields-label'}}
|
||||
ul.sidebar-list
|
||||
li(class="{{#if Filter.customFields.isSelected undefined}}active{{/if}}")
|
||||
a.name.js-toggle-custom-fields-filter
|
||||
a.name.js-toggle-custom-fields-filter(data-filter-id="__none__")
|
||||
span.sidebar-list-item-description
|
||||
| {{_ 'filter-no-custom-fields'}}
|
||||
if Filter.customFields.isSelected undefined
|
||||
i.fa.fa-check
|
||||
each currentBoard.customFields
|
||||
li(class="{{#if Filter.customFields.isSelected _id}}active{{/if}}")
|
||||
a.name.js-toggle-custom-fields-filter
|
||||
a.name.js-toggle-custom-fields-filter(data-filter-id="{{_id}}")
|
||||
span.sidebar-list-item-description
|
||||
| {{ name }}
|
||||
if Filter.customFields.isSelected _id
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { Filter } from '/client/lib/filter';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { MultiSelection } from '/client/lib/multiSelection';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
// SubsManager removed for Meteor 3 migration
|
||||
|
||||
function getFilterIdFromEvent(evt, fallbackId) {
|
||||
const filterId = evt.currentTarget?.getAttribute('data-filter-id');
|
||||
if (filterId === '__none__') {
|
||||
return undefined;
|
||||
}
|
||||
if (filterId !== null) {
|
||||
return filterId;
|
||||
}
|
||||
return fallbackId;
|
||||
}
|
||||
|
||||
Template.filterSidebar.events({
|
||||
'submit .js-list-filter'(evt, tpl) {
|
||||
@@ -15,17 +30,17 @@ Template.filterSidebar.events({
|
||||
},
|
||||
'click .js-toggle-label-filter'(evt) {
|
||||
evt.preventDefault();
|
||||
Filter.labelIds.toggle(Template.currentData()._id);
|
||||
Filter.labelIds.toggle(getFilterIdFromEvent(evt, this?._id));
|
||||
Filter.resetExceptions();
|
||||
},
|
||||
'click .js-toggle-member-filter'(evt) {
|
||||
evt.preventDefault();
|
||||
Filter.members.toggle(Template.currentData()._id);
|
||||
Filter.members.toggle(getFilterIdFromEvent(evt, this?._id));
|
||||
Filter.resetExceptions();
|
||||
},
|
||||
'click .js-toggle-assignee-filter'(evt) {
|
||||
evt.preventDefault();
|
||||
Filter.assignees.toggle(Template.currentData()._id);
|
||||
Filter.assignees.toggle(getFilterIdFromEvent(evt, this?._id));
|
||||
Filter.resetExceptions();
|
||||
},
|
||||
'click .js-toggle-no-due-date-filter'(evt) {
|
||||
@@ -64,7 +79,7 @@ Template.filterSidebar.events({
|
||||
Filter.resetExceptions();
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
if (!currentBoardId) return;
|
||||
subManager.subscribe(
|
||||
Meteor.subscribe(
|
||||
'board',
|
||||
currentBoardId,
|
||||
Filter.archive.isSelected(),
|
||||
@@ -77,7 +92,7 @@ Template.filterSidebar.events({
|
||||
},
|
||||
'click .js-toggle-custom-fields-filter'(evt) {
|
||||
evt.preventDefault();
|
||||
Filter.customFields.toggle(Template.currentData()._id);
|
||||
Filter.customFields.toggle(getFilterIdFromEvent(evt, this?._id));
|
||||
Filter.resetExceptions();
|
||||
},
|
||||
'change .js-field-advanced-filter'(evt, tpl) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
Template.searchSidebar.onCreated(function () {
|
||||
this.term = new ReactiveVar('');
|
||||
});
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { SWIMLANE_COLORS } from '/models/metadata/colors';
|
||||
import Swimlanes from '/models/swimlanes';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
const { calculateIndexData } = Utils;
|
||||
|
||||
let swimlaneColors;
|
||||
Meteor.startup(() => {
|
||||
swimlaneColors = Swimlanes.simpleSchema()._schema.color.allowedValues;
|
||||
swimlaneColors = SWIMLANE_COLORS;
|
||||
});
|
||||
|
||||
function swimlaneHeaderCollapsed(check = undefined) {
|
||||
@@ -117,7 +120,7 @@ Template.swimlaneAddPopup.onCreated(function () {
|
||||
});
|
||||
|
||||
Template.swimlaneAddPopup.events({
|
||||
submit(event, tpl) {
|
||||
async submit(event, tpl) {
|
||||
event.preventDefault();
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const nextSwimlane = currentBoard.nextSwimlane(tpl.currentSwimlane);
|
||||
@@ -133,7 +136,7 @@ Template.swimlaneAddPopup.events({
|
||||
: 'swimlane';
|
||||
|
||||
if (title) {
|
||||
Swimlanes.insert({
|
||||
await Swimlanes.insertAsync({
|
||||
title,
|
||||
boardId: Session.get('currentBoard'),
|
||||
sort: sortValue.base || 0,
|
||||
@@ -172,13 +175,16 @@ Template.setSwimlaneColorPopup.events({
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-palette-color'(event, tpl) {
|
||||
tpl.currentColor.set(Template.currentData().color);
|
||||
const paletteData = Blaze.getData(event.currentTarget);
|
||||
tpl.currentColor.set(paletteData?.color);
|
||||
},
|
||||
async 'click .js-submit'(event, tpl) {
|
||||
event.preventDefault();
|
||||
await tpl.currentSwimlane.setColor(tpl.currentColor.get());
|
||||
Popup.back();
|
||||
},
|
||||
async 'click .js-remove-color'(event, tpl) {
|
||||
event.preventDefault();
|
||||
await tpl.currentSwimlane.setColor(null);
|
||||
Popup.back();
|
||||
},
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { once } from '/imports/lib/collectionHelpers';
|
||||
import dragscroll from '@wekanteam/dragscroll';
|
||||
import Lists from '/models/lists';
|
||||
import { CSSEvents } from '/client/lib/cssEvents';
|
||||
import { Filter } from '/client/lib/filter';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
const { calculateIndex } = Utils;
|
||||
|
||||
function saveSorting(ui) {
|
||||
@@ -137,10 +142,23 @@ function saveSorting(ui) {
|
||||
|
||||
function currentListIsInThisSwimlane(swimlaneId) {
|
||||
const currentList = Utils.getCurrentList();
|
||||
return (
|
||||
currentList &&
|
||||
(currentList.swimlaneId === swimlaneId || currentList.swimlaneId === '')
|
||||
if (!currentList) return false;
|
||||
// Match the list's own swimlane, or shared/orphaned lists (empty/null
|
||||
// swimlaneId are visible in every swimlane as a fallback).
|
||||
if (currentList.swimlaneId === swimlaneId || !currentList.swimlaneId) {
|
||||
return true;
|
||||
}
|
||||
// Also match when the list has an orphaned swimlaneId (references a deleted
|
||||
// swimlane) and THIS is the first swimlane — orphaned lists are shown there.
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
if (!currentBoard) return false;
|
||||
const allSwimlanes = ReactiveCache.getSwimlanes(
|
||||
{ boardId: currentBoard._id, archived: false },
|
||||
{ sort: ['sort'] },
|
||||
);
|
||||
if (!allSwimlanes.length || allSwimlanes[0]._id !== swimlaneId) return false;
|
||||
const validIds = new Set(allSwimlanes.map(s => s._id));
|
||||
return !validIds.has(currentList.swimlaneId);
|
||||
}
|
||||
|
||||
function currentCardIsInThisList(listId, swimlaneId) {
|
||||
@@ -154,7 +172,10 @@ function currentCardIsInThisList(listId, swimlaneId) {
|
||||
return (
|
||||
currentCard &&
|
||||
currentCard.listId === listId &&
|
||||
currentCard.swimlaneId === swimlaneId
|
||||
// Match cards for this swimlane, AND orphaned/shared cards that have no
|
||||
// swimlaneId assigned (null/empty) — they are shown in every swimlane as
|
||||
// a fallback so no content is ever invisible.
|
||||
(currentCard.swimlaneId === swimlaneId || !currentCard.swimlaneId)
|
||||
);
|
||||
else if (
|
||||
//currentUser &&
|
||||
@@ -577,8 +598,38 @@ Template.swimlane.helpers({
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
lists() {
|
||||
// Return per-swimlane lists for this swimlane
|
||||
return this.myLists();
|
||||
const swimlane = this;
|
||||
// myLists() already covers:
|
||||
// • lists owned by this swimlane (swimlaneId === this._id)
|
||||
// • shared / pre-migration lists (swimlaneId empty or null)
|
||||
const regularLists = swimlane.myLists();
|
||||
|
||||
// Additionally, detect lists whose swimlaneId references a swimlane that
|
||||
// no longer exists ("orphaned-swimlane" lists). These would be invisible
|
||||
// in every swimlane without this fallback. Show them in the FIRST swimlane
|
||||
// on the board so they are always accessible without a DB migration.
|
||||
const allSwimlanes = ReactiveCache.getSwimlanes(
|
||||
{ boardId: swimlane.boardId, archived: false },
|
||||
{ sort: ['sort'] },
|
||||
);
|
||||
// Only the first swimlane picks up orphaned-swimlane lists.
|
||||
if (!allSwimlanes.length || allSwimlanes[0]._id !== swimlane._id) {
|
||||
return regularLists;
|
||||
}
|
||||
const validIds = allSwimlanes.map(s => s._id);
|
||||
const orphaned = swimlane.orphanedSwimlaneLists(validIds);
|
||||
if (!orphaned.length) return regularLists;
|
||||
|
||||
// Merge, deduplicating by _id (regularLists may already contain some).
|
||||
const seen = new Set(regularLists.map(l => l._id));
|
||||
const combined = [...regularLists];
|
||||
for (const l of orphaned) {
|
||||
if (!seen.has(l._id)) {
|
||||
seen.add(l._id);
|
||||
combined.push(l);
|
||||
}
|
||||
}
|
||||
return combined;
|
||||
},
|
||||
collapseSwimlane() {
|
||||
return Utils.getSwimlaneCollapseState(this);
|
||||
@@ -604,7 +655,9 @@ Template.swimlane.helpers({
|
||||
}
|
||||
}
|
||||
if (Filter.hideEmpty.isSelected()) {
|
||||
const cards = list.cards();
|
||||
// Pass the current swimlane ID so we only count cards belonging to
|
||||
// this swimlane (not cards in other swimlanes that happen to match).
|
||||
const cards = list.cards(this._id);
|
||||
if (cards.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
|
||||
// Reactive source for invalidating people row when avatars change
|
||||
export const avatarUpdateCounter = new ReactiveVar(0);
|
||||
@@ -70,11 +70,11 @@ template(name="changeAvatarPopup")
|
||||
each uploadedAvatars
|
||||
li: a.js-select-avatar
|
||||
.member
|
||||
img.avatar.avatar-image(src="{{link}}")
|
||||
img.avatar.avatar-image(src="{{avatarLink}}")
|
||||
if isSelected
|
||||
i.fa.fa-check
|
||||
p.sub-name
|
||||
a.js-delete-avatar {{_ 'delete'}}
|
||||
a.js-delete-avatar(data-avatar-id="{{_id}}") {{_ 'delete'}}
|
||||
| -
|
||||
= name
|
||||
li: a.js-select-initials
|
||||
@@ -100,6 +100,41 @@ template(name="deleteAvatarPopup")
|
||||
p {{_ 'delete-avatar-confirm'}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="adminChangeAvatarPopup")
|
||||
.admin-change-avatar-header
|
||||
h3 {{_ 'change-avatar'}} - {{ username }}
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
ul.pop-over-list
|
||||
each uploadedAvatars
|
||||
li: a.js-select-avatar
|
||||
.member
|
||||
img.avatar.avatar-image(src="{{avatarLink}}")
|
||||
if isSelected
|
||||
i.fa.fa-check
|
||||
p.sub-name
|
||||
a.js-delete-avatar(data-avatar-id="{{_id}}") {{_ 'delete'}}
|
||||
| -
|
||||
= name
|
||||
li: a.js-select-initials
|
||||
.member
|
||||
+userAvatarInitials(userData=userData)
|
||||
| {{_ 'initials' }}
|
||||
if noAvatarUrl
|
||||
i.fa.fa-check
|
||||
p.sub-name {{_ 'default-avatar'}}
|
||||
input.hide.js-upload-avatar-input(accept="image/*;capture=camera" type="file")
|
||||
if Meteor.settings.public.avatarsUploadMaxSize
|
||||
| {{_ 'max-avatar-filesize'}} {{Meteor.settings.public.avatarsUploadMaxSize}}
|
||||
br
|
||||
if Meteor.settings.public.avatarsUploadMimeTypes
|
||||
| {{_ 'allowed-avatar-filetypes'}} {{Meteor.settings.public.avatarsUploadMimeTypes}}
|
||||
br
|
||||
| {{_ 'invalid-file'}}
|
||||
button.full.js-upload-avatar
|
||||
i.fa.fa-upload
|
||||
| {{_ 'upload-avatar' }}
|
||||
|
||||
template(name="cardMemberPopup")
|
||||
.board-member-menu
|
||||
.mini-profile-info
|
||||
|
||||
Loaded 100 of 1033 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user