Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b2759f97d | ||
|
|
adf4312e5a | ||
|
|
b1707b8691 | ||
|
|
25dff3f2da | ||
|
|
bc88c0dfc2 | ||
|
|
1ca2dfd1b1 | ||
|
|
bdb9274485 | ||
|
|
14f9ac39ad | ||
|
|
d4b52f7de9 | ||
|
|
bdc65ec6f4 | ||
|
|
7fb31cecd5 | ||
|
|
317c82a9f0 | ||
|
|
9b5c89d235 | ||
|
|
c23e190e8e | ||
|
|
4a73371499 | ||
|
|
6f8af55489 | ||
|
|
2a05f62bf9 | ||
|
|
d6eb1eb76f | ||
|
|
27542f3a51 | ||
|
|
0328e70d5f | ||
|
|
c46932ee30 | ||
|
|
c7408ad8d1 | ||
|
|
05dc8f8f93 | ||
|
|
aea8c285c2 | ||
|
|
a859137971 | ||
|
|
08ebad0fe4 | ||
|
|
d54ebfa0e1 | ||
|
|
304a007d07 | ||
|
|
b6b58c9d95 | ||
|
|
7f0c42cca5 | ||
|
|
9508d46ebd | ||
|
|
bcbf4771ea | ||
|
|
9da5ec25c6 | ||
|
|
028b404377 | ||
|
|
8da0f34ee6 | ||
|
|
cac2628b19 | ||
|
|
9c69530256 | ||
|
|
6e108358d7 | ||
|
|
87f592c67f | ||
|
|
fc95abd587 | ||
|
|
ce3e51b147 | ||
|
|
bb48b23266 | ||
|
|
fdf00e06f5 | ||
|
|
950b246da8 | ||
|
|
6ac0df8448 | ||
|
|
80ae3a18a8 | ||
|
|
972bfb9677 | ||
|
|
b1149e4d6e | ||
|
|
3cd2ba847b | ||
|
|
8bf869654c | ||
|
|
3b98f862bc | ||
|
|
9bc889e274 | ||
|
|
5df924f416 | ||
|
|
5ef94b7079 | ||
|
|
dfcaaf0ae7 | ||
|
|
34e7ff532f | ||
|
|
fbdfcead50 | ||
|
|
b8bbe4431b | ||
|
|
9e31666280 | ||
|
|
5846d9c9b0 | ||
|
|
7707a5b7fc | ||
|
|
18c495940e | ||
|
|
d3155d24c6 | ||
|
|
c0661506c4 | ||
|
|
fa2b34764f | ||
|
|
7a7909b49f | ||
|
|
17d3b3088f | ||
|
|
6a78a178e2 | ||
|
|
749de39e2a | ||
|
|
cd55c0e12e | ||
|
|
d04037739e | ||
|
|
f3a97b9938 | ||
|
|
104fbef37a | ||
|
|
98b3dc71af | ||
|
|
7b28e4d589 | ||
|
|
57291dbe50 | ||
|
|
9f9eefce27 | ||
|
|
af3361b35b | ||
|
|
24d65e1e72 | ||
|
|
29922fca6a | ||
|
|
81319fc48b | ||
|
|
28b7209407 | ||
|
|
0b59f87887 | ||
|
|
b22df9e93d | ||
|
|
1fa72fd34a | ||
|
|
a2f38e7494 | ||
|
|
30a1792c92 | ||
|
|
3d70f24a78 | ||
|
|
b3d14fb5e8 | ||
|
|
59956ca720 | ||
|
|
aaf7690b9a | ||
|
|
9f549dfcd2 | ||
|
|
f191392e3a | ||
|
|
56f26418ea | ||
|
|
118e0e5d5c | ||
|
|
264dcca42b | ||
|
|
73d69f4631 | ||
|
|
485b773338 | ||
|
|
8443e3511d | ||
|
|
b2a349aec5 | ||
|
|
faf8453c6d | ||
|
|
7484c8cce3 | ||
|
|
2b02e7712e | ||
|
|
8a3a7b19aa | ||
|
|
9fa6574c80 | ||
|
|
2bad17010c | ||
|
|
4168385607 | ||
|
|
c0aa1ef375 | ||
|
|
811b79b641 | ||
|
|
dab9755dd4 | ||
|
|
6863479c09 | ||
|
|
e05eebb8b0 | ||
|
|
28ef0e6e89 | ||
|
|
09e65ccac2 | ||
|
|
03e0cfdc15 | ||
|
|
7634cd6c2e | ||
|
|
4594711be4 | ||
|
|
c267933bf7 | ||
|
|
edc6b18b5f | ||
|
|
60657c6e94 | ||
|
|
3c189f9c7f | ||
|
|
bf02c9a453 | ||
|
|
e6b6cd3036 | ||
|
|
6e8589b1f9 | ||
|
|
1354f84a70 | ||
|
|
d07cf92f7c | ||
|
|
e079644ed6 | ||
|
|
6b393178dc | ||
|
|
26e1c9ab69 | ||
|
|
145a17fee5 | ||
|
|
ccc9301ed4 | ||
|
|
f2e46d660f | ||
|
|
8b4676175c | ||
|
|
e9a547c89b | ||
|
|
d07fcceb6a | ||
|
|
6b5002f314 | ||
|
|
5418ffd558 | ||
|
|
ea98afeb68 | ||
|
|
be7d7e26fb | ||
|
|
56377aaf4c | ||
|
|
43601c5e3e | ||
|
|
1db7f3a57d | ||
|
|
e58e99c285 | ||
|
|
b38e25c9f1 | ||
|
|
ce52b8a0ab | ||
|
|
f7800846a5 | ||
|
|
02b75824e5 | ||
|
|
cf52c2f6e4 | ||
|
|
6bc68ba57b | ||
|
|
5ec9cfa537 | ||
|
|
3457cab0b1 | ||
|
|
8349fb6b63 | ||
|
|
33267fa2e4 | ||
|
|
5c4130311a | ||
|
|
ed718f083a | ||
|
|
2aa8ecfc39 | ||
|
|
cdf1d3305b | ||
|
|
fd58bb9e4d | ||
|
|
55ec0b5ff5 | ||
|
|
e5c551f009 | ||
|
|
7666c6c0e1 | ||
|
|
e6ccbfd3b8 | ||
|
|
d8a2e06a95 | ||
|
|
c38fa1c272 | ||
|
|
1a5e23617d | ||
|
|
87916c20bf | ||
|
|
12c223d226 | ||
|
|
9ae2842fa4 | ||
|
|
79853a2fb7 | ||
|
|
ec1e383558 | ||
|
|
df067f01e8 | ||
|
|
4b8cc0683e | ||
|
|
f43b76c323 | ||
|
|
e90b2e384c | ||
|
|
6125822cf7 | ||
|
|
4223c2a79f | ||
|
|
7059cc244a | ||
|
|
5fc608e310 | ||
|
|
29550cbd93 | ||
|
|
35674a205b | ||
|
|
a702596bee | ||
|
|
02fef72e7e | ||
|
|
d5c9e402ce | ||
|
|
b788d398b9 | ||
|
|
5c3b73e3b0 | ||
|
|
82f1de078f | ||
|
|
ddc353023c | ||
|
|
ef75b9ecac | ||
|
|
cf2b3e4fc9 | ||
|
|
cf21991a3f | ||
|
|
9e4f72e2a4 | ||
|
|
f7da243c9e | ||
|
|
a71efd2668 | ||
|
|
6a23290584 | ||
|
|
0e59033ea6 | ||
|
|
186f7a6b82 | ||
|
|
5cf3bba68f | ||
|
|
4647ddc2b7 | ||
|
|
1ac708b0d9 | ||
|
|
07eb062474 | ||
|
|
f660817627 | ||
|
|
0fdf45e847 | ||
|
|
0c3cebd30a | ||
|
|
c589918989 | ||
|
|
256aa0a053 | ||
|
|
67c56e41b0 | ||
|
|
ade9e0f589 | ||
|
|
e14dfb9e7e | ||
|
|
c26cc313fc | ||
|
|
55660e0e69 | ||
|
|
3701544f6a | ||
|
|
acfb081b90 | ||
|
|
a30d7cabe3 | ||
|
|
517d148387 | ||
|
|
f8aec4de73 | ||
|
|
e1b6cf6124 | ||
|
|
1eac00e7fa | ||
|
|
278ed23440 | ||
|
|
b42689a54d | ||
|
|
10bfa30458 | ||
|
|
a0a1cda0a7 | ||
|
|
001af3fabd | ||
|
|
c0316e9446 | ||
|
|
d80a6e7e38 | ||
|
|
8d8d8f9cee | ||
|
|
0630d62ad0 | ||
|
|
c07b282dc9 | ||
|
|
5aa39463b0 | ||
|
|
f1e71247c7 | ||
|
|
419fb2535b | ||
|
|
23697d2a33 | ||
|
|
55edc7ef63 | ||
|
|
c1e3ce64a7 | ||
|
|
d34163402c | ||
|
|
5c5ed102ca | ||
|
|
0bbadfe9a6 | ||
|
|
21ab640686 | ||
|
|
a3f6d200ac | ||
|
|
701c10d618 | ||
|
|
7f8de1f988 | ||
|
|
95ff8ef1f2 | ||
|
|
4eb885f88a | ||
|
|
9a7a0b7271 | ||
|
|
4a8293f881 | ||
|
|
56ed341510 | ||
|
|
7584afda6d | ||
|
|
bbb8f8e51d | ||
|
|
ddd873c7a0 | ||
|
|
63ce45c538 | ||
|
|
8ffc0cb5ff | ||
|
|
ce95907778 | ||
|
|
4ed3250352 | ||
|
|
2dce7a84cd | ||
|
|
b9631659f7 | ||
|
|
405cf3efa3 | ||
|
|
462b3801ee | ||
|
|
7ec42880f0 | ||
|
|
3d6797a305 | ||
|
|
065fe35b6a | ||
|
|
4a4e4135ab | ||
|
|
e388d18aad | ||
|
|
60d9698343 | ||
|
|
a7c9f7aa17 | ||
|
|
7de1b04ebc | ||
|
|
024ec23b7f | ||
|
|
7b2128b356 | ||
|
|
6d54b826e8 | ||
|
|
30c4010512 | ||
|
|
c4c8446fc2 | ||
|
|
c374c87c08 | ||
|
|
8486bb1e87 | ||
|
|
655f44919a | ||
|
|
17ea7978f0 | ||
|
|
b518946c41 | ||
|
|
ca8a4af354 | ||
|
|
1682f8834d | ||
|
|
eac472c54b | ||
|
|
3a873559f8 | ||
|
|
aec6779601 | ||
|
|
973d9eccae | ||
|
|
7468b30001 | ||
|
|
e68cb5f32f | ||
|
|
a157ae5d1c | ||
|
|
620e5ce9dc | ||
|
|
2e3fbf25db | ||
|
|
e8fee653c3 | ||
|
|
0d3f3474d1 | ||
|
|
6ecb6bdaa7 | ||
|
|
738655d380 | ||
|
|
3f0eb227ec | ||
|
|
409a624e33 | ||
|
|
1b02184482 | ||
|
|
f88e6671a2 | ||
|
|
073c297490 | ||
|
|
e92578d41d | ||
|
|
0644aeb70e | ||
|
|
456bda1169 | ||
|
|
4b83a3f861 | ||
|
|
7e18526ed1 | ||
|
|
ffef908c8d | ||
|
|
be5bdbe314 | ||
|
|
6d0f869309 | ||
|
|
bfd0f9d0e4 | ||
|
|
ee9b5a0d25 | ||
|
|
445746fd7f | ||
|
|
ac33d8012b | ||
|
|
8513b2e456 | ||
|
|
26d393f8ee | ||
|
|
bee322cb2e | ||
|
|
bf209bfdd7 | ||
|
|
0e410b6d7a | ||
|
|
2e4e3b1a06 | ||
|
|
7ed26f437d | ||
|
|
e50b9f211d | ||
|
|
e705df6634 | ||
|
|
e669a9fe64 | ||
|
|
6249af041c | ||
|
|
a4a4fc1cf2 | ||
|
|
6acc7c95ff | ||
|
|
3d75e905d1 | ||
|
|
55bc0ed411 | ||
|
|
37142f3bb9 | ||
|
|
c11aa2a194 | ||
|
|
9da7adad95 | ||
|
|
69277d4b24 | ||
|
|
2e157501c3 | ||
|
|
610849ab0b | ||
|
|
b966607070 | ||
|
|
d0c44bded8 | ||
|
|
c79f5be07a | ||
|
|
baab46a3fb | ||
|
|
92beaa3137 | ||
|
|
f03d0ddf30 | ||
|
|
4dea702ff2 | ||
|
|
7e55b83a7b | ||
|
|
259f3776d2 | ||
|
|
2f07eb7097 | ||
|
|
6d6247d449 | ||
|
|
c3f66cce55 | ||
|
|
8ee178dd89 | ||
|
|
d01d7fbcc9 | ||
|
|
252b95f661 | ||
|
|
89f262feae | ||
|
|
7167f53270 | ||
|
|
c989c21d4f | ||
|
|
5d83d4c239 | ||
|
|
94089f9505 | ||
|
|
872dd578c8 | ||
|
|
fba00535fa | ||
|
|
ccd23f6cde | ||
|
|
7a1db4e040 | ||
|
|
7d0a422714 | ||
|
|
e411a4fd8e | ||
|
|
77b57d8233 | ||
|
|
d14005a183 | ||
|
|
0c29fa3cbe | ||
|
|
60bc1d55f4 | ||
|
|
bcadc517c3 | ||
|
|
eb4cb341c6 | ||
|
|
331bc2d1b9 | ||
|
|
b5782c7b86 | ||
|
|
01bb5a930d | ||
|
|
1bb657b073 | ||
|
|
740f39e17c | ||
|
|
e34e4b8404 | ||
|
|
e6fbfa87f0 | ||
|
|
a31de9e1a0 | ||
|
|
a3ac219503 | ||
|
|
5af4a5395b | ||
|
|
d2168aca3d | ||
|
|
6148d96b9a | ||
|
|
b6a6180dc3 | ||
|
|
6edd3c77ec | ||
|
|
36741d7ae5 | ||
|
|
507b196101 | ||
|
|
6fc1cb9127 | ||
|
|
b3ae3871f2 | ||
|
|
1f6b5ed0e3 | ||
|
|
ab5fe6411a | ||
|
|
5dd3209ee8 | ||
|
|
fccefcaa6c | ||
|
|
98249dda8e | ||
|
|
26732d4f6e | ||
|
|
9a6319772e | ||
|
|
2547967113 | ||
|
|
f436525f20 | ||
|
|
7b3f7b8e6b | ||
|
|
a3dece58fb | ||
|
|
a159856628 | ||
|
|
7fb6e2a5d9 | ||
|
|
0cdae9def1 | ||
|
|
63df6cb197 | ||
|
|
55d2d0843a | ||
|
|
7e378be1d8 | ||
|
|
89f86caf69 | ||
|
|
9604598a79 | ||
|
|
90d9805abf | ||
|
|
0deb7ab641 | ||
|
|
230f954571 | ||
|
|
e3f739e5d9 | ||
|
|
6b75bfcb0c | ||
|
|
db47ed5430 | ||
|
|
48361b2047 | ||
|
|
6953cc0670 | ||
|
|
477e1c89e5 | ||
|
|
bae23f9ed8 | ||
|
|
d3625db755 | ||
|
|
d9e2e8f97e | ||
|
|
f1625ad1f5 | ||
|
|
012947c076 | ||
|
|
3b6f6fa80a | ||
|
|
2848b1a38a | ||
|
|
53a0beffee | ||
|
|
4cb99c6014 | ||
|
|
e485168169 | ||
|
|
44a083e095 | ||
|
|
1bd3363708 | ||
|
|
ca87e5c3cf | ||
|
|
2cd702f48d | ||
|
|
97b0c392be | ||
|
|
274f1309c3 | ||
|
|
326f9bed84 | ||
|
|
9f79a8b6ed | ||
|
|
98eda7f49f | ||
|
|
5fe0b737c2 | ||
|
|
c29e92de1c | ||
|
|
e9507d37cf | ||
|
|
3454ab0278 | ||
|
|
cfc9abea99 | ||
|
|
8e68bb6343 | ||
|
|
69ab456ed2 | ||
|
|
441b2b7c0d | ||
|
|
81d3a08b9b | ||
|
|
3ebeac6b30 | ||
|
|
fa2f36b76b | ||
|
|
8b1e1dd92a | ||
|
|
31614e58f2 | ||
|
|
9ff15562d9 | ||
|
|
8c4f432e0c | ||
|
|
d34a886cac | ||
|
|
3ee4d1cace | ||
|
|
ff7729fc35 | ||
|
|
7127862bea | ||
|
|
4a8037047a | ||
|
|
9b3ecd795f | ||
|
|
cc06f515ce | ||
|
|
d57c5f736f | ||
|
|
cf8c82b3fb | ||
|
|
f73bcfd134 | ||
|
|
3e400820e5 | ||
|
|
b6f9eac37e | ||
|
|
7b443b7bfc | ||
|
|
8dafc774d8 | ||
|
|
2fe490ec3d | ||
|
|
d25da91e0b | ||
|
|
1b74fd8d4e | ||
|
|
d796c4d9bb | ||
|
|
8787ce84ac | ||
|
|
33e0c08adc | ||
|
|
a34c2f35a6 | ||
|
|
6903e12112 | ||
|
|
62216e36c1 | ||
|
|
73eb98c57a | ||
|
|
1ee9b2e917 | ||
|
|
8c00adc6b8 | ||
|
|
1c8667eae8 | ||
|
|
f0deed448a | ||
|
|
06d418b12b | ||
|
|
f6f16608dd | ||
|
|
8816c886cf | ||
|
|
6192c19fde | ||
|
|
f463198e40 | ||
|
|
ed907f8c61 | ||
|
|
55d2a5d15c | ||
|
|
6168356ad2 | ||
|
|
71b1a0381f | ||
|
|
640629f69c | ||
|
|
cde5af62cf | ||
|
|
d016e66938 | ||
|
|
5be23f61d0 | ||
|
|
a0c918c646 | ||
|
|
1d910b8a89 | ||
|
|
c042eeb07e | ||
|
|
afd40b21f1 | ||
|
|
f40a00ef0e | ||
|
|
e77be37450 | ||
|
|
15f979f08a | ||
|
|
c8beb6018f | ||
|
|
f934aea2a5 | ||
|
|
3d69c33caf | ||
|
|
2b15a8ff09 | ||
|
|
5212f3beb3 | ||
|
|
4f45ebef84 | ||
|
|
8afdcd7a4c | ||
|
|
8ba18a971e | ||
|
|
c47d443dcf | ||
|
|
b5a13f0206 | ||
|
|
dace6b78c0 | ||
|
|
29c5d6fbb3 | ||
|
|
bebea9efea | ||
|
|
5a5eaec340 | ||
|
|
217cd7bcb6 | ||
|
|
6bf60c496b | ||
|
|
68fb0a81e3 | ||
|
|
779f014fc5 | ||
|
|
4c5da0254a | ||
|
|
fed2e9dd4e | ||
|
|
3e8c2e98e7 | ||
|
|
0db7ab2eaa | ||
|
|
66ccba9267 | ||
|
|
0951c37536 | ||
|
|
1870c256b6 | ||
|
|
117a00c996 | ||
|
|
dd798b4c13 | ||
|
|
b6ceef6775 | ||
|
|
89ce2f825d | ||
|
|
eaf2d3fbb3 | ||
|
|
91eb206ed5 | ||
|
|
eab8d7e2fc | ||
|
|
4d65e214c3 | ||
|
|
4d80dab633 | ||
|
|
8e1a800c31 | ||
|
|
af30a00af2 | ||
|
|
b52a337d36 | ||
|
|
0f4ff6e30e | ||
|
|
ffe7bb6ce5 | ||
|
|
fc33f10e9b | ||
|
|
f8aa487e91 | ||
|
|
77041e0d74 | ||
|
|
1fd2468af6 | ||
|
|
1b8b8d2eef | ||
|
|
d152d8fc1b | ||
|
|
473f8640d0 | ||
|
|
6acdee51b6 | ||
|
|
537ea369bf | ||
|
|
dba265aaef | ||
|
|
97dd5d2064 | ||
|
|
dc0b68ee80 | ||
|
|
93ebae78b8 | ||
|
|
9610d8f883 | ||
|
|
631c250f40 | ||
|
|
5836e50e69 | ||
|
|
0a92e896f8 | ||
|
|
d6f9626a10 | ||
|
|
0ae9865fcb | ||
|
|
0987154a7f | ||
|
|
cb6d487cbd | ||
|
|
55710835fe | ||
|
|
c7b46c86c3 | ||
|
|
4456bc1360 | ||
|
|
41c417fda0 | ||
|
|
345f9ec223 | ||
|
|
8bae45add3 | ||
|
|
6c2e2f271d | ||
|
|
90b0c47c41 | ||
|
|
79e9dadc38 | ||
|
|
404dd0735d | ||
|
|
30458c617e | ||
|
|
e8b9a3a163 | ||
|
|
d68ad47de6 | ||
|
|
10cef2f254 | ||
|
|
8c252163c9 | ||
|
|
c31758960f | ||
|
|
ef32a72b4b | ||
|
|
7dbebed6db | ||
|
|
dda22ddb50 | ||
|
|
42d1b1c725 | ||
|
|
e16e3aac2d | ||
|
|
19237ca7ce | ||
|
|
71eb01e233 | ||
|
|
bb12311e2f | ||
|
|
2f6e34c5f5 | ||
|
|
e241e27ca1 | ||
|
|
bbd9fd838b | ||
|
|
35715ef2a3 | ||
|
|
14de981ac3 | ||
|
|
88b35a6415 | ||
|
|
b55e1bbd40 | ||
|
|
520304d560 | ||
|
|
be1ad7d041 | ||
|
|
f73eab23f9 | ||
|
|
9c2f782f84 | ||
|
|
0b891464b9 | ||
|
|
03439d1bcc | ||
|
|
6e9c8a174f | ||
|
|
ef84d798b5 | ||
|
|
a76f40a4d0 | ||
|
|
537e25df93 | ||
|
|
0764b46a02 | ||
|
|
b0f983dfa9 | ||
|
|
a3c7269d9e | ||
|
|
dc94618c26 | ||
|
|
37d0daee59 | ||
|
|
9225b61225 | ||
|
|
a0a8d0186c | ||
|
|
11dab6680d | ||
|
|
b431600d32 | ||
|
|
fb79ccaa55 | ||
|
|
fa3966e1f7 | ||
|
|
81c81ff4aa | ||
|
|
788406c217 | ||
|
|
49a272d6c0 | ||
|
|
0fcdf47545 | ||
|
|
65ce948bb0 | ||
|
|
5d6014d496 | ||
|
|
6e48988118 | ||
|
|
7d56dca80b | ||
|
|
eb0c9ac1e6 | ||
|
|
752bd501bc | ||
|
|
542ec7b78b | ||
|
|
218855eebf | ||
|
|
f4a449b4ab | ||
|
|
8ce77880bd | ||
|
|
ac70fe2848 | ||
|
|
7ff174cf66 | ||
|
|
a419d831a4 | ||
|
|
7ad04f4535 | ||
|
|
440f553de0 | ||
|
|
c29ff995b3 | ||
|
|
dee71b652d | ||
|
|
7a76cac80c | ||
|
|
3e0ecea0c9 | ||
|
|
60846a4495 | ||
|
|
15eba12e91 | ||
|
|
98e5adfba8 | ||
|
|
422514e907 | ||
|
|
b2c7c7f55b | ||
|
|
82097d622b | ||
|
|
e72019fa55 | ||
|
|
38711f0a29 | ||
|
|
be6635c2a3 | ||
|
|
d298ab7486 | ||
|
|
c912542223 | ||
|
|
bf5d50e8a9 | ||
|
|
4a5cfaf929 | ||
|
|
0ede9d6d93 | ||
|
|
1bf4bf1f30 | ||
|
|
710d522e06 | ||
|
|
ba4f4af29b | ||
|
|
2f25f47d0b | ||
|
|
59c6003fe1 | ||
|
|
4b0cd22bf7 | ||
|
|
3aca142770 | ||
|
|
256990b95c | ||
|
|
537ad1eff3 | ||
|
|
2217a27757 | ||
|
|
55abe2ee75 | ||
|
|
70825d3425 | ||
|
|
7f1bb73b75 | ||
|
|
8b362a4165 | ||
|
|
25eedd187e | ||
|
|
ff9f29d12d | ||
|
|
42968d4c15 | ||
|
|
ca2083c858 | ||
|
|
de0ff4002e | ||
|
|
d0dc206ffc | ||
|
|
02d2ba2b6b | ||
|
|
904211dba5 | ||
|
|
1d374db0f3 | ||
|
|
e0249493d0 | ||
|
|
47f067e38e | ||
|
|
3b36d61698 | ||
|
|
60ecddfce4 | ||
|
|
cc8b771eb4 | ||
|
|
f503b7f517 | ||
|
|
639ac9549f | ||
|
|
839098c884 | ||
|
|
d8495ab08f | ||
|
|
53a314af65 | ||
|
|
2d0c4f5bd8 | ||
|
|
94a3575e2c | ||
|
|
aca661583d | ||
|
|
ca79fcb7e9 | ||
|
|
04bfa0e6ba | ||
|
|
3f8059baad | ||
|
|
b803d5336e | ||
|
|
7b72fcaee1 | ||
|
|
c56bdb84d2 | ||
|
|
ccc0d16b52 | ||
|
|
f38e3624cf | ||
|
|
8586fa8ce0 | ||
|
|
a5444e08ab | ||
|
|
f9e584e13c | ||
|
|
c795dfe96b | ||
|
|
a528a411da | ||
|
|
d96b8942e7 | ||
|
|
a31a615da6 | ||
|
|
e177bf54e2 | ||
|
|
ce55f0d8f4 | ||
|
|
320b76aec4 | ||
|
|
5d86bcce6d | ||
|
|
0b9f23bd4a | ||
|
|
526251397e |
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)
|
||||
|
||||
+2
-11
@@ -93,30 +93,24 @@
|
||||
"Template": false,
|
||||
"$": false,
|
||||
"_": false,
|
||||
"autosize": false,
|
||||
"Avatar": true,
|
||||
"Avatars": true,
|
||||
"BlazeComponent": false,
|
||||
|
||||
|
||||
"CollectionHooks": false,
|
||||
"DocHead": false,
|
||||
"ESSearchResults": false,
|
||||
"FastRender": false,
|
||||
"FlowRouter": false,
|
||||
"FS": false,
|
||||
"getSlug": false,
|
||||
"Migrations": false,
|
||||
"moment": false,
|
||||
"Mousetrap": false,
|
||||
"Picker": false,
|
||||
"Presence": true,
|
||||
"presences": true,
|
||||
"Ps": true,
|
||||
"ReactiveTabs": false,
|
||||
"Restivus": false,
|
||||
"SimpleSchema": false,
|
||||
"SubsManager": false,
|
||||
"T9n": false,
|
||||
"T9n": false,
|
||||
"TAPi18n": false,
|
||||
"AccountsTemplates": true,
|
||||
"Activities": true,
|
||||
@@ -133,13 +127,11 @@
|
||||
"CSSEvents": true,
|
||||
"EscapeActions": true,
|
||||
"Filter": true,
|
||||
"Mixins": true,
|
||||
"Modal": true,
|
||||
"MultiSelection": true,
|
||||
"Popup": true,
|
||||
"Sidebar": true,
|
||||
"Utils": true,
|
||||
"InlinedForm": true,
|
||||
"UnsavedEdits": true,
|
||||
"Notifications": true,
|
||||
"allowIsBoardAdmin": true,
|
||||
@@ -152,7 +144,6 @@
|
||||
"Checklists": true,
|
||||
"Settings": true,
|
||||
"InvitationCodes": true,
|
||||
"JsonRoutes": true,
|
||||
"Authentication": true,
|
||||
"Integrations": true,
|
||||
"HTTP": true,
|
||||
|
||||
@@ -13,32 +13,6 @@ body:
|
||||
description: A clear and concise description of what the bug is. Mention versions of WeKan, Node.js, database name and version, frontend webserver version like Caddy etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform / Installation Method
|
||||
options:
|
||||
- Snap Stable
|
||||
- Snap Candidate
|
||||
- Docker
|
||||
- Sandstorm
|
||||
- Source (Meteor)
|
||||
- Windows
|
||||
- Mac
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: CPU
|
||||
attributes:
|
||||
label: CPU
|
||||
options:
|
||||
- amd64
|
||||
- arm64
|
||||
- s390x
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
@@ -66,4 +40,3 @@ body:
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context, anonymized screenshots or GIF video about the bug, and screenshot mockups about how it should work.
|
||||
|
||||
@@ -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@5e57cd118135c172c3672efd75eb46360885c0ef
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -48,14 +48,14 @@ jobs:
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
name: Release All Platforms
|
||||
|
||||
# Triggered when maintainer pushes a version tag, e.g.: git tag v8.43 && git push --tags
|
||||
# Does not work:
|
||||
# Required GitHub Secrets:
|
||||
# QUAY_USERNAME - Quay.io username
|
||||
# QUAY_PASSWORD - Quay.io password or token
|
||||
# DOCKERHUB_USERNAME - Docker Hub username
|
||||
# DOCKERHUB_TOKEN - Docker Hub access token
|
||||
# SNAP_STORE_LOGIN - Snapcraft store credentials: run 'snapcraft export-login --snaps wekan --channels stable -' and base64-encode the output
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: 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
|
||||
|
||||
- name: Extract version from tag
|
||||
id: version
|
||||
run: echo "version=${GITHUB_REF_NAME#v}" >> $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: 24.14.1 (ABI 143).
|
||||
build-amd64:
|
||||
needs: prepare
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 180
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- 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 Node 24.14.1 — the exact version bundled by Meteor 3.5-beta.7.
|
||||
# Using the same version for build and npm rebuild ensures consistent native module ABI.
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24.14.1'
|
||||
|
||||
- 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/bundler-cache
|
||||
meteor npm install
|
||||
meteor build .build --directory
|
||||
|
||||
- name: Install dependencies in bundle
|
||||
run: |
|
||||
cd .build/bundle/programs/server
|
||||
meteor npm install
|
||||
|
||||
- name: Create amd64 zip
|
||||
run: |
|
||||
cd .build
|
||||
zip -r wekan-${VERSION}-amd64.zip bundle
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bundle-amd64
|
||||
path: .build/wekan-${{ needs.prepare.outputs.version }}-amd64.zip
|
||||
retention-days: 1
|
||||
|
||||
# ─── 3. Build arm64 bundle (rebuild native modules natively) ─────────────────
|
||||
# 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 }}
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential g++ make python3 curl unzip zip
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24.14.1'
|
||||
|
||||
- name: Download amd64 bundle
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: bundle-amd64
|
||||
|
||||
- name: Rebuild native modules for arm64
|
||||
run: |
|
||||
unzip wekan-${VERSION}-amd64.zip
|
||||
cd bundle/programs/server
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
cd ../..
|
||||
|
||||
- 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:24.14.1-slim (s390x variant) for npm rebuild.
|
||||
# node:24.14.1-slim is pinned to match Meteor 3.5-beta.7's bundled Node.js version exactly.
|
||||
build-s390x:
|
||||
needs: [prepare, build-amd64]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
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: Rebuild native modules for s390x in container
|
||||
run: |
|
||||
unzip wekan-${VERSION}-amd64.zip
|
||||
rm -rf bundle/programs/server/node_modules
|
||||
# node:24.14.1-slim s390x — same Node.js version as Meteor 3.5-beta.7 bundled
|
||||
docker run --rm --platform linux/s390x \
|
||||
-v "$(pwd)/bundle:/bundle" \
|
||||
-w /bundle/programs/server \
|
||||
node:24.14.1-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:24.14.1-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 }}
|
||||
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: Rebuild native modules for ppc64le in container
|
||||
run: |
|
||||
unzip wekan-${VERSION}-amd64.zip
|
||||
rm -rf bundle/programs/server/node_modules
|
||||
# node:24.14.1-slim ppc64le — same Node.js version as Meteor 3.5-beta.7 bundled
|
||||
docker run --rm --platform linux/ppc64le \
|
||||
-v "$(pwd)/bundle:/bundle" \
|
||||
-w /bundle/programs/server \
|
||||
node:24.14.1-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 (rebuild native modules via QEMU container) ──────
|
||||
# Meteor 3.5-beta.7 does NOT support arm/v7 (armhf). Installer explicitly rejects it.
|
||||
# GitHub Actions has no native arm/v7 runners.
|
||||
# Solution: QEMU emulation + node:24.14.1-slim (arm/v7 variant) for npm rebuild.
|
||||
# arm/v7 under QEMU is slowest — timeout set to 120 min.
|
||||
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
|
||||
# node:24.14.1-slim arm/v7 — same Node.js version as Meteor 3.5-beta.7 bundled
|
||||
docker run --rm --platform linux/arm/v7 \
|
||||
-v "$(pwd)/bundle:/bundle" \
|
||||
-w /bundle/programs/server \
|
||||
node:24.14.1-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: '24.14.1'
|
||||
|
||||
## 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: Rebuild native modules for Windows
|
||||
shell: cmd
|
||||
run: |
|
||||
7z x wekan-%VERSION%-amd64.zip
|
||||
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-armhf, 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@v2
|
||||
with:
|
||||
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 }}-armhf.zip
|
||||
wekan-${{ needs.prepare.outputs.version }}-win64.zip
|
||||
|
||||
# ─── 7. Build and push multi-platform Docker image ──────────────────────────
|
||||
#docker:
|
||||
# needs: [prepare, release]
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# VERSION: ${{ needs.prepare.outputs.version }}
|
||||
# steps:
|
||||
# - uses: actions/checkout@v6
|
||||
#
|
||||
# - name: Set up QEMU
|
||||
# uses: docker/setup-qemu-action@v4
|
||||
#
|
||||
# - name: Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@v3
|
||||
#
|
||||
# - name: Log in to Docker Hub
|
||||
# uses: docker/login-action@v4
|
||||
# with:
|
||||
# username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
# password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
#
|
||||
# - name: Log in to Quay.io
|
||||
# uses: docker/login-action@v4
|
||||
# with:
|
||||
# registry: quay.io
|
||||
# username: ${{ secrets.QUAY_USERNAME }}
|
||||
# password: ${{ secrets.QUAY_PASSWORD }}
|
||||
#
|
||||
# - name: Log in to GitHub Container Registry
|
||||
# uses: docker/login-action@v4
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.actor }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
#
|
||||
# - name: Build and push Docker image to all registries
|
||||
# uses: docker/build-push-action@v7
|
||||
# with:
|
||||
# context: .
|
||||
# platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/ppc64le,linux/s390x
|
||||
# push: true
|
||||
# build-args: VERSION=${{ needs.prepare.outputs.version }}
|
||||
# tags: |
|
||||
# #wekanteam/wekan:v${{ needs.prepare.outputs.version }}
|
||||
# #wekanteam/wekan:latest
|
||||
# quay.io/wekan/wekan:v${{ needs.prepare.outputs.version }}
|
||||
# quay.io/wekan/wekan:latest
|
||||
# ghcr.io/wekan/wekan:v${{ needs.prepare.outputs.version }}
|
||||
# ghcr.io/wekan/wekan:latest
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
|
||||
# ─── 8. Build and publish snap packages (all architectures via Launchpad) ────
|
||||
# Uses snapcraft remote-build on Launchpad's native builders (more reliable
|
||||
# than QEMU for snap builds). Each architecture bundles its own database:
|
||||
# amd64, arm64 — MongoDB 7.x prebuilt native binary
|
||||
# armhf, s390x, ppc64el — MongoDB 7.x amd64 binary run via qemu-x86_64-static
|
||||
# (no prebuilt MongoDB binaries exist for these arches)
|
||||
#snap:
|
||||
# needs: [prepare, release]
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# VERSION: ${{ needs.prepare.outputs.version }}
|
||||
# steps:
|
||||
# - uses: actions/checkout@v6
|
||||
#
|
||||
# - name: Install snapcraft
|
||||
# run: sudo snap install snapcraft --classic
|
||||
#
|
||||
# - name: Remote-build snap for all architectures via Launchpad
|
||||
# env:
|
||||
# SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_STORE_LOGIN }}
|
||||
# run: |
|
||||
# snapcraft remote-build \
|
||||
# --build-for=amd64,arm64,armhf,s390x,ppc64el \
|
||||
# --launchpad-accept-public-upload
|
||||
#
|
||||
# - name: Upload all snap files as artifacts
|
||||
# uses: actions/upload-artifact@v7
|
||||
# with:
|
||||
# name: snaps-all-arches
|
||||
# path: "*.snap"
|
||||
# retention-days: 30
|
||||
#
|
||||
# # Publish to edge — NOT stable.
|
||||
# # Stable channel holds the old WeKan version until database migrations are ready.
|
||||
# # Promote to stable manually via the Snap Store dashboard when migrations exist.
|
||||
# - name: Publish all snaps to Snap Store (candidate, beta, edge)
|
||||
# env:
|
||||
# SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_STORE_LOGIN }}
|
||||
# run: |
|
||||
# for snap_file in *.snap; do
|
||||
# echo "Publishing $snap_file ..."
|
||||
# snapcraft upload --release=edge "$snap_file"
|
||||
# done
|
||||
#
|
||||
# - name: Upload all snaps to GitHub Release
|
||||
# uses: softprops/action-gh-release@v2
|
||||
# with:
|
||||
# files: "*.snap"
|
||||
@@ -124,19 +124,24 @@ jobs:
|
||||
restore-keys: |
|
||||
v1-meteor_build_cache-
|
||||
|
||||
- name: Setup meteor
|
||||
uses: meteorengineer/setup-meteor@v2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
meteor-release: '2.2'
|
||||
node-version: '24.14.1'
|
||||
|
||||
- name: Setup meteor
|
||||
uses: meteorengineer/setup-meteor@v3
|
||||
with:
|
||||
meteor-release: '3.5-beta.7'
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: meteor npm ci
|
||||
run: meteor npm install
|
||||
|
||||
- name: Run Tests
|
||||
run: sh ./test-wekan.sh -cv
|
||||
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage-folder
|
||||
path: .coverage/
|
||||
@@ -150,7 +155,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download coverage
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: coverage-folder
|
||||
path: .coverage/
|
||||
|
||||
+14
@@ -35,7 +35,21 @@ 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
|
||||
+41
-65
@@ -3,30 +3,36 @@
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
meteor-base@1.5.1
|
||||
meteor-base@1.5.2
|
||||
|
||||
# Build system
|
||||
ecmascript@0.16.8
|
||||
standard-minifier-js@2.8.1
|
||||
mquandalle:jade
|
||||
coffeescript@2.4.1!
|
||||
ecmascript@0.17.0
|
||||
standard-minifier-js@3.2.0
|
||||
rspack@1.0.0
|
||||
|
||||
# Polyfills
|
||||
es5-shim@4.8.0
|
||||
blaze@3.0.2
|
||||
|
||||
# Collections
|
||||
aldeed:collection2
|
||||
cottz:publish-relations
|
||||
dburles:collection-helpers
|
||||
idmontie:migrations
|
||||
mongo@1.16.8
|
||||
mquandalle:collection-mutations
|
||||
aldeed:schema-index@4.0.0-beta.5
|
||||
reywood:publish-composite
|
||||
mongo@2.3.0-beta350.7
|
||||
|
||||
# The holy trifecta - this is a replacement for subs-manager
|
||||
# for IndexedDB persistence and auto-sync
|
||||
jam:offline
|
||||
# for optimistic methods with offline queuing
|
||||
jam:method
|
||||
# global subscription caching
|
||||
jam:pub-sub
|
||||
|
||||
# Account system
|
||||
accounts-password@2.4.0
|
||||
useraccounts:core
|
||||
useraccounts:unstyled
|
||||
simple:rest-accounts-password
|
||||
accounts-password@3.2.3-beta350.7
|
||||
communitypackages:core
|
||||
communitypackages:unstyled
|
||||
communitypackages:flow-routing
|
||||
# Routing
|
||||
ostrio:flow-router-extra
|
||||
wekan-ldap
|
||||
wekan-accounts-cas
|
||||
wekan-accounts-sandstorm
|
||||
@@ -35,59 +41,29 @@ wekan-oidc
|
||||
wekan-accounts-oidc
|
||||
|
||||
# Utilities
|
||||
check@1.3.2
|
||||
jquery@3.0.0!
|
||||
random@1.2.1
|
||||
reactive-dict@1.3.1
|
||||
session@1.2.1
|
||||
tracker@1.3.3
|
||||
underscore@1.0.13
|
||||
audit-argument-checks@1.0.7
|
||||
kadira:dochead
|
||||
mquandalle:autofocus
|
||||
ongoworks:speakingurl
|
||||
raix:handlebar-helpers
|
||||
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.2
|
||||
tracker@1.3.4
|
||||
audit-argument-checks@1.0.8
|
||||
|
||||
# UI components
|
||||
ostrio:i18n
|
||||
reactive-var@1.0.12
|
||||
mousetrap:mousetrap
|
||||
mquandalle:jquery-textcomplete
|
||||
mquandalle:mousetrap-bindglobal
|
||||
templates:tabs
|
||||
meteor-autosize
|
||||
shell-server@0.5.0
|
||||
email@2.2.5
|
||||
dynamic-import@0.7.3
|
||||
msavin:usercache
|
||||
# Keep stylus in 1.1.0, because building v2 takes extra 52 minutes.
|
||||
meteorhacks:subs-manager
|
||||
meteorhacks:aggregate@1.3.0
|
||||
reactive-var@1.0.13
|
||||
shell-server@0.7.0
|
||||
email@3.2.0-beta350.7
|
||||
dynamic-import@0.7.4
|
||||
wekan-markdown
|
||||
konecty:mongo-counter
|
||||
percolate:synced-cron
|
||||
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
|
||||
matb33:collection-hooks
|
||||
simple:json-routes
|
||||
spacebars
|
||||
service-configuration@1.3.2
|
||||
communitypackages:picker
|
||||
minifier-css@1.6.4
|
||||
blaze
|
||||
peerlibrary:blaze-components
|
||||
ejson@1.1.3
|
||||
logging@1.3.3
|
||||
service-configuration@1.3.5
|
||||
minifier-css@2.0.1
|
||||
ejson@1.2.0-beta350.7
|
||||
logging@1.3.6
|
||||
wekan-fullcalendar
|
||||
momentjs:moment@2.29.3
|
||||
# wekan-fontawesome
|
||||
|
||||
useraccounts:flow-routing-extra
|
||||
ostrio:flow-router-extra
|
||||
wekan-fontawesome
|
||||
+1
-1
@@ -1 +1 @@
|
||||
METEOR@2.16
|
||||
METEOR@3.5-beta.7
|
||||
+113
-149
@@ -1,157 +1,121 @@
|
||||
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
|
||||
coffeescript@2.7.0
|
||||
coffeescript-compiler@2.4.1
|
||||
communitypackages:picker@1.1.1
|
||||
cottz:publish-relations@2.0.8
|
||||
dburles:collection-helpers@1.1.0
|
||||
ddp@1.4.1
|
||||
ddp-client@2.6.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
|
||||
idmontie:migrations@1.0.3
|
||||
inter-process-messaging@0.1.1
|
||||
jquery@3.0.0
|
||||
kadira:dochead@1.5.0
|
||||
konecty:mongo-counter@0.0.5_3
|
||||
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:aggregate@1.3.0
|
||||
meteorhacks:collection-utils@1.2.0
|
||||
meteorhacks:picker@1.0.3
|
||||
meteorhacks:subs-manager@1.6.4
|
||||
accounts-base@3.3.0-beta350.7
|
||||
accounts-oauth@1.4.6
|
||||
accounts-password@3.2.3-beta350.7
|
||||
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.13.0
|
||||
babel-runtime@1.5.2
|
||||
base64@1.0.13
|
||||
binary-heap@1.0.12
|
||||
blaze@3.0.2
|
||||
blaze-tools@2.0.0
|
||||
boilerplate-generator@2.1.0
|
||||
caching-compiler@2.0.1
|
||||
caching-html-compiler@2.0.0
|
||||
callback-hook@1.6.1
|
||||
check@1.5.0
|
||||
communitypackages:core@2.0.3
|
||||
communitypackages:flow-routing@2.1.0
|
||||
communitypackages:unstyled@2.0.1
|
||||
core-runtime@1.0.0
|
||||
ddp@1.4.2
|
||||
ddp-client@3.2.0-beta350.7
|
||||
ddp-common@1.4.4
|
||||
ddp-rate-limiter@1.3.0-beta350.7
|
||||
ddp-server@3.2.0-beta350.7
|
||||
diff-sequence@1.1.3
|
||||
dynamic-import@0.7.4
|
||||
ecmascript@0.17.0
|
||||
ecmascript-runtime@0.8.3
|
||||
ecmascript-runtime-client@0.12.3
|
||||
ecmascript-runtime-server@0.11.1
|
||||
ejson@1.2.0-beta350.7
|
||||
email@3.2.0-beta350.7
|
||||
es5-shim@4.8.1
|
||||
facts-base@1.0.2
|
||||
fetch@0.1.6
|
||||
geojson-utils@1.0.12
|
||||
hot-code-push@1.0.5
|
||||
html-tools@2.0.0
|
||||
htmljs@2.0.1
|
||||
http@1.4.4
|
||||
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.2.0
|
||||
meteor-base@1.5.2
|
||||
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
|
||||
momentjs:moment@2.29.3
|
||||
mongo@1.16.10
|
||||
mongo-decimal@0.1.3
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.8
|
||||
mongo-livedata@1.0.12
|
||||
mousetrap:mousetrap@1.4.6_1
|
||||
mquandalle:autofocus@1.0.0
|
||||
mquandalle:collection-mutations@0.1.0
|
||||
mquandalle:jade@0.4.9
|
||||
mquandalle:jade-compiler@0.4.5
|
||||
mquandalle:jquery-textcomplete@0.8.0_1
|
||||
mquandalle:mousetrap-bindglobal@0.0.1
|
||||
msavin:usercache@1.8.0
|
||||
npm-mongo@4.17.2
|
||||
oauth@2.2.1
|
||||
oauth2@1.3.2
|
||||
observe-sequence@1.0.21
|
||||
ongoworks:speakingurl@1.1.0
|
||||
ordered-dict@1.1.0
|
||||
ostrio:cookies@2.7.2
|
||||
minifier-css@2.0.1
|
||||
minifier-js@3.1.0
|
||||
minimongo@2.1.0-beta350.7
|
||||
modern-browsers@0.2.3
|
||||
modules@0.20.3
|
||||
modules-runtime@0.13.2
|
||||
mongo@2.3.0-beta350.7
|
||||
mongo-decimal@0.2.0
|
||||
mongo-dev-server@1.1.1
|
||||
mongo-id@1.0.9
|
||||
npm-mongo@6.16.1
|
||||
oauth@3.0.2
|
||||
oauth2@1.3.3
|
||||
observe-sequence@2.0.0
|
||||
ordered-dict@1.2.0
|
||||
ostrio:cookies@2.9.1
|
||||
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.12.1
|
||||
ostrio:i18n@3.2.1
|
||||
peerlibrary:assert@0.3.0
|
||||
peerlibrary:base-component@0.17.1
|
||||
peerlibrary:blaze-components@0.23.0
|
||||
peerlibrary:computed-field@0.10.0
|
||||
peerlibrary:data-lookup@0.3.0
|
||||
peerlibrary:reactive-field@0.6.0
|
||||
percolate:synced-cron@1.5.2
|
||||
promise@0.12.2
|
||||
raix:eventemitter@0.1.3
|
||||
raix:handlebar-helpers@0.2.5
|
||||
random@1.2.1
|
||||
rate-limit@1.1.1
|
||||
react-fast-refresh@0.2.8
|
||||
reactive-dict@1.3.1
|
||||
reactive-var@1.0.12
|
||||
reload@1.3.1
|
||||
retry@1.1.0
|
||||
routepolicy@1.1.1
|
||||
service-configuration@1.3.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
|
||||
templates:tabs@2.3.0
|
||||
templating@1.4.1
|
||||
templating-compiler@1.4.1
|
||||
templating-runtime@1.5.0
|
||||
templating-tools@1.2.2
|
||||
tracker@1.3.3
|
||||
typescript@4.9.5
|
||||
ui@1.0.13
|
||||
underscore@1.6.1
|
||||
url@1.3.2
|
||||
useraccounts:core@1.16.2
|
||||
useraccounts:flow-routing-extra@1.1.0
|
||||
useraccounts:unstyled@1.14.2
|
||||
webapp@1.13.8
|
||||
webapp-hashing@1.1.1
|
||||
wekan-accounts-cas@0.1.0
|
||||
wekan-accounts-lockout@1.0.0
|
||||
promise@1.0.0
|
||||
quave:synced-cron@2.4.0
|
||||
raix:eventemitter@1.0.0
|
||||
random@1.2.2
|
||||
rate-limit@1.2.0-beta350.7
|
||||
react-fast-refresh@0.3.0
|
||||
reactive-dict@1.3.2
|
||||
reactive-var@1.0.13
|
||||
reload@1.3.2
|
||||
retry@1.1.1
|
||||
reywood:publish-composite@1.9.0
|
||||
routepolicy@1.1.2
|
||||
rspack@1.0.0
|
||||
service-configuration@1.3.5
|
||||
session@1.2.2
|
||||
sha@1.0.10
|
||||
shell-server@0.7.0
|
||||
socket-stream-client@0.7.0-beta350.7
|
||||
spacebars@2.0.0
|
||||
spacebars-compiler@2.0.0
|
||||
standard-minifier-js@3.2.0
|
||||
templating@1.4.4
|
||||
templating-compiler@2.0.0
|
||||
templating-runtime@2.0.1
|
||||
templating-tools@2.0.0
|
||||
tools-core@1.0.0
|
||||
tracker@1.3.4
|
||||
typescript@5.9.3
|
||||
url@1.3.5
|
||||
webapp@2.2.0-beta350.7
|
||||
webapp-hashing@1.1.2
|
||||
wekan-accounts-cas@0.2.0
|
||||
wekan-accounts-lockout@1.1.0
|
||||
wekan-accounts-oidc@1.0.10
|
||||
wekan-accounts-sandstorm@0.8.0
|
||||
wekan-fullcalendar@3.10.5
|
||||
wekan-ldap@0.0.2
|
||||
wekan-accounts-sandstorm@0.9.0
|
||||
wekan-fontawesome@6.4.2
|
||||
wekan-fullcalendar@5.11.5
|
||||
wekan-ldap@0.1.0
|
||||
wekan-markdown@1.0.9
|
||||
wekan-oidc@1.0.12
|
||||
yasaricli:slugify@0.0.7
|
||||
wekan-oidc@1.1.0
|
||||
zodern:types@1.0.13
|
||||
@@ -0,0 +1 @@
|
||||
npm-packages/
|
||||
Vendored
+4
-1
@@ -9,5 +9,8 @@
|
||||
"TERM": "xterm-256color"
|
||||
},
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"terminal.integrated.shellArgs.linux": ["-l"]
|
||||
"terminal.integrated.shellArgs.linux": [
|
||||
"-l"
|
||||
],
|
||||
"files.simpleDialog.enable": true
|
||||
}
|
||||
+1000
-21
File diff suppressed because it is too large.
Load diff
+16
-15
@@ -6,24 +6,25 @@ Not paid:
|
||||
- Security reports, see [SECURITY.md](SECURITY.md)
|
||||
- Pull requests
|
||||
- xet7 checking pull requests
|
||||
- Public Community Support
|
||||
- This is because full-featured WeKan FLOSS kanban with MIT license is free to use
|
||||
also for Commercial Use without any payment, with full source code at
|
||||
https://github.com/wekan/wekan for all platforms at https://wekan.fi/install/ .
|
||||
There is nothing proprietary. WeKan is NOT Big Tech.
|
||||
WeKan is maintained by https://github.com/xet7 at Finland https://wekan.fi .
|
||||
- Public FLOSS Developer Community Support:
|
||||
- https://github.com/wekan/wekan/issues
|
||||
|
||||
Paid by customers of WeKan Team:
|
||||
Paid by customers of WeKan to WeKan:
|
||||
|
||||
- Commercial Support at https://wekan.fi/commercial-support/
|
||||
- Support
|
||||
- Private Chat
|
||||
- Features
|
||||
- Fixes
|
||||
- Hosting
|
||||
- Optional Private Paid Commercial Support at https://wekan.fi/commercial-support/
|
||||
- Optional Paid Support/Features/Fixes/PrivateChat . All Paid Features and Fixes are added directly to FLOSS WeKan https://github.com/wekan/wekan
|
||||
- Optional Paid Official SaaS Hosting https://wekan.fi/saas/ , if you need full-featured hosting, that is more than free app https://wekan.fi/app/
|
||||
|
||||
## Contributing Security related
|
||||
|
||||
For responsible security disclosure, please follow this process:
|
||||
https://github.com/wekan/wekan/blob/main/SECURITY.md
|
||||
For responsible security disclosure, please follow this process: [SECURITY.md](SECURITY.md)
|
||||
|
||||
CVE Hall of Fame is at https://wekan.github.io/hall-of-fame/
|
||||
CVE Hall of Fame is at https://wekan.fi/hall-of-fame/
|
||||
|
||||
## Contributing to Documentation Wiki
|
||||
|
||||
@@ -33,7 +34,7 @@ and click send PR.
|
||||
|
||||
## Contributing code
|
||||
|
||||
[Building WeKan and sending PR](https://github.com/wekan/wekan/wiki/Emoji).
|
||||
[Building WeKan and sending PR](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md).
|
||||
|
||||
WeKan code contributors Hall of Fame is at ChangeLog, where
|
||||
GitHub usernames are mentioned with changes added:
|
||||
@@ -44,9 +45,9 @@ Changes can be like typo fixes, bugfixes, features, or anything else
|
||||
like for example at open GitHub issues https://github.com/wekan/wekan/issues .
|
||||
Closed issues are already fixed or implemented.
|
||||
|
||||
Also see other docs at wiki, for example:
|
||||
Also see other docs:
|
||||
|
||||
https://github.com/wekan/wekan/wiki/Developer-Documentation
|
||||
https://github.com/wekan/wekan/tree/main/docs/DeveloperDocs
|
||||
|
||||
Do not use code formatting or linting like eslist or prettier.
|
||||
|
||||
@@ -64,7 +65,7 @@ so that WeKan code will still work OK.
|
||||
## Contributing translations
|
||||
|
||||
Non-English translations are contributed only at
|
||||
https://transifex.com/wekan/wekan
|
||||
https://app.transifex.com/wekan/wekan
|
||||
|
||||
When adding new features, in your PR to
|
||||
https://github.com/wekan/wekan/pulls
|
||||
|
||||
+48
-114
@@ -4,28 +4,23 @@ LABEL org.opencontainers.image.ref.name="ubuntu"
|
||||
LABEL org.opencontainers.image.version="24.04"
|
||||
LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
|
||||
|
||||
# 2022-04-25:
|
||||
# - gyp does not yet work with Ubuntu 22.04 ubuntu:rolling,
|
||||
# so changing to 21.10. https://github.com/wekan/wekan/issues/4488
|
||||
|
||||
# 2021-09-18:
|
||||
# - Above Ubuntu base image copied from Docker Hub ubuntu:hirsute-20210825
|
||||
# to Quay to avoid Docker Hub rate limits.
|
||||
# TARGETARCH and TARGETVARIANT are automatically provided by Docker Buildx
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
ARG VERSION=8.78
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ENV BUILD_DEPS="apt-utils gnupg gosu wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
|
||||
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
|
||||
|
||||
ENV \
|
||||
DEBUG=false \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
METEOR_RELEASE=METEOR@2.14 \
|
||||
NODE_VERSION=v24.14.1 \
|
||||
METEOR_RELEASE=METEOR@3.5-beta.7 \
|
||||
USE_EDGE=false \
|
||||
METEOR_EDGE=1.5-beta.17 \
|
||||
NPM_VERSION=6.14.17 \
|
||||
FIBERS_VERSION=4.0.1 \
|
||||
ARCHITECTURE=linux-x64 \
|
||||
NPM_VERSION=11.11.0 \
|
||||
SRC_PATH=./ \
|
||||
WITH_API=true \
|
||||
MONGO_OPLOG_URL="" \
|
||||
RESULTS_PER_PAGE="" \
|
||||
DEFAULT_BOARD_ID="" \
|
||||
ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
|
||||
@@ -163,134 +158,73 @@ ENV \
|
||||
MONGO_PASSWORD_FILE="" \
|
||||
S3_SECRET_FILE=""
|
||||
|
||||
# NODE_OPTIONS="--max_old_space_size=4096"
|
||||
|
||||
#---------------------------------------------
|
||||
# == at docker-compose.yml: AUTOLOGIN WITH OIDC/OAUTH2 ====
|
||||
# https://github.com/wekan/wekan/wiki/autologin
|
||||
#- OIDC_REDIRECTION_ENABLED=true
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Copy the app to the image
|
||||
#COPY ${SRC_PATH} /home/wekan/app
|
||||
|
||||
# Install OS
|
||||
RUN <<EOR
|
||||
set -o xtrace
|
||||
|
||||
# Add non-root user wekan
|
||||
# Create Wekan user
|
||||
useradd --user-group --system --home-dir /home/wekan wekan
|
||||
# OS dependencies
|
||||
|
||||
# OS Updates
|
||||
apt-get update --assume-yes
|
||||
apt-get upgrade --assume-yes
|
||||
apt-get install --assume-yes --no-install-recommends ${BUILD_DEPS}
|
||||
|
||||
# Meteor installer doesn't work with the default tar binary, so using bsdtar while installing.
|
||||
# https://github.com/coreos/bugs/issues/1095#issuecomment-350574389
|
||||
# Multi-arch mapping: Docker TARGETARCH -> Node.js arch name + WeKan bundle name
|
||||
# arm/v7 uses TARGETARCH=arm; armhf has no MongoDB Community -> uses FerretDB
|
||||
case "${TARGETARCH}" in
|
||||
"amd64") NODE_ARCH="x64" WEKAN_ARCH="amd64" ;;
|
||||
"arm64") NODE_ARCH="arm64" WEKAN_ARCH="arm64" ;;
|
||||
"arm") NODE_ARCH="armv7l" WEKAN_ARCH="armhf" ;;
|
||||
"ppc64le") NODE_ARCH="ppc64le" WEKAN_ARCH="ppc64le" ;;
|
||||
"s390x") NODE_ARCH="s390x" WEKAN_ARCH="s390x" ;;
|
||||
*) echo "Unsupported architecture: ${TARGETARCH}"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Node.js Installation (official nodejs.org builds for Node 22)
|
||||
cd /tmp
|
||||
wget "https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz"
|
||||
wget "https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc"
|
||||
grep "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt.asc | shasum -a 256 -c -
|
||||
tar xzf "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" -C /usr/local --strip-components=1 --no-same-owner
|
||||
rm -f "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt.asc
|
||||
ln -s "/usr/local/bin/node" "/usr/local/bin/nodejs"
|
||||
|
||||
# NPM configuration
|
||||
npm install -g npm@${NPM_VERSION} --production
|
||||
chown --recursive wekan:wekan /home/wekan/
|
||||
|
||||
# Temporary Tar swap for Meteor bundle
|
||||
cp $(which tar) $(which tar)~
|
||||
ln -sf $(which bsdtar) $(which tar)
|
||||
|
||||
# Install NodeJS
|
||||
cd /tmp
|
||||
|
||||
# Download nodejs
|
||||
#wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz"
|
||||
wget "https://github.com/wekan/node-v14-esm/releases/download/v14.21.4/node-v14.21.4-linux-x64.tar.gz"
|
||||
#wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt"
|
||||
wget "https://github.com/wekan/node-v14-esm/releases/download/v14.21.4/SHASUMS256.txt"
|
||||
|
||||
# Verify nodejs authenticity
|
||||
#grep "node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz" "SHASUMS256.txt" | shasum -a 256 -c -
|
||||
grep "node-v14.21.4-linux-x64.tar.gz" "SHASUMS256.txt" | shasum -a 256 -c -
|
||||
rm -f "SHASUMS256.txt"
|
||||
|
||||
# Install Node
|
||||
#tar xzf "node-$NODE_VERSION-$ARCHITECTURE.tar.gz" -C /usr/local --strip-components=1 --no-same-owner
|
||||
tar xzf "node-v14.21.4-linux-x64.tar.gz" -C /usr/local --strip-components=1 --no-same-owner
|
||||
#rm "node-$NODE_VERSION-$ARCHITECTURE.tar.gz" "SHASUMS256.txt"
|
||||
rm "node-v14.21.4-linux-x64.tar.gz" "SHASUMS256.txt"
|
||||
ln -s "/usr/local/bin/node" "/usr/local/bin/nodejs"
|
||||
#mkdir -p "/opt/nodejs/lib/node_modules/fibers/.node-gyp" "/root/.node-gyp/${NODE_VERSION} /home/wekan/.config"
|
||||
#mkdir -p "/opt/nodejs/lib/node_modules/fibers/.node-gyp" "/root/.node-gyp/v14.21.4 /home/wekan/.config"
|
||||
|
||||
# Install node dependencies
|
||||
#npm install -g npm@${NPM_VERSION} --production
|
||||
npm install -g npm@$6.14.17 --production
|
||||
chown --recursive wekan:wekan /home/wekan/.config
|
||||
|
||||
# Install Meteor
|
||||
cd /home/wekan
|
||||
chown --recursive wekan:wekan /home/wekan
|
||||
echo "Starting meteor ${METEOR_RELEASE} installation... \n"
|
||||
#gosu wekan:wekan curl https://install.meteor.com/ | /bin/sh
|
||||
# Specify Meteor version 2.14 to be compatible: https://github.com/wekan/wekan/pull/5816/files
|
||||
#gosu wekan:wekan npm -g install meteor@2.14 --unsafe-perm
|
||||
#mv /root/.meteor /home/wekan/
|
||||
#chown --recursive wekan:wekan /home/wekan/.meteor
|
||||
|
||||
#sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' /home/wekan/app/packages/meteor-useraccounts-core/package.js
|
||||
#cd /home/wekan/.meteor
|
||||
#gosu wekan:wekan /home/wekan/.meteor/meteor -- help
|
||||
|
||||
# Build app (Production)
|
||||
#cd /home/wekan/app
|
||||
# WeKan Bundle Installation
|
||||
mkdir -p /home/wekan/app
|
||||
cd /home/wekan/app
|
||||
#mkdir -p /home/wekan/.npm
|
||||
#chown --recursive wekan:wekan /home/wekan/.npm
|
||||
#chmod u+w *.json
|
||||
#gosu wekan:wekan meteor npm install --production
|
||||
#gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build
|
||||
#cd /home/wekan/app_build/bundle/programs/server/
|
||||
#chmod u+w *.json
|
||||
#gosu wekan:wekan meteor npm install --production
|
||||
#cd node_modules/fibers
|
||||
#node build.js
|
||||
#cd ../..
|
||||
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
||||
#rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
|
||||
#mv /home/wekan/app_build/bundle /build
|
||||
wget "https://github.com/wekan/wekan/releases/download/v8.22/wekan-8.22-amd64.zip"
|
||||
unzip wekan-8.22-amd64.zip
|
||||
rm wekan-8.22-amd64.zip
|
||||
wget "https://github.com/wekan/wekan/releases/download/v${VERSION}/wekan-${VERSION}-${WEKAN_ARCH}.zip"
|
||||
unzip "wekan-${VERSION}-${WEKAN_ARCH}.zip"
|
||||
rm "wekan-${VERSION}-${WEKAN_ARCH}.zip"
|
||||
npm install --production --no-audit --prefix ./bundle/programs/server
|
||||
mv /home/wekan/app/bundle /build
|
||||
|
||||
# Put back the original tar
|
||||
# Restore original tar
|
||||
mv $(which tar)~ $(which tar)
|
||||
|
||||
# Cleanup
|
||||
apt-get remove --purge --assume-yes ${BUILD_DEPS}
|
||||
#npm uninstall -g api2html
|
||||
apt-get autoremove --assume-yes
|
||||
apt-get clean --assume-yes
|
||||
rm -Rf /tmp/*
|
||||
rm -Rf /var/lib/apt/lists/*
|
||||
rm -Rf /var/cache/apt
|
||||
rm -Rf /var/lib/apt/lists
|
||||
rm -Rf /home/wekan/app_build
|
||||
rm -Rf /home/wekan/app
|
||||
rm -Rf /home/wekan/.meteor
|
||||
|
||||
mkdir /data
|
||||
chown wekan --recursive /data
|
||||
mkdir -p /data
|
||||
chown wekan:wekan --recursive /data
|
||||
EOR
|
||||
|
||||
USER wekan
|
||||
|
||||
ENV PORT=8080
|
||||
EXPOSE $PORT
|
||||
|
||||
STOPSIGNAL SIGKILL
|
||||
WORKDIR /home/wekan/app
|
||||
WORKDIR /build
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
# https://github.com/wekan/wekan/issues/3585#issuecomment-1021522132
|
||||
# Add more Node heap:
|
||||
# NODE_OPTIONS="--max_old_space_size=4096"
|
||||
# Add more stack:
|
||||
# bash -c "ulimit -s 65500; exec node --stack-size=65500 main.js"
|
||||
#---------------------------------------------------------------------
|
||||
#
|
||||
# CMD ["node", "/build/main.js"]
|
||||
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 /build/main.js"]
|
||||
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 --max-old-space-size=8192 /build/main.js"]
|
||||
CMD ["bash", "-c", "ulimit -s 65500; exec node /build/main.js"]
|
||||
CMD ["bash", "-c", "ulimit -s 65500; exec node main.js"]
|
||||
@@ -1,98 +0,0 @@
|
||||
FROM arm64v8/ubuntu:23.04 AS builder
|
||||
#FROM amd64/alpine:latest AS builder
|
||||
|
||||
# Set the environment variables for builder
|
||||
ENV QEMU_VERSION=v7.2.0-1 \
|
||||
QEMU_ARCHITECTURE=aarch64 \
|
||||
NODE_ARCHITECTURE=linux-arm64 \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
WEKAN_VERSION=latest \
|
||||
WEKAN_ARCHITECTURE=arm64
|
||||
|
||||
# Install dependencies
|
||||
#RUN apk update && apk add ca-certificates outils-sha1 && \
|
||||
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||
RUN apt update && apt install ca-certificates wget unzip -y && \
|
||||
\
|
||||
# Download qemu static for our architecture
|
||||
wget https://github.com/multiarch/qemu-user-static/releases/download/${QEMU_VERSION}/qemu-${QEMU_ARCHITECTURE}-static.tar.gz -O - | tar -xz && \
|
||||
\
|
||||
# Download wekan and shasum
|
||||
wget https://releases.wekan.team/raspi3/wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
|
||||
wget https://releases.wekan.team/raspi3/SHA256SUMS.txt && \
|
||||
# Verify wekan
|
||||
grep wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip SHA256SUMS.txt | sha256sum -c - && \
|
||||
\
|
||||
# Unzip wekan
|
||||
unzip wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
|
||||
\
|
||||
# Download node and shasums
|
||||
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
|
||||
\
|
||||
# Verify nodejs authenticity
|
||||
grep node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz SHASUMS256.txt | sha256sum -c - && \
|
||||
\
|
||||
# Extract node and remove tar.gz
|
||||
tar xvzf node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz
|
||||
|
||||
# Build wekan dockerfile
|
||||
FROM --platform=linux/arm64 arm64v8/ubuntu:23.04
|
||||
LABEL maintainer="wekan"
|
||||
|
||||
# Set the environment variables (defaults where required)
|
||||
ENV QEMU_ARCHITECTURE=aarch64 \
|
||||
NODE_ARCHITECTURE=linux-arm64 \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
NODE_ENV=production \
|
||||
NPM_VERSION=latest \
|
||||
WITH_API=true \
|
||||
PORT=8080 \
|
||||
ROOT_URL=http://localhost \
|
||||
MONGO_URL=mongodb://127.0.0.1:27017/wekan \
|
||||
LDAP_AUTHENTIFICATION_PASSWORD_FILE="" \
|
||||
OAUTH2_SECRET_FILE="" \
|
||||
MAIL_SERVICE_PASSWORD_FILE="" \
|
||||
MONGO_PASSWORD_FILE="" \
|
||||
S3_SECRET_FILE=""
|
||||
|
||||
# Copy qemu-static to image
|
||||
COPY --from=builder qemu-${QEMU_ARCHITECTURE}-static /usr/bin
|
||||
|
||||
# Copy the app to the image
|
||||
COPY --from=builder bundle /home/wekan/bundle
|
||||
|
||||
# Copy
|
||||
COPY --from=builder node-${NODE_VERSION}-${NODE_ARCHITECTURE} /opt/nodejs
|
||||
|
||||
RUN \
|
||||
set -o xtrace && \
|
||||
# Add non-root user wekan
|
||||
useradd --user-group --system --home-dir /home/wekan wekan && \
|
||||
\
|
||||
# Install Node
|
||||
ln -s /opt/nodejs/bin/node /usr/bin/node && \
|
||||
ln -s /opt/nodejs/bin/npm /usr/bin/npm && \
|
||||
mkdir -p /opt/nodejs/lib/node_modules/fibers/.node-gyp /root/.node-gyp/8.16.1 /home/wekan/.config && \
|
||||
chown wekan --recursive /home/wekan/.config
|
||||
|
||||
# \
|
||||
# # Install Node dependencies
|
||||
# #npm install -g npm@${NPM_VERSION} && \
|
||||
# \
|
||||
# # Install Health Check dependencies
|
||||
# #apk add curl
|
||||
#
|
||||
#HEALTHCHECK --start-period=30s --interval=30s --timeout=10s --retries=3 \
|
||||
# CMD curl --fail "http://localhost:$PORT" || exit 1
|
||||
|
||||
EXPOSE $PORT
|
||||
USER wekan
|
||||
|
||||
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 --max-old-space-size=8192 /home/wekan/bundle/main.js"]
|
||||
CMD ["bash", "-c", "ulimit -s 65500; exec node /home/wekan/bundle/main.js"]
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
FROM arm64v8/ubuntu:23.04 AS builder
|
||||
#FROM --platform=linux/amd64 amd64/ubuntu:23.04 AS builder
|
||||
#FROM --platform=linux/amd64 ghcr.io/wekan/wekan:main AS builder
|
||||
#FROM arm64v8/ubuntu:23.04 AS builder
|
||||
#FROM amd64/alpine:latest AS builder
|
||||
|
||||
# Set the environment variables for builder
|
||||
ENV QEMU_VERSION=v7.2.0-1 \
|
||||
QEMU_ARCHITECTURE=s390x \
|
||||
NODE_ARCHITECTURE=linux-s390x \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
WEKAN_VERSION=latest \
|
||||
WEKAN_ARCHITECTURE=s390x
|
||||
|
||||
# Install dependencies
|
||||
#RUN apk update && apk add ca-certificates outils-sha1 && \
|
||||
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||
RUN apt update && apt install ca-certificates wget unzip -y && \
|
||||
\
|
||||
# Download qemu static for our architecture
|
||||
wget https://github.com/multiarch/qemu-user-static/releases/download/${QEMU_VERSION}/qemu-${QEMU_ARCHITECTURE}-static.tar.gz -O - | tar -xz && \
|
||||
\
|
||||
# Download wekan and shasum
|
||||
wget https://releases.wekan.team/${WEKAN_ARCHITECTURE}/wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
|
||||
wget https://releases.wekan.team/${WEKAN_ARCHITECTURE}/SHA256SUMS.txt && \
|
||||
# Verify wekan
|
||||
grep wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip SHA256SUMS.txt | sha256sum -c - && \
|
||||
\
|
||||
# Unzip wekan
|
||||
unzip wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
|
||||
\
|
||||
# Download node and shasums
|
||||
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
|
||||
\
|
||||
# Verify nodejs authenticity
|
||||
grep node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz SHASUMS256.txt | sha256sum -c - && \
|
||||
\
|
||||
# Extract node and remove tar.gz
|
||||
tar xvzf node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz
|
||||
|
||||
# Build wekan dockerfile
|
||||
FROM --platform=linux/s390x s390x/ubuntu:23.04
|
||||
LABEL maintainer="wekan"
|
||||
|
||||
# Set the environment variables (defaults where required)
|
||||
ENV QEMU_ARCHITECTURE=s390x \
|
||||
NODE_ARCHITECTURE=linux-s390x \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
NODE_ENV=production \
|
||||
NPM_VERSION=latest \
|
||||
WITH_API=true \
|
||||
PORT=8080 \
|
||||
ROOT_URL=http://localhost \
|
||||
MONGO_URL=mongodb://127.0.0.1:27017/wekan \
|
||||
LDAP_AUTHENTIFICATION_PASSWORD_FILE="" \
|
||||
OAUTH2_SECRET_FILE="" \
|
||||
MAIL_SERVICE_PASSWORD_FILE="" \
|
||||
MONGO_PASSWORD_FILE="" \
|
||||
S3_SECRET_FILE=""
|
||||
|
||||
# Copy qemu-static to image
|
||||
COPY --from=builder qemu-${QEMU_ARCHITECTURE}-static /usr/bin
|
||||
|
||||
# Copy the app to the image
|
||||
COPY --from=builder bundle /home/wekan/bundle
|
||||
|
||||
# Copy
|
||||
COPY --from=builder node-${NODE_VERSION}-${NODE_ARCHITECTURE} /opt/nodejs
|
||||
|
||||
RUN \
|
||||
set -o xtrace && \
|
||||
# Add non-root user wekan
|
||||
useradd --user-group --system --home-dir /home/wekan wekan && \
|
||||
\
|
||||
# Install Node
|
||||
ln -s /opt/nodejs/bin/node /usr/bin/node && \
|
||||
ln -s /opt/nodejs/bin/npm /usr/bin/npm && \
|
||||
mkdir -p /opt/nodejs/lib/node_modules/fibers/.node-gyp /root/.node-gyp/8.16.1 /home/wekan/.config && \
|
||||
chown wekan --recursive /home/wekan/.config
|
||||
|
||||
# \
|
||||
# # Install Node dependencies
|
||||
# #npm install -g npm@${NPM_VERSION} && \
|
||||
# \
|
||||
# # Install Health Check dependencies
|
||||
# #apk add curl
|
||||
#
|
||||
#HEALTHCHECK --start-period=30s --interval=30s --timeout=10s --retries=3 \
|
||||
# CMD curl --fail "http://localhost:$PORT" || exit 1
|
||||
|
||||
EXPOSE $PORT
|
||||
USER wekan
|
||||
|
||||
CMD ["bash", "-c", "ulimit -s 65500; exec node /home/wekan/bundle/main.js"]
|
||||
+20
-4
@@ -1,10 +1,26 @@
|
||||
## About money
|
||||
|
||||
See [CONTRIBUTING](CONTRIBUTING.md)
|
||||
|
||||
## Responsible Security Disclosure
|
||||
|
||||
- To send email, use [ProtonMail](https://proton.me) email address or use PGP key [security-at-wekan.fi.asc](security-at-wekan.fi.asc)
|
||||
- Send info about security issue ONLY to security@wekan.fi (that is Protomail email address). NOT TO ANYWHERE ELSE. NO CC, NO BCC.
|
||||
- Wait for new WeKan release that fixes security issue
|
||||
- If you approve, we thank you by adding you to Hall of Fame: https://wekan.fi/hall-of-fame/
|
||||
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.
|
||||
|
||||
## Bonus Points
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v8.22.0"
|
||||
appVersion: "v8.78.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
+13
-7
@@ -5,16 +5,21 @@ if ('serviceWorker' in navigator) {
|
||||
});
|
||||
}
|
||||
|
||||
// Import Blaze helpers (replaces removed raix:handlebar-helpers package)
|
||||
import '/client/config/blazeHelpers';
|
||||
|
||||
// Import board converter for on-demand conversion
|
||||
import '/client/lib/boardConverter';
|
||||
import '/client/components/boardConversionProgress';
|
||||
import '/client/features/boardConversion';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
// Import migration manager and progress UI
|
||||
import '/client/lib/attachmentMigrationManager';
|
||||
import '/client/components/settings/migrationProgress';
|
||||
// Import migration manager and progress UI - COMMENTED OUT
|
||||
// import '/client/lib/attachmentMigrationManager';
|
||||
// import '/client/components/settings/migrationProgress';
|
||||
|
||||
// Import cron settings
|
||||
import '/client/components/settings/cronSettings';
|
||||
// Import cron settings - COMMENTED OUT
|
||||
// import '/client/components/settings/cronSettings';
|
||||
// Custom head tags
|
||||
|
||||
// Mirror Meteor login token into a cookie for server-side file route auth
|
||||
// This enables cookie-based auth for /cdn/storage/* without leaking ROOT_URL
|
||||
@@ -61,6 +66,7 @@ Meteor.startup(() => {
|
||||
syncCookie();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Subscribe to per-user small publications
|
||||
@@ -71,7 +77,7 @@ Meteor.startup(() => {
|
||||
Meteor.subscribe('userDesktopDragHandles');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Initialize mobile mode on startup for iOS devices
|
||||
// This ensures mobile mode is applied correctly on page load
|
||||
Tracker.afterFlush(() => {
|
||||
|
||||
@@ -9,11 +9,11 @@ template(name="activities")
|
||||
+cardActivities
|
||||
|
||||
template(name="boardActivities")
|
||||
each activityData in currentBoard.activities
|
||||
each activityData in boardActivitiesList
|
||||
+activity(activity=activityData card=card mode=mode)
|
||||
|
||||
template(name="cardActivities")
|
||||
each activityData in activities
|
||||
each activityData in cardActivitiesList
|
||||
+activity(activity=activityData card=card mode=mode)
|
||||
|
||||
template(name="activity")
|
||||
@@ -189,15 +189,15 @@ template(name="activity")
|
||||
if(currentData.timeKey)
|
||||
| {{_ activity.activityType }}
|
||||
= ' '
|
||||
i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
|
||||
i(title=currentData.timeValue).activity-meta {{ displayDate currentData.timeValue 'LLL' }}
|
||||
if (currentData.timeOldValue)
|
||||
= ' '
|
||||
| {{{_ "previous_as" }}}
|
||||
= ' '
|
||||
i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
|
||||
i(title=currentData.timeOldValue).activity-meta {{ displayDate currentData.timeOldValue 'LLL' }}
|
||||
= ' @'
|
||||
else if(currentData.timeValue)
|
||||
| {{_ activity.activityType currentData.timeValue}}
|
||||
|
||||
if($neq mode 'none')
|
||||
div(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
|
||||
div(title=activity.createdAt).activity-meta {{ displayDate activity.createdAt }}
|
||||
@@ -1,168 +1,192 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Template } from 'meteor/templating';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import { getSidebarInstance } from '/client/features/sidebar/service';
|
||||
|
||||
const activitiesPerPage = 500;
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
// XXX Should we use ReactiveNumber?
|
||||
this.page = new ReactiveVar(1);
|
||||
this.loadNextPageLocked = false;
|
||||
// TODO is sidebar always available? E.g. on small screens/mobile devices
|
||||
const sidebar = Sidebar;
|
||||
sidebar && sidebar.callFirstWith(null, 'resetNextPeak');
|
||||
this.autorun(() => {
|
||||
let mode = this.data()?.mode;
|
||||
if (mode) {
|
||||
const capitalizedMode = Utils.capitalize(mode);
|
||||
let searchId;
|
||||
const showActivities = this.showActivities();
|
||||
if (mode === 'linkedcard' || mode === 'linkedboard') {
|
||||
const currentCard = Utils.getCurrentCard();
|
||||
searchId = currentCard.linkedId;
|
||||
mode = mode.replace('linked', '');
|
||||
} else if (mode === 'card') {
|
||||
searchId = Utils.getCurrentCardId();
|
||||
} else {
|
||||
searchId = Session.get(`current${capitalizedMode}`);
|
||||
}
|
||||
const limit = this.page.get() * activitiesPerPage;
|
||||
if (searchId === null) return;
|
||||
Template.activities.onCreated(function () {
|
||||
// Register with sidebar so it can call loadNextPage on us
|
||||
const Sidebar = getSidebarInstance();
|
||||
if (Sidebar) {
|
||||
Sidebar.activitiesInstance = this;
|
||||
}
|
||||
|
||||
this.subscribe('activities', mode, searchId, limit, showActivities, () => {
|
||||
this.loadNextPageLocked = false;
|
||||
|
||||
// TODO the guard can be removed as soon as the TODO above is resolved
|
||||
if (!sidebar) return;
|
||||
// If the sibear peak hasn't increased, that mean that there are no more
|
||||
// activities, and we can stop calling new subscriptions.
|
||||
// XXX This is hacky! We need to know excatly and reactively how many
|
||||
// activities there are, we probably want to denormalize this number
|
||||
// dirrectly into card and board documents.
|
||||
const nextPeakBefore = sidebar.callFirstWith(null, 'getNextPeak');
|
||||
sidebar.calculateNextPeak();
|
||||
const nextPeakAfter = sidebar.callFirstWith(null, 'getNextPeak');
|
||||
if (nextPeakBefore === nextPeakAfter) {
|
||||
sidebar.callFirstWith(null, 'resetNextPeak');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
loadNextPage() {
|
||||
// XXX Should we use ReactiveNumber?
|
||||
this.page = new ReactiveVar(1);
|
||||
this.loadNextPageLocked = false;
|
||||
this.loadNextPage = () => {
|
||||
if (this.loadNextPageLocked === false) {
|
||||
this.page.set(this.page.get() + 1);
|
||||
this.loadNextPageLocked = true;
|
||||
}
|
||||
},
|
||||
showActivities() {
|
||||
let ret = false;
|
||||
let mode = this.data()?.mode;
|
||||
};
|
||||
|
||||
// TODO is sidebar always available? E.g. on small screens/mobile devices
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar && sidebar.infiniteScrolling) {
|
||||
sidebar.infiniteScrolling.resetNextPeak();
|
||||
}
|
||||
this.autorun(() => {
|
||||
const data = Template.currentData();
|
||||
let mode = data?.mode;
|
||||
if (mode) {
|
||||
const capitalizedMode = Utils.capitalize(mode);
|
||||
let searchId;
|
||||
const showActivities = _showActivities(data);
|
||||
if (mode === 'linkedcard' || mode === 'linkedboard') {
|
||||
const currentCard = Utils.getCurrentCard();
|
||||
ret = currentCard.showActivities ?? false;
|
||||
searchId = currentCard.linkedId;
|
||||
mode = mode.replace('linked', '');
|
||||
} else if (mode === 'card') {
|
||||
ret = this.data()?.card?.showActivities ?? false;
|
||||
searchId = Utils.getCurrentCardId();
|
||||
} else {
|
||||
ret = Utils.getCurrentBoard().showActivities ?? false;
|
||||
searchId = Session.get(`current${capitalizedMode}`);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
activities() {
|
||||
const ret = this.data().card.activities();
|
||||
return ret;
|
||||
},
|
||||
}).register('activities');
|
||||
const limit = this.page.get() * activitiesPerPage;
|
||||
if (searchId === null) return;
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
this.subscribe('activities', mode, searchId, limit, showActivities, () => {
|
||||
this.loadNextPageLocked = false;
|
||||
|
||||
// TODO the guard can be removed as soon as the TODO above is resolved
|
||||
if (!sidebar || !sidebar.infiniteScrolling) return;
|
||||
// If the sidebar peak hasn't increased, that means that there are no more
|
||||
// activities, and we can stop calling new subscriptions.
|
||||
const nextPeakBefore = sidebar.infiniteScrolling.getNextPeak();
|
||||
sidebar.calculateNextPeak();
|
||||
const nextPeakAfter = sidebar.infiniteScrolling.getNextPeak();
|
||||
if (nextPeakBefore === nextPeakAfter) {
|
||||
sidebar.infiniteScrolling.resetNextPeak();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function _showActivities(data) {
|
||||
let ret = false;
|
||||
let mode = data?.mode;
|
||||
if (mode) {
|
||||
if (mode === 'linkedcard' || mode === 'linkedboard') {
|
||||
const currentCard = Utils.getCurrentCard();
|
||||
ret = currentCard.showActivities ?? false;
|
||||
} else if (mode === 'card') {
|
||||
// For card mode, get showActivities from data context or fallback to current card
|
||||
const card = data?.card || Utils.getCurrentCard();
|
||||
ret = card?.showActivities ?? false;
|
||||
} else {
|
||||
ret = Utils.getCurrentBoard().showActivities ?? false;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
Template.activities.helpers({
|
||||
showActivities() {
|
||||
const data = Template.currentData();
|
||||
return _showActivities(data);
|
||||
},
|
||||
|
||||
activities() {
|
||||
const activities = this.card?.activities?.();
|
||||
return activities || [];
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardActivities.helpers({
|
||||
boardActivitiesList() {
|
||||
const board = Utils.getCurrentBoard();
|
||||
const activities = board?.activities?.();
|
||||
return activities || [];
|
||||
},
|
||||
});
|
||||
|
||||
Template.cardActivities.helpers({
|
||||
cardActivitiesList() {
|
||||
const data = Template.currentData();
|
||||
const card = data?.card;
|
||||
const activities = card?.activities?.();
|
||||
return activities || [];
|
||||
},
|
||||
});
|
||||
|
||||
Template.activity.helpers({
|
||||
checkItem() {
|
||||
const checkItemId = this.currentData().activity.checklistItemId;
|
||||
const checkItemId = this.activity.checklistItemId;
|
||||
const checkItem = ReactiveCache.getChecklistItem(checkItemId);
|
||||
return checkItem && checkItem.title;
|
||||
},
|
||||
|
||||
boardLabelLink() {
|
||||
const data = this.currentData();
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
if (data.mode !== 'board') {
|
||||
// data.mode: card, linkedcard, linkedboard
|
||||
return createBoardLink(data.activity.board(), data.activity.listName ? data.activity.listName : null);
|
||||
if (this.mode !== 'board') {
|
||||
return createBoardLink(this.activity.board(), this.activity.listName ? this.activity.listName : null);
|
||||
}
|
||||
else if (currentBoardId != data.activity.boardId) {
|
||||
// data.mode: board
|
||||
// current activitie is linked
|
||||
return createBoardLink(data.activity.board(), data.activity.listName ? data.activity.listName : null);
|
||||
else if (currentBoardId != this.activity.boardId) {
|
||||
return createBoardLink(this.activity.board(), this.activity.listName ? this.activity.listName : null);
|
||||
}
|
||||
return TAPi18n.__('this-board');
|
||||
},
|
||||
|
||||
cardLabelLink() {
|
||||
const data = this.currentData();
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
if (data.mode == 'card') {
|
||||
// data.mode: card
|
||||
if (this.mode == 'card') {
|
||||
return TAPi18n.__('this-card');
|
||||
}
|
||||
else if (data.mode !== 'board') {
|
||||
// data.mode: linkedcard, linkedboard
|
||||
return createCardLink(data.activity.card(), null);
|
||||
else if (this.mode !== 'board') {
|
||||
return createCardLink(this.activity.card(), null);
|
||||
}
|
||||
else if (currentBoardId != data.activity.boardId) {
|
||||
// data.mode: board
|
||||
// current activitie is linked
|
||||
return createCardLink(data.activity.card(), data.activity.board().title);
|
||||
else if (currentBoardId != this.activity.boardId) {
|
||||
return createCardLink(this.activity.card(), this.activity.board().title);
|
||||
}
|
||||
return createCardLink(this.currentData().activity.card(), null);
|
||||
return createCardLink(this.activity.card(), null);
|
||||
},
|
||||
|
||||
cardLink() {
|
||||
const data = this.currentData();
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
if (data.mode !== 'board') {
|
||||
// data.mode: card, linkedcard, linkedboard
|
||||
return createCardLink(data.activity.card(), null);
|
||||
if (this.mode !== 'board') {
|
||||
return createCardLink(this.activity.card(), null);
|
||||
}
|
||||
else if (currentBoardId != data.activity.boardId) {
|
||||
// data.mode: board
|
||||
// current activitie is linked
|
||||
return createCardLink(data.activity.card(), data.activity.board().title);
|
||||
else if (currentBoardId != this.activity.boardId) {
|
||||
return createCardLink(this.activity.card(), this.activity.board().title);
|
||||
}
|
||||
return createCardLink(this.currentData().activity.card(), null);
|
||||
return createCardLink(this.activity.card(), null);
|
||||
},
|
||||
|
||||
receivedDate() {
|
||||
const receivedDate = this.currentData().activity.card();
|
||||
if (!receivedDate) return null;
|
||||
return receivedDate.receivedAt;
|
||||
const card = this.activity.card();
|
||||
if (!card) return null;
|
||||
return card.receivedAt;
|
||||
},
|
||||
|
||||
startDate() {
|
||||
const startDate = this.currentData().activity.card();
|
||||
if (!startDate) return null;
|
||||
return startDate.startAt;
|
||||
const card = this.activity.card();
|
||||
if (!card) return null;
|
||||
return card.startAt;
|
||||
},
|
||||
|
||||
dueDate() {
|
||||
const dueDate = this.currentData().activity.card();
|
||||
if (!dueDate) return null;
|
||||
return dueDate.dueAt;
|
||||
const card = this.activity.card();
|
||||
if (!card) return null;
|
||||
return card.dueAt;
|
||||
},
|
||||
|
||||
endDate() {
|
||||
const endDate = this.currentData().activity.card();
|
||||
if (!endDate) return null;
|
||||
return endDate.endAt;
|
||||
const card = this.activity.card();
|
||||
if (!card) return null;
|
||||
return card.endAt;
|
||||
},
|
||||
|
||||
lastLabel() {
|
||||
const lastLabelId = this.currentData().activity.labelId;
|
||||
const lastLabelId = this.activity.labelId;
|
||||
if (!lastLabelId) return null;
|
||||
const lastLabel = ReactiveCache.getBoard(
|
||||
this.currentData().activity.boardId,
|
||||
this.activity.boardId,
|
||||
).getLabelById(lastLabelId);
|
||||
if (lastLabel && (lastLabel.name === undefined || lastLabel.name === '')) {
|
||||
return lastLabel.color;
|
||||
@@ -175,7 +199,7 @@ BlazeComponent.extendComponent({
|
||||
|
||||
lastCustomField() {
|
||||
const lastCustomField = ReactiveCache.getCustomField(
|
||||
this.currentData().activity.customFieldId,
|
||||
this.activity.customFieldId,
|
||||
);
|
||||
if (!lastCustomField) return null;
|
||||
return lastCustomField.name;
|
||||
@@ -183,16 +207,15 @@ BlazeComponent.extendComponent({
|
||||
|
||||
lastCustomFieldValue() {
|
||||
const lastCustomField = ReactiveCache.getCustomField(
|
||||
this.currentData().activity.customFieldId,
|
||||
this.activity.customFieldId,
|
||||
);
|
||||
if (!lastCustomField) return null;
|
||||
const value = this.currentData().activity.value;
|
||||
const value = this.activity.value;
|
||||
if (
|
||||
lastCustomField.settings.dropdownItems &&
|
||||
lastCustomField.settings.dropdownItems.length > 0
|
||||
) {
|
||||
const dropDownValue = _.find(
|
||||
lastCustomField.settings.dropdownItems,
|
||||
const dropDownValue = lastCustomField.settings.dropdownItems.find(
|
||||
item => {
|
||||
return item._id === value;
|
||||
},
|
||||
@@ -203,13 +226,13 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
listLabel() {
|
||||
const activity = this.currentData().activity;
|
||||
const activity = this.activity;
|
||||
const list = activity.list();
|
||||
return (list && list.title) || activity.title;
|
||||
},
|
||||
|
||||
sourceLink() {
|
||||
const source = this.currentData().activity.source;
|
||||
const source = this.activity.source;
|
||||
if (source) {
|
||||
if (source.url) {
|
||||
return Blaze.toHTML(
|
||||
@@ -229,12 +252,12 @@ BlazeComponent.extendComponent({
|
||||
|
||||
memberLink() {
|
||||
return Blaze.toHTMLWithData(Template.memberName, {
|
||||
user: this.currentData().activity.member(),
|
||||
user: this.activity.member(),
|
||||
});
|
||||
},
|
||||
|
||||
attachmentLink() {
|
||||
const attachment = this.currentData().activity.attachment();
|
||||
const attachment = this.activity.attachment();
|
||||
// trying to display url before file is stored generates js errors
|
||||
return (
|
||||
(attachment &&
|
||||
@@ -248,17 +271,16 @@ BlazeComponent.extendComponent({
|
||||
sanitizeText(attachment.name),
|
||||
),
|
||||
)) ||
|
||||
sanitizeText(this.currentData().activity.attachmentName)
|
||||
sanitizeText(this.activity.attachmentName)
|
||||
);
|
||||
},
|
||||
|
||||
customField() {
|
||||
const customField = this.currentData().activity.customField();
|
||||
const customField = this.activity.customField();
|
||||
if (!customField) return null;
|
||||
return customField.name;
|
||||
},
|
||||
|
||||
}).register('activity');
|
||||
});
|
||||
|
||||
Template.activity.helpers({
|
||||
sanitize(value) {
|
||||
|
||||
@@ -25,13 +25,14 @@ template(name="comment")
|
||||
= text
|
||||
.edit-controls
|
||||
button.primary(type="submit") {{_ 'edit'}}
|
||||
a.js-close-inlined-form(title="{{_ 'close' }}") ❌
|
||||
a.js-close-inlined-form(title="{{_ 'close' }}")
|
||||
i.fa.fa-times-thin
|
||||
else
|
||||
.comment-text
|
||||
+viewer
|
||||
= text
|
||||
+commentReactions(reactions=reactions commentId=_id)
|
||||
span(title=createdAt).comment-meta {{ moment createdAt }}
|
||||
span(title=createdAt).comment-meta {{ displayDate createdAt }}
|
||||
if($eq currentUser._id userId)
|
||||
+editOrDeleteComment
|
||||
else if currentUser.isBoardAdmin
|
||||
|
||||
@@ -1,95 +1,85 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import CardComments from '/models/cardComments';
|
||||
import { UnsavedEdits } from '/client/lib/unsavedEdits';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import autosize from 'autosize';
|
||||
|
||||
const commentFormIsOpen = new ReactiveVar(false);
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onDestroyed() {
|
||||
commentFormIsOpen.set(false);
|
||||
$('.note-popover').hide();
|
||||
},
|
||||
Template.commentForm.onDestroyed(function () {
|
||||
commentFormIsOpen.set(false);
|
||||
$('.note-popover').hide();
|
||||
});
|
||||
|
||||
Template.commentForm.helpers({
|
||||
commentFormIsOpen() {
|
||||
return commentFormIsOpen.get();
|
||||
},
|
||||
});
|
||||
|
||||
getInput() {
|
||||
return this.$('.js-new-comment-input');
|
||||
Template.commentForm.events({
|
||||
'submit .js-new-comment-form'(evt, tpl) {
|
||||
const input = tpl.$('.js-new-comment-input');
|
||||
const text = input.val().trim();
|
||||
const card = Template.currentData();
|
||||
let boardId = card.boardId;
|
||||
let cardId = card._id;
|
||||
if (card.isLinkedCard()) {
|
||||
boardId = ReactiveCache.getCard(card.linkedId).boardId;
|
||||
cardId = card.linkedId;
|
||||
} else if (card.isLinkedBoard()) {
|
||||
boardId = card.linkedId;
|
||||
}
|
||||
if (text) {
|
||||
CardComments.insert({
|
||||
text,
|
||||
boardId,
|
||||
cardId,
|
||||
});
|
||||
resetCommentInput(input);
|
||||
Tracker.flush();
|
||||
autosize.update(input);
|
||||
input.trigger('submitted');
|
||||
}
|
||||
evt.preventDefault();
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-new-comment-form'(evt) {
|
||||
const input = this.getInput();
|
||||
const text = input.val().trim();
|
||||
const card = this.currentData();
|
||||
let boardId = card.boardId;
|
||||
let cardId = card._id;
|
||||
if (card.isLinkedCard()) {
|
||||
boardId = ReactiveCache.getCard(card.linkedId).boardId;
|
||||
cardId = card.linkedId;
|
||||
} else if (card.isLinkedBoard()) {
|
||||
boardId = card.linkedId;
|
||||
}
|
||||
if (text) {
|
||||
CardComments.insert({
|
||||
text,
|
||||
boardId,
|
||||
cardId,
|
||||
});
|
||||
resetCommentInput(input);
|
||||
Tracker.flush();
|
||||
autosize.update(input);
|
||||
input.trigger('submitted');
|
||||
}
|
||||
evt.preventDefault();
|
||||
},
|
||||
// Pressing Ctrl+Enter should submit the form
|
||||
'keydown form textarea'(evt) {
|
||||
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
|
||||
this.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
// Pressing Ctrl+Enter should submit the form
|
||||
'keydown form textarea'(evt, tpl) {
|
||||
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
|
||||
tpl.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
}).register('commentForm');
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.comments.helpers({
|
||||
getComments() {
|
||||
const data = this.data();
|
||||
const data = Template.currentData();
|
||||
if (!data || typeof data.comments !== 'function') return [];
|
||||
return data.comments();
|
||||
},
|
||||
}).register("comments");
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-delete-comment': Popup.afterConfirm('deleteComment', () => {
|
||||
const commentId = this.data()._id;
|
||||
CardComments.remove(commentId);
|
||||
Popup.back();
|
||||
}),
|
||||
'submit .js-edit-comment'(evt) {
|
||||
evt.preventDefault();
|
||||
const commentText = this.currentComponent()
|
||||
.getValue()
|
||||
.trim();
|
||||
const commentId = this.data()._id;
|
||||
if (commentText) {
|
||||
CardComments.update(commentId, {
|
||||
$set: {
|
||||
text: commentText,
|
||||
},
|
||||
});
|
||||
}
|
||||
Template.comment.events({
|
||||
'click .js-delete-comment': Popup.afterConfirm('deleteComment', function () {
|
||||
const commentId = this._id;
|
||||
CardComments.remove(commentId);
|
||||
Popup.back();
|
||||
}),
|
||||
'submit .js-edit-comment'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const textarea = tpl.find('.js-edit-comment textarea,input[type=text]');
|
||||
const commentText = textarea && textarea.value ? textarea.value.trim() : '';
|
||||
const commentId = this._id;
|
||||
if (commentText) {
|
||||
CardComments.update(commentId, {
|
||||
$set: {
|
||||
text: commentText,
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
},
|
||||
}).register("comment");
|
||||
});
|
||||
|
||||
// XXX This should be a static method of the `commentForm` component
|
||||
function resetCommentInput(input) {
|
||||
|
||||
@@ -170,14 +170,14 @@
|
||||
width: 95%;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
|
||||
.board-conversion-header,
|
||||
.board-conversion-content,
|
||||
.board-conversion-footer {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
|
||||
.board-conversion-header h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@@ -3,25 +3,25 @@ template(name="boardConversionProgress")
|
||||
.board-conversion-modal
|
||||
.board-conversion-header
|
||||
h3
|
||||
| ⚙️
|
||||
i.fa.fa-cog
|
||||
| {{_ 'converting-board'}}
|
||||
p {{_ 'converting-board-description'}}
|
||||
|
||||
|
||||
.board-conversion-content
|
||||
.conversion-progress
|
||||
.progress-bar
|
||||
.progress-fill(style="width: {{conversionProgress}}%")
|
||||
.progress-text {{conversionProgress}}%
|
||||
|
||||
|
||||
.conversion-status
|
||||
| ⚙️
|
||||
i.fa.fa-cog
|
||||
| {{conversionStatus}}
|
||||
|
||||
|
||||
.conversion-time(style="{{#unless conversionEstimatedTime}}display: none;{{/unless}}")
|
||||
| ⏰
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'estimated-time-remaining'}}: {{conversionEstimatedTime}}
|
||||
|
||||
|
||||
.board-conversion-footer
|
||||
.conversion-info
|
||||
| ℹ️
|
||||
i.fa.fa-info-circle
|
||||
| {{_ 'conversion-info-text'}}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import {
|
||||
import {
|
||||
boardConverter,
|
||||
isConverting,
|
||||
conversionProgress,
|
||||
@@ -12,15 +12,15 @@ Template.boardConversionProgress.helpers({
|
||||
isConverting() {
|
||||
return isConverting.get();
|
||||
},
|
||||
|
||||
|
||||
conversionProgress() {
|
||||
return conversionProgress.get();
|
||||
},
|
||||
|
||||
|
||||
conversionStatus() {
|
||||
return conversionStatus.get();
|
||||
},
|
||||
|
||||
|
||||
conversionEstimatedTime() {
|
||||
return conversionEstimatedTime.get();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
template(name="archivedBoards")
|
||||
h2
|
||||
span(title="{{_ 'archived-boards'}}") 📦
|
||||
span(title="{{_ 'archived-boards'}}")
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archived-boards'}}
|
||||
|
||||
ul.archived-lists
|
||||
@@ -8,13 +9,13 @@ template(name="archivedBoards")
|
||||
li.archived-lists-item
|
||||
div.board-header-btns
|
||||
button.board-header-btn.js-delete-board
|
||||
| 🗑️
|
||||
i.fa.fa-trash
|
||||
| {{_ 'delete-board'}}
|
||||
button.board-header-btn.js-restore-board
|
||||
| ↩️
|
||||
i.fa.fa-undo
|
||||
| {{_ 'restore-board'}}
|
||||
= title
|
||||
span {{ moment archivedAt 'LLL' }}
|
||||
span {{ displayDate archivedAt 'LLL' }}
|
||||
else
|
||||
li.no-items-message {{_ 'no-archived-boards'}}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import Boards from '/models/boards';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.subscribe('archivedBoards');
|
||||
},
|
||||
Template.archivedBoards.onCreated(function () {
|
||||
this.subscribe('archivedBoards');
|
||||
});
|
||||
|
||||
Template.archivedBoards.helpers({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
@@ -19,38 +21,34 @@ BlazeComponent.extendComponent({
|
||||
);
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-restore-board'() {
|
||||
// TODO : Make isSandstorm variable global
|
||||
const isSandstorm =
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.sandstorm;
|
||||
if (isSandstorm && Utils.getCurrentBoardId()) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
currentBoard.archive();
|
||||
}
|
||||
const board = this.currentData();
|
||||
board.restore();
|
||||
Utils.goBoardId(board._id);
|
||||
},
|
||||
'click .js-delete-board': Popup.afterConfirm('boardDelete', function() {
|
||||
Popup.back();
|
||||
const isSandstorm =
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.sandstorm;
|
||||
if (isSandstorm && Utils.getCurrentBoardId()) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
Boards.remove(currentBoard._id);
|
||||
}
|
||||
Boards.remove(this._id);
|
||||
FlowRouter.go('home');
|
||||
}),
|
||||
},
|
||||
];
|
||||
Template.archivedBoards.events({
|
||||
async 'click .js-restore-board'() {
|
||||
// TODO : Make isSandstorm variable global
|
||||
const isSandstorm =
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.sandstorm;
|
||||
if (isSandstorm && Utils.getCurrentBoardId()) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
await currentBoard.archive();
|
||||
}
|
||||
const board = this;
|
||||
await board.restore();
|
||||
Utils.goBoardId(board._id);
|
||||
},
|
||||
}).register('archivedBoards');
|
||||
'click .js-delete-board': Popup.afterConfirm('boardDelete', async function() {
|
||||
Popup.back();
|
||||
const isSandstorm =
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.sandstorm;
|
||||
if (isSandstorm && Utils.getCurrentBoardId()) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
await Boards.removeAsync(currentBoard._id);
|
||||
}
|
||||
await Boards.removeAsync(this._id);
|
||||
FlowRouter.go('home');
|
||||
}),
|
||||
});
|
||||
@@ -293,7 +293,7 @@ body.mobile-mode.iphone-device .card-details .card-details-item-title {
|
||||
.board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
|
||||
|
||||
/* In desktop mode on small screens, still keep overlay behind card */
|
||||
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
template(name="board")
|
||||
|
||||
if isConverting.get
|
||||
|
||||
if isConverting
|
||||
+boardConversionProgress
|
||||
else if isBoardReady.get
|
||||
else if isBoardReady
|
||||
if currentBoard
|
||||
if onlyShowCurrentCard
|
||||
+cardDetails(currentCard)
|
||||
@@ -28,10 +28,10 @@ template(name="boardBody")
|
||||
class="{{#if hasSwimlanes}}dragscroll{{/if}}"
|
||||
class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
|
||||
class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
|
||||
class="{{#if draggingActive.get}}is-dragging-active{{/if}}"
|
||||
class="{{#if draggingActive}}is-dragging-active{{/if}}"
|
||||
class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}"
|
||||
class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
if showOverlay.get
|
||||
if showOverlay
|
||||
.board-overlay
|
||||
if currentBoard.isTemplatesBoard
|
||||
each currentBoard.swimlanes
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -49,6 +49,12 @@ THEME - NEPHRITIS
|
||||
border-bottom: 2px solid #27ae60;
|
||||
border-right: 2px solid #27ae60;
|
||||
}
|
||||
.board-color-nephritis .checklist-progress-bar {
|
||||
background-color: #d4f1dd !important;
|
||||
}
|
||||
.board-color-nephritis .checklist-progress-bar .checklist-progress {
|
||||
background-color: #27ae60 !important;
|
||||
}
|
||||
.board-color-nephritis .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e7faef;
|
||||
}
|
||||
@@ -150,6 +156,12 @@ THEME - Pomegranate
|
||||
border-bottom: 2px solid #c0392b;
|
||||
border-right: 2px solid #c0392b;
|
||||
}
|
||||
.board-color-pomegranate .checklist-progress-bar {
|
||||
background-color: #f5d5d2 !important;
|
||||
}
|
||||
.board-color-pomegranate .checklist-progress-bar .checklist-progress {
|
||||
background-color: #c0392b !important;
|
||||
}
|
||||
.board-color-pomegranate .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #faeae9;
|
||||
}
|
||||
@@ -251,6 +263,12 @@ THEME - Belize
|
||||
border-bottom: 2px solid #2980b9;
|
||||
border-right: 2px solid #2980b9;
|
||||
}
|
||||
.board-color-belize .checklist-progress-bar {
|
||||
background-color: #d1e7f5 !important;
|
||||
}
|
||||
.board-color-belize .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2980b9 !important;
|
||||
}
|
||||
.board-color-belize .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e8f3fa;
|
||||
}
|
||||
@@ -352,6 +370,12 @@ THEME - Wisteria
|
||||
border-bottom: 2px solid #8e44ad;
|
||||
border-right: 2px solid #8e44ad;
|
||||
}
|
||||
.board-color-wisteria .checklist-progress-bar {
|
||||
background-color: #e8d9f0 !important;
|
||||
}
|
||||
.board-color-wisteria .checklist-progress-bar .checklist-progress {
|
||||
background-color: #8e44ad !important;
|
||||
}
|
||||
.board-color-wisteria .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #f4ecf7;
|
||||
}
|
||||
@@ -453,6 +477,12 @@ THEME - Midnight
|
||||
border-bottom: 2px solid #2c3e50;
|
||||
border-right: 2px solid #2c3e50;
|
||||
}
|
||||
.board-color-midnight .checklist-progress-bar {
|
||||
background-color: #d2dae2 !important;
|
||||
}
|
||||
.board-color-midnight .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2c3e50 !important;
|
||||
}
|
||||
.board-color-midnight .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e6ecf1;
|
||||
}
|
||||
@@ -554,6 +584,12 @@ THEME - Pumpkin
|
||||
border-bottom: 2px solid #e67e22;
|
||||
border-right: 2px solid #e67e22;
|
||||
}
|
||||
.board-color-pumpkin .checklist-progress-bar {
|
||||
background-color: #f9e5d1 !important;
|
||||
}
|
||||
.board-color-pumpkin .checklist-progress-bar .checklist-progress {
|
||||
background-color: #e67e22 !important;
|
||||
}
|
||||
.board-color-pumpkin .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #fdf2e9;
|
||||
}
|
||||
@@ -655,6 +691,12 @@ THEME - Moderate Pink
|
||||
border-bottom: 2px solid #cd5a91;
|
||||
border-right: 2px solid #cd5a91;
|
||||
}
|
||||
.board-color-moderatepink .checklist-progress-bar {
|
||||
background-color: #f4dde8 !important;
|
||||
}
|
||||
.board-color-moderatepink .checklist-progress-bar .checklist-progress {
|
||||
background-color: #cd5a91 !important;
|
||||
}
|
||||
.board-color-moderatepink .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #faeef4;
|
||||
}
|
||||
@@ -756,6 +798,12 @@ THEME - Strong Cyan
|
||||
border-bottom: 2px solid #00aecc;
|
||||
border-right: 2px solid #00aecc;
|
||||
}
|
||||
.board-color-strongcyan .checklist-progress-bar {
|
||||
background-color: #ccf2f9 !important;
|
||||
}
|
||||
.board-color-strongcyan .checklist-progress-bar .checklist-progress {
|
||||
background-color: #00aecc !important;
|
||||
}
|
||||
.board-color-strongcyan .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e0fbff;
|
||||
}
|
||||
@@ -857,6 +905,12 @@ THEME - Lime Green
|
||||
border-bottom: 2px solid #4bbf6b;
|
||||
border-right: 2px solid #4bbf6b;
|
||||
}
|
||||
.board-color-limegreen .checklist-progress-bar {
|
||||
background-color: #daf4de !important;
|
||||
}
|
||||
.board-color-limegreen .checklist-progress-bar .checklist-progress {
|
||||
background-color: #4bbf6b !important;
|
||||
}
|
||||
.board-color-limegreen .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #edf9f0;
|
||||
}
|
||||
@@ -959,6 +1013,12 @@ THEME - Dark
|
||||
border-bottom: 2px solid #2c3e51;
|
||||
border-right: 2px solid #2c3e51;
|
||||
}
|
||||
.board-color-dark .checklist-progress-bar {
|
||||
background-color: #d2dae2 !important;
|
||||
}
|
||||
.board-color-dark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2c3e51 !important;
|
||||
}
|
||||
.board-color-dark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e6ecf1;
|
||||
}
|
||||
@@ -1162,6 +1222,12 @@ THEME - Relax
|
||||
border-bottom: 2px solid #27ae61;
|
||||
border-right: 2px solid #27ae61;
|
||||
}
|
||||
.board-color-relax .checklist-progress-bar {
|
||||
background-color: #d4f1dd !important;
|
||||
}
|
||||
.board-color-relax .checklist-progress-bar .checklist-progress {
|
||||
background-color: #27ae61 !important;
|
||||
}
|
||||
.board-color-relax .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e7faef;
|
||||
}
|
||||
@@ -1292,6 +1358,12 @@ THEME - Corteza
|
||||
border-bottom: 2px solid #568ba2;
|
||||
border-right: 2px solid #568ba2;
|
||||
}
|
||||
.board-color-corteza .checklist-progress-bar {
|
||||
background-color: #dce6ec !important;
|
||||
}
|
||||
.board-color-corteza .checklist-progress-bar .checklist-progress {
|
||||
background-color: #568ba2 !important;
|
||||
}
|
||||
.board-color-corteza .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #eef3f6;
|
||||
}
|
||||
@@ -1397,6 +1469,12 @@ THEME - Clear Blue
|
||||
border-bottom: 2px solid #499bea;
|
||||
border-right: 2px solid #499bea;
|
||||
}
|
||||
.board-color-clearblue .checklist-progress-bar {
|
||||
background-color: #daeefb !important;
|
||||
}
|
||||
.board-color-clearblue .checklist-progress-bar .checklist-progress {
|
||||
background-color: #499bea !important;
|
||||
}
|
||||
.board-color-clearblue .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e0fbff;
|
||||
}
|
||||
@@ -1660,6 +1738,12 @@ THEME - Natural
|
||||
border-bottom: 2px solid #596557;
|
||||
border-right: 2px solid #596557;
|
||||
}
|
||||
.board-color-natural .checklist-progress-bar {
|
||||
background-color: #dee0dd !important;
|
||||
}
|
||||
.board-color-natural .checklist-progress-bar .checklist-progress {
|
||||
background-color: #596557 !important;
|
||||
}
|
||||
.board-color-natural .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #eef0ee;
|
||||
}
|
||||
@@ -1770,6 +1854,12 @@ THEME - Modern
|
||||
border-bottom: 2px solid #2a80b8;
|
||||
border-right: 2px solid #2a80b8;
|
||||
}
|
||||
.board-color-modern .checklist-progress-bar {
|
||||
background-color: #d1e7f5 !important;
|
||||
}
|
||||
.board-color-modern .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2a80b8 !important;
|
||||
}
|
||||
.board-color-modern .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e8f3fa;
|
||||
}
|
||||
@@ -2062,6 +2152,12 @@ THEME - Modern Dark
|
||||
border-bottom: 2px solid #2a2a2a;
|
||||
border-right: 2px solid #2a2a2a;
|
||||
}
|
||||
.board-color-moderndark .checklist-progress-bar {
|
||||
background-color: #d1d1d1 !important;
|
||||
}
|
||||
.board-color-moderndark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2a2a2a !important;
|
||||
}
|
||||
.board-color-moderndark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #eaeaea;
|
||||
}
|
||||
@@ -2547,6 +2643,12 @@ THEME - Exodark
|
||||
border-bottom: 2px solid #dbdbdb!important;/*Fix contrast of checkbox*/
|
||||
border-right: 2px solid #dbdbdb!important;
|
||||
}
|
||||
.board-color-exodark .checklist-progress-bar {
|
||||
background-color: #cccccc !important;
|
||||
}
|
||||
.board-color-exodark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
.board-color-exodark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
@@ -3140,6 +3242,12 @@ THEME - Clean Dark
|
||||
margin-left: 3px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.board-color-cleandark .checklist-progress-bar {
|
||||
background-color: #6b6b78 !important;
|
||||
}
|
||||
.board-color-cleandark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #23232B !important;
|
||||
}
|
||||
|
||||
.board-color-cleandark .allBoards {
|
||||
white-space: nowrap;
|
||||
@@ -3892,6 +4000,13 @@ THEME - Clean Light
|
||||
margin-left: 3px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.board-color-cleanlight .checklist-progress-bar {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
.board-color-cleanlight .checklist-progress-bar .checklist-progress {
|
||||
background-color: #c0c0c0 !important;
|
||||
color: #010101 !important;
|
||||
}
|
||||
|
||||
.board-color-cleanlight .allBoards {
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -14,41 +14,39 @@ template(name="boardHeaderBar")
|
||||
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}}")
|
||||
| {{#if currentBoard.isPublic}}🌐{{else}}🔒{{/if}}
|
||||
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(title="{{_ 'star-board'}}")
|
||||
if isStarred
|
||||
| ⭐
|
||||
else
|
||||
| ☆
|
||||
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}}")
|
||||
| {{sortCardsIcon}}
|
||||
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
|
||||
@@ -59,116 +57,114 @@ template(name="boardHeaderBar")
|
||||
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}}")
|
||||
| {{#if currentBoard.isPublic}}🌐{{else}}🔒{{/if}}
|
||||
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"
|
||||
| 🔕
|
||||
a.board-header-btn.js-star-board(title="{{_ 'star-board'}}")
|
||||
if isStarred
|
||||
| ⭐
|
||||
else
|
||||
| ☆
|
||||
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}}")
|
||||
| {{sortCardsIcon}}
|
||||
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-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(
|
||||
title="{{_ 'board-view'}}")
|
||||
| ▼
|
||||
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'
|
||||
| {{_ 'board-view-swimlanes'}}
|
||||
| {{_ 'swimlanes'}}
|
||||
if $eq boardView 'board-view-lists'
|
||||
| {{_ 'board-view-lists'}}
|
||||
| {{_ 'lists'}}
|
||||
if $eq boardView 'board-view-cal'
|
||||
| {{_ 'board-view-cal'}}
|
||||
| {{_ 'calendar'}}
|
||||
if $eq boardView 'board-view-gantt'
|
||||
| {{_ '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}}js-multiselection-active{{/if}}")
|
||||
if MultiSelection.isActive
|
||||
| 🗂️
|
||||
else
|
||||
| 🗂️
|
||||
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="{{_ 'multi-selection-off'}}")
|
||||
| ❌
|
||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||
i.fa.fa-times-thin
|
||||
|
||||
.separator
|
||||
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
|
||||
| ☰
|
||||
i.fa.fa-bars
|
||||
|
||||
template(name="boardVisibilityList")
|
||||
ul.pop-over-list
|
||||
li
|
||||
with "private"
|
||||
a.js-select-visibility
|
||||
| 🔒
|
||||
i.fa.fa-lock
|
||||
| {{_ 'private'}}
|
||||
if visibilityCheck
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
span.sub-name {{_ 'private-desc'}}
|
||||
if notAllowPrivateVisibilityOnly
|
||||
li
|
||||
with "public"
|
||||
a.js-select-visibility
|
||||
| 🌐
|
||||
i.fa.fa-globe
|
||||
| {{_ 'public'}}
|
||||
if visibilityCheck
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
span.sub-name {{_ 'public-desc'}}
|
||||
|
||||
template(name="boardChangeVisibilityPopup")
|
||||
@@ -179,26 +175,26 @@ template(name="boardChangeWatchPopup")
|
||||
li
|
||||
with "watching"
|
||||
a.js-select-watch
|
||||
| 👁️
|
||||
i.fa.fa-eye
|
||||
| {{_ 'watching'}}
|
||||
if watchCheck
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
span.sub-name {{_ 'watching-info'}}
|
||||
li
|
||||
with "tracking"
|
||||
a.js-select-watch
|
||||
| 🔔
|
||||
i.fa.fa-bell
|
||||
| {{_ 'tracking'}}
|
||||
if watchCheck
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
span.sub-name {{_ 'tracking-info'}}
|
||||
li
|
||||
with "muted"
|
||||
a.js-select-watch
|
||||
| 🔕
|
||||
i.fa.fa-bell-slash
|
||||
| {{_ 'muted'}}
|
||||
if watchCheck
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
span.sub-name {{_ 'muted-info'}}
|
||||
|
||||
template(name="boardChangeViewPopup")
|
||||
@@ -206,47 +202,47 @@ template(name="boardChangeViewPopup")
|
||||
li
|
||||
with "board-view-swimlanes"
|
||||
a.js-open-swimlanes-view
|
||||
| 🏊
|
||||
| {{_ 'board-view-swimlanes'}}
|
||||
i.fa.fa-th-large
|
||||
| {{_ 'swimlanes'}}
|
||||
if $eq Utils.boardView "board-view-swimlanes"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
li
|
||||
with "board-view-lists"
|
||||
a.js-open-lists-view
|
||||
| 📋
|
||||
i.fa.fa-trello
|
||||
| {{_ 'board-view-lists'}}
|
||||
if $eq Utils.boardView "board-view-lists"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
li
|
||||
with "board-view-cal"
|
||||
a.js-open-cal-view
|
||||
| 📅
|
||||
i.fa.fa-calendar
|
||||
| {{_ 'board-view-cal'}}
|
||||
if $eq Utils.boardView "board-view-cal"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
li
|
||||
with "board-view-gantt"
|
||||
a.js-open-gantt-view
|
||||
| 📊
|
||||
i.fa.fa-bar-chart
|
||||
| {{_ 'board-view-gantt'}}
|
||||
if $eq Utils.boardView "board-view-gantt"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
|
||||
template(name="createBoard")
|
||||
form
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
|
||||
if visibilityMenuIsOpen.get
|
||||
if visibilityMenuIsOpen
|
||||
+boardVisibilityList
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility.get 'public'
|
||||
span 🌐
|
||||
if $eq visibility 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span 🔒
|
||||
span.fa.fa-lock.colorful
|
||||
= " "
|
||||
| {{{_ 'board-private-info'}}}
|
||||
a.js-change-visibility {{_ 'change'}}.
|
||||
@@ -266,16 +262,45 @@ template(name="createBoardPopup")
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
|
||||
if visibilityMenuIsOpen.get
|
||||
if visibilityMenuIsOpen
|
||||
+boardVisibilityList
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility.get 'public'
|
||||
span 🌐
|
||||
if $eq visibility 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span 🔒
|
||||
span.fa.fa-lock.colorful
|
||||
= " "
|
||||
| {{{_ 'board-private-info'}}}
|
||||
a.js-change-visibility {{_ 'change'}}.
|
||||
a.flex.js-toggle-add-template-container
|
||||
.materialCheckBox#add-template-container
|
||||
span {{_ 'add-template-container'}}
|
||||
input.primary.wide(type="submit" value="{{_ 'create'}}")
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-import-board {{_ 'import'}}
|
||||
span.quiet
|
||||
| /
|
||||
a.js-board-template {{_ 'template'}}
|
||||
|
||||
template(name="headerBarCreateBoardPopup")
|
||||
form
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
|
||||
if visibilityMenuIsOpen
|
||||
+boardVisibilityList
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span.fa.fa-lock.colorful
|
||||
= " "
|
||||
| {{{_ 'board-private-info'}}}
|
||||
a.js-change-visibility {{_ 'change'}}.
|
||||
@@ -296,16 +321,16 @@ template(name="createTemplateContainerPopup")
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
|
||||
if visibilityMenuIsOpen.get
|
||||
if visibilityMenuIsOpen
|
||||
+boardVisibilityList
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility.get 'public'
|
||||
span 🌐
|
||||
if $eq visibility 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span 🔒
|
||||
span.fa.fa-lock.colorful
|
||||
= " "
|
||||
| {{{_ 'board-private-info'}}}
|
||||
a.js-change-visibility {{_ 'change'}}.
|
||||
@@ -321,19 +346,30 @@ template(name="createTemplateContainerPopup")
|
||||
a.js-board-template {{_ 'template'}}
|
||||
|
||||
//template(name="listsortPopup")
|
||||
// h2
|
||||
// | {{_ 'list-sort-by'}}
|
||||
// hr
|
||||
// ul.pop-over-list
|
||||
// each value in allowedSortValues
|
||||
// li
|
||||
// a.js-sort-by(name="{{value.name}}")
|
||||
// if $eq sortby value.name
|
||||
// | {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
|
||||
// | {{_ value.label }}{{_ value.shortLabel}}
|
||||
// if $eq sortby value.name
|
||||
// | ✅
|
||||
|
||||
//
|
||||
h2
|
||||
//
|
||||
| {{_ 'list-sort-by'}}
|
||||
//
|
||||
hr
|
||||
//
|
||||
ul.pop-over-list
|
||||
//
|
||||
each value in allowedSortValues
|
||||
//
|
||||
li
|
||||
//
|
||||
a.js-sort-by(name="{{value.name}}")
|
||||
//
|
||||
if $eq sortby value.name
|
||||
//
|
||||
| {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
|
||||
//
|
||||
| {{_ value.label }}{{_ value.shortLabel}}
|
||||
//
|
||||
if $eq sortby value.name
|
||||
//
|
||||
i.fa.fa-check
|
||||
template(name="boardChangeTitlePopup")
|
||||
form
|
||||
label
|
||||
@@ -353,21 +389,21 @@ template(name="cardsSortPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-sort-due
|
||||
| 📅
|
||||
i.fa.fa-calendar
|
||||
| {{_ 'due-date'}}
|
||||
hr
|
||||
li
|
||||
a.js-sort-title
|
||||
| 🔤
|
||||
i.fa.fa-sort-alpha-asc
|
||||
| {{_ 'title-alphabetically'}}
|
||||
hr
|
||||
li
|
||||
a.js-sort-created-desc
|
||||
| ⬇️
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'created-at-newest-first'}}
|
||||
hr
|
||||
li
|
||||
a.js-sort-created-asc
|
||||
| ⬆️
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'created-at-oldest-first'}}
|
||||
|
||||
@@ -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';
|
||||
@@ -10,7 +18,8 @@ const UPCLS = 'fa-sort-up';
|
||||
const sortCardsBy = new ReactiveVar('');
|
||||
|
||||
Template.boardChangeTitlePopup.events({
|
||||
submit(event, templateInstance) {
|
||||
async submit(event, templateInstance) {
|
||||
event.preventDefault();
|
||||
const newTitle = templateInstance
|
||||
.$('.js-board-name')
|
||||
.val()
|
||||
@@ -20,22 +29,22 @@ Template.boardChangeTitlePopup.events({
|
||||
.val()
|
||||
.trim();
|
||||
if (newTitle) {
|
||||
this.rename(newTitle);
|
||||
this.setDescription(newDesc);
|
||||
const board = Utils.getCurrentBoard();
|
||||
if (board) {
|
||||
await board.rename(newTitle);
|
||||
await board.setDescription(newDesc);
|
||||
}
|
||||
Popup.back();
|
||||
}
|
||||
event.preventDefault();
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.boardHeaderBar.helpers({
|
||||
watchLevel() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return currentBoard && currentBoard.getWatchLevel(Meteor.userId());
|
||||
},
|
||||
|
||||
|
||||
|
||||
isStarred() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
@@ -47,130 +56,7 @@ BlazeComponent.extendComponent({
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return currentBoard && currentBoard.stars >= 2;
|
||||
},
|
||||
/*
|
||||
showSort() {
|
||||
return ReactiveCache.getCurrentUser().hasSortBy();
|
||||
},
|
||||
directionClass() {
|
||||
return this.currentDirection() === -1 ? DOWNCLS : UPCLS;
|
||||
},
|
||||
changeDirection() {
|
||||
const direction = 0 - this.currentDirection() === -1 ? '-' : '';
|
||||
Meteor.call('setListSortBy', direction + this.currentListSortBy());
|
||||
},
|
||||
currentDirection() {
|
||||
return ReactiveCache.getCurrentUser().getListSortByDirection();
|
||||
},
|
||||
currentListSortBy() {
|
||||
return ReactiveCache.getCurrentUser().getListSortBy();
|
||||
},
|
||||
listSortShortDesc() {
|
||||
return `list-label-short-${this.currentListSortBy()}`;
|
||||
},
|
||||
*/
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
|
||||
'click .js-star-board'() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
if (boardId) {
|
||||
Meteor.call('toggleBoardStar', boardId);
|
||||
}
|
||||
},
|
||||
'click .js-open-board-menu': Popup.open('boardMenu'),
|
||||
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
|
||||
'click .js-watch-board': Popup.open('boardChangeWatch'),
|
||||
'click .js-open-archived-board'() {
|
||||
Modal.open('archivedBoards');
|
||||
},
|
||||
'click .js-toggle-board-view': Popup.open('boardChangeView'),
|
||||
'click .js-toggle-sidebar'() {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Hamburger menu clicked');
|
||||
}
|
||||
// Use the same approach as keyboard shortcuts
|
||||
if (typeof Sidebar !== 'undefined' && Sidebar && typeof Sidebar.toggle === 'function') {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Using Sidebar.toggle()');
|
||||
}
|
||||
Sidebar.toggle();
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('Sidebar not available, trying alternative approach');
|
||||
}
|
||||
// Try to trigger the sidebar through the global Blaze helper
|
||||
if (typeof Blaze !== 'undefined' && Blaze._globalHelpers && Blaze._globalHelpers.Sidebar) {
|
||||
const sidebar = Blaze._globalHelpers.Sidebar();
|
||||
if (sidebar && typeof sidebar.toggle === 'function') {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Using Blaze helper Sidebar.toggle()');
|
||||
}
|
||||
sidebar.toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-open-filter-view'() {
|
||||
if (Sidebar) {
|
||||
Sidebar.setView('filter');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
},
|
||||
'click .js-sort-cards': Popup.open('cardsSort'),
|
||||
/*
|
||||
'click .js-open-sort-view'(evt) {
|
||||
const target = evt.target;
|
||||
if (target.tagName === 'I') {
|
||||
// click on the text, popup choices
|
||||
this.changeDirection();
|
||||
} else {
|
||||
// change the sort order
|
||||
Popup.open('listsort')(evt);
|
||||
}
|
||||
},
|
||||
*/
|
||||
'click .js-filter-reset'(event) {
|
||||
event.stopPropagation();
|
||||
if (Sidebar) {
|
||||
Sidebar.setView();
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
Filter.reset();
|
||||
},
|
||||
'click .js-sort-reset'() {
|
||||
Session.set('sortBy', '');
|
||||
},
|
||||
'click .js-open-search-view'() {
|
||||
if (Sidebar) {
|
||||
Sidebar.setView('search');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
},
|
||||
'click .js-multiselection-activate'() {
|
||||
const currentCard = Utils.getCurrentCardId();
|
||||
MultiSelection.activate();
|
||||
if (currentCard) {
|
||||
MultiSelection.add(currentCard);
|
||||
}
|
||||
},
|
||||
'click .js-multiselection-reset'(event) {
|
||||
event.stopPropagation();
|
||||
MultiSelection.disable();
|
||||
},
|
||||
'click .js-log-in'() {
|
||||
FlowRouter.go('atSignIn');
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
}).register('boardHeaderBar');
|
||||
|
||||
Template.boardHeaderBar.helpers({
|
||||
boardView() {
|
||||
return Utils.boardView();
|
||||
},
|
||||
@@ -182,7 +68,7 @@ Template.boardHeaderBar.helpers({
|
||||
if (!sortBy) {
|
||||
return '🃏'; // Card icon when nothing is selected
|
||||
}
|
||||
|
||||
|
||||
// Determine which sort option is active based on sortBy object
|
||||
if (sortBy.dueAt) {
|
||||
return '📅'; // Due date icon
|
||||
@@ -191,11 +77,111 @@ Template.boardHeaderBar.helpers({
|
||||
} else if (sortBy.createdAt) {
|
||||
return sortBy.createdAt === 1 ? '⬆️' : '⬇️'; // Up/down arrow based on direction
|
||||
}
|
||||
|
||||
|
||||
return '🃏'; // Default card icon
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardHeaderBar.events({
|
||||
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
|
||||
'click .js-star-board'() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
if (boardId) {
|
||||
Meteor.call('toggleBoardStar', boardId);
|
||||
}
|
||||
},
|
||||
'click .js-open-board-menu': Popup.open('boardMenu'),
|
||||
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
|
||||
'click .js-watch-board': Popup.open('boardChangeWatch'),
|
||||
'click .js-open-archived-board'() {
|
||||
Modal.open('archivedBoards');
|
||||
},
|
||||
'click .js-toggle-board-view': Popup.open('boardChangeView'),
|
||||
'click .js-toggle-sidebar'() {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Hamburger menu clicked');
|
||||
}
|
||||
// Use the same approach as keyboard shortcuts
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar && typeof sidebar.toggle === 'function') {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Using Sidebar.toggle()');
|
||||
}
|
||||
sidebar.toggle();
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('Sidebar not available, trying alternative approach');
|
||||
}
|
||||
// Try to trigger the sidebar through the global Blaze helper
|
||||
if (typeof Blaze !== 'undefined' && Blaze._globalHelpers && Blaze._globalHelpers.Sidebar) {
|
||||
const blazeSidebar = Blaze._globalHelpers.Sidebar();
|
||||
if (blazeSidebar && typeof blazeSidebar.toggle === 'function') {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Using Blaze helper Sidebar.toggle()');
|
||||
}
|
||||
blazeSidebar.toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-open-filter-view'() {
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar) {
|
||||
sidebar.setView('filter');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
},
|
||||
'click .js-sort-cards': Popup.open('cardsSort'),
|
||||
/*
|
||||
'click .js-open-sort-view'(evt) {
|
||||
const target = evt.target;
|
||||
if (target.tagName === 'I') {
|
||||
// click on the text, popup choices
|
||||
this.changeDirection();
|
||||
} else {
|
||||
// change the sort order
|
||||
Popup.open('listsort')(evt);
|
||||
}
|
||||
},
|
||||
*/
|
||||
'click .js-filter-reset'(event) {
|
||||
event.stopPropagation();
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar) {
|
||||
sidebar.setView();
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
Filter.reset();
|
||||
},
|
||||
'click .js-sort-reset'() {
|
||||
Session.set('sortBy', '');
|
||||
},
|
||||
'click .js-open-search-view'() {
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar) {
|
||||
sidebar.setView('search');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
},
|
||||
'click .js-multiselection-activate'() {
|
||||
const currentCard = Utils.getCurrentCardId();
|
||||
MultiSelection.activate();
|
||||
if (currentCard) {
|
||||
MultiSelection.add(currentCard);
|
||||
}
|
||||
},
|
||||
'click .js-multiselection-reset'(event) {
|
||||
event.stopPropagation();
|
||||
MultiSelection.disable();
|
||||
},
|
||||
'click .js-log-in'() {
|
||||
FlowRouter.go('atSignIn');
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardChangeViewPopup.events({
|
||||
'click .js-open-lists-view'() {
|
||||
Utils.setBoardView('board-view-lists');
|
||||
@@ -215,219 +201,229 @@ Template.boardChangeViewPopup.events({
|
||||
},
|
||||
});
|
||||
|
||||
const CreateBoard = BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'createBoard';
|
||||
},
|
||||
// Shared setup for all create board popups
|
||||
function setupCreateBoardState(tpl) {
|
||||
tpl.visibilityMenuIsOpen = new ReactiveVar(false);
|
||||
tpl.visibility = new ReactiveVar('private');
|
||||
tpl.boardId = new ReactiveVar('');
|
||||
Meteor.subscribe('tableVisibilityModeSettings');
|
||||
}
|
||||
|
||||
onCreated() {
|
||||
this.visibilityMenuIsOpen = new ReactiveVar(false);
|
||||
this.visibility = new ReactiveVar('private');
|
||||
this.boardId = new ReactiveVar('');
|
||||
Meteor.subscribe('tableVisibilityModeSettings');
|
||||
},
|
||||
function createBoardHelpers() {
|
||||
return {
|
||||
visibilityMenuIsOpen() {
|
||||
return Template.instance().visibilityMenuIsOpen.get();
|
||||
},
|
||||
visibility() {
|
||||
return Template.instance().visibility.get();
|
||||
},
|
||||
notAllowPrivateVisibilityOnly() {
|
||||
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
|
||||
},
|
||||
visibilityCheck() {
|
||||
return Template.currentData() === Template.instance().visibility.get();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
notAllowPrivateVisibilityOnly(){
|
||||
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
|
||||
},
|
||||
async function createBoardSubmit(tpl, event) {
|
||||
event.preventDefault();
|
||||
const title = tpl.find('.js-new-board-title').value;
|
||||
const slug = getSlug(title) || 'board';
|
||||
|
||||
visibilityCheck() {
|
||||
return this.currentData() === this.visibility.get();
|
||||
},
|
||||
const addTemplateContainer = $('#add-template-container.is-checked').length > 0;
|
||||
if (addTemplateContainer) {
|
||||
tpl.boardId.set(
|
||||
await Meteor.callAsync('createBoardWithInitialSwimlanes', {
|
||||
title,
|
||||
slug,
|
||||
permission: 'private',
|
||||
type: 'template-container',
|
||||
migrationVersion: 1,
|
||||
swimlanes: [
|
||||
{ title: 'Card Templates', sort: 1, type: 'template-container' },
|
||||
{ title: 'List Templates', sort: 2, type: 'template-container' },
|
||||
{ title: 'Board Templates', sort: 3, type: 'template-container' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
setVisibility(visibility) {
|
||||
this.visibility.set(visibility);
|
||||
this.visibilityMenuIsOpen.set(false);
|
||||
},
|
||||
// Assign to space if one was selected
|
||||
const spaceId = Session.get('createBoardInWorkspace');
|
||||
if (spaceId) {
|
||||
Meteor.call('assignBoardToWorkspace', tpl.boardId.get(), spaceId, (err) => {
|
||||
if (err) console.error('Error assigning board to space:', err);
|
||||
});
|
||||
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||
}
|
||||
|
||||
toggleVisibilityMenu() {
|
||||
this.visibilityMenuIsOpen.set(!this.visibilityMenuIsOpen.get());
|
||||
},
|
||||
FlowRouter.go('board', { id: tpl.boardId.get(), slug });
|
||||
|
||||
toggleAddTemplateContainer() {
|
||||
} else {
|
||||
const visibility = tpl.visibility.get();
|
||||
|
||||
tpl.boardId.set(
|
||||
await Meteor.callAsync('createBoardWithInitialSwimlanes', {
|
||||
title,
|
||||
slug,
|
||||
permission: visibility,
|
||||
migrationVersion: 1,
|
||||
swimlanes: [{ title: 'Default' }],
|
||||
}),
|
||||
);
|
||||
|
||||
// Assign to space if one was selected
|
||||
const spaceId = Session.get('createBoardInWorkspace');
|
||||
if (spaceId) {
|
||||
Meteor.call('assignBoardToWorkspace', tpl.boardId.get(), spaceId, (err) => {
|
||||
if (err) console.error('Error assigning board to space:', err);
|
||||
});
|
||||
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||
}
|
||||
|
||||
FlowRouter.go('board', { id: tpl.boardId.get(), slug });
|
||||
}
|
||||
}
|
||||
|
||||
function createBoardEvents() {
|
||||
return {
|
||||
'click .js-select-visibility'(event, tpl) {
|
||||
tpl.visibility.set(Template.currentData());
|
||||
tpl.visibilityMenuIsOpen.set(false);
|
||||
},
|
||||
'click .js-change-visibility'(event, tpl) {
|
||||
tpl.visibilityMenuIsOpen.set(!tpl.visibilityMenuIsOpen.get());
|
||||
},
|
||||
'click .js-import': Popup.open('boardImportBoard'),
|
||||
async 'submit'(event, tpl) {
|
||||
await createBoardSubmit(tpl, event);
|
||||
},
|
||||
'click .js-import-board': Popup.open('chooseBoardSource'),
|
||||
'click .js-board-template': Popup.open('searchElement'),
|
||||
'click .js-toggle-add-template-container'() {
|
||||
$('#add-template-container').toggleClass('is-checked');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// createBoard (non-popup version)
|
||||
Template.createBoard.onCreated(function () {
|
||||
setupCreateBoardState(this);
|
||||
});
|
||||
|
||||
Template.createBoard.helpers(createBoardHelpers());
|
||||
|
||||
Template.createBoard.events(createBoardEvents());
|
||||
|
||||
// createBoardPopup
|
||||
Template.createBoardPopup.onCreated(function () {
|
||||
setupCreateBoardState(this);
|
||||
});
|
||||
|
||||
Template.createBoardPopup.helpers(createBoardHelpers());
|
||||
|
||||
Template.createBoardPopup.events(createBoardEvents());
|
||||
|
||||
// createTemplateContainerPopup
|
||||
Template.createTemplateContainerPopup.onCreated(function () {
|
||||
setupCreateBoardState(this);
|
||||
});
|
||||
|
||||
Template.createTemplateContainerPopup.onRendered(function () {
|
||||
// Always pre-check the template container checkbox for this popup
|
||||
$('#add-template-container').addClass('is-checked');
|
||||
});
|
||||
|
||||
Template.createTemplateContainerPopup.helpers(createBoardHelpers());
|
||||
|
||||
Template.createTemplateContainerPopup.events(createBoardEvents());
|
||||
|
||||
// headerBarCreateBoardPopup
|
||||
Template.headerBarCreateBoardPopup.onCreated(function () {
|
||||
setupCreateBoardState(this);
|
||||
});
|
||||
|
||||
Template.headerBarCreateBoardPopup.helpers(createBoardHelpers());
|
||||
|
||||
Template.headerBarCreateBoardPopup.events({
|
||||
'click .js-select-visibility'(event, tpl) {
|
||||
tpl.visibility.set(Template.currentData());
|
||||
tpl.visibilityMenuIsOpen.set(false);
|
||||
},
|
||||
'click .js-change-visibility'(event, tpl) {
|
||||
tpl.visibilityMenuIsOpen.set(!tpl.visibilityMenuIsOpen.get());
|
||||
},
|
||||
'click .js-import': Popup.open('boardImportBoard'),
|
||||
async submit(event, tpl) {
|
||||
await createBoardSubmit(tpl, event);
|
||||
// Immediately star boards created with the headerbar popup.
|
||||
await ReactiveCache.getCurrentUser().toggleBoardStar(tpl.boardId.get());
|
||||
},
|
||||
'click .js-import-board': Popup.open('chooseBoardSource'),
|
||||
'click .js-board-template': Popup.open('searchElement'),
|
||||
'click .js-toggle-add-template-container'() {
|
||||
$('#add-template-container').toggleClass('is-checked');
|
||||
},
|
||||
});
|
||||
|
||||
onSubmit(event) {
|
||||
event.preventDefault();
|
||||
const title = this.find('.js-new-board-title').value;
|
||||
Template.boardChangeVisibilityPopup.onCreated(function () {
|
||||
Meteor.subscribe('tableVisibilityModeSettings');
|
||||
});
|
||||
|
||||
const addTemplateContainer = $('#add-template-container.is-checked').length > 0;
|
||||
if (addTemplateContainer) {
|
||||
//const templateContainerId = Meteor.call('setCreateTemplateContainer');
|
||||
//Utils.goBoardId(templateContainerId);
|
||||
//alert('niinku template ' + Meteor.call('setCreateTemplateContainer'));
|
||||
|
||||
this.boardId.set(
|
||||
Boards.insert({
|
||||
// title: TAPi18n.__('templates'),
|
||||
title: title,
|
||||
permission: 'private',
|
||||
type: 'template-container',
|
||||
migrationVersion: 1, // Latest version - no migration needed
|
||||
}),
|
||||
);
|
||||
|
||||
// Insert the card templates swimlane
|
||||
Swimlanes.insert({
|
||||
// title: TAPi18n.__('card-templates-swimlane'),
|
||||
title: 'Card Templates',
|
||||
boardId: this.boardId.get(),
|
||||
sort: 1,
|
||||
type: 'template-container',
|
||||
}),
|
||||
|
||||
// Insert the list templates swimlane
|
||||
Swimlanes.insert(
|
||||
{
|
||||
// title: TAPi18n.__('list-templates-swimlane'),
|
||||
title: 'List Templates',
|
||||
boardId: this.boardId.get(),
|
||||
sort: 2,
|
||||
type: 'template-container',
|
||||
},
|
||||
);
|
||||
|
||||
// Insert the board templates swimlane
|
||||
Swimlanes.insert(
|
||||
{
|
||||
//title: TAPi18n.__('board-templates-swimlane'),
|
||||
title: 'Board Templates',
|
||||
boardId: this.boardId.get(),
|
||||
sort: 3,
|
||||
type: 'template-container',
|
||||
},
|
||||
);
|
||||
|
||||
// Assign to space if one was selected
|
||||
const spaceId = Session.get('createBoardInWorkspace');
|
||||
if (spaceId) {
|
||||
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
|
||||
if (err) console.error('Error assigning board to space:', err);
|
||||
});
|
||||
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||
}
|
||||
|
||||
Utils.goBoardId(this.boardId.get());
|
||||
|
||||
} else {
|
||||
const visibility = this.visibility.get();
|
||||
|
||||
this.boardId.set(
|
||||
Boards.insert({
|
||||
title,
|
||||
permission: visibility,
|
||||
migrationVersion: 1, // Latest version - no migration needed
|
||||
}),
|
||||
);
|
||||
|
||||
Swimlanes.insert({
|
||||
title: 'Default',
|
||||
boardId: this.boardId.get(),
|
||||
});
|
||||
|
||||
// Assign to space if one was selected
|
||||
const spaceId = Session.get('createBoardInWorkspace');
|
||||
if (spaceId) {
|
||||
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
|
||||
if (err) console.error('Error assigning board to space:', err);
|
||||
});
|
||||
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||
}
|
||||
|
||||
Utils.goBoardId(this.boardId.get());
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-select-visibility'() {
|
||||
this.setVisibility(this.currentData());
|
||||
},
|
||||
'click .js-change-visibility': this.toggleVisibilityMenu,
|
||||
'click .js-import': Popup.open('boardImportBoard'),
|
||||
submit: this.onSubmit,
|
||||
'click .js-import-board': Popup.open('chooseBoardSource'),
|
||||
'click .js-board-template': Popup.open('searchElement'),
|
||||
'click .js-toggle-add-template-container': this.toggleAddTemplateContainer,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('createBoardPopup');
|
||||
|
||||
(class CreateTemplateContainerPopup extends CreateBoard {
|
||||
onRendered() {
|
||||
// Always pre-check the template container checkbox for this popup
|
||||
$('#add-template-container').addClass('is-checked');
|
||||
}
|
||||
}).register('createTemplateContainerPopup');
|
||||
|
||||
(class HeaderBarCreateBoard extends CreateBoard {
|
||||
onSubmit(event) {
|
||||
super.onSubmit(event);
|
||||
// Immediately star boards crated with the headerbar popup.
|
||||
ReactiveCache.getCurrentUser().toggleBoardStar(this.boardId.get());
|
||||
}
|
||||
}.register('headerBarCreateBoardPopup'));
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.boardChangeVisibilityPopup.helpers({
|
||||
notAllowPrivateVisibilityOnly(){
|
||||
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
|
||||
},
|
||||
visibilityCheck() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return this.currentData() === currentBoard.permission;
|
||||
return this === currentBoard.permission;
|
||||
},
|
||||
});
|
||||
|
||||
selectBoardVisibility() {
|
||||
Template.boardChangeVisibilityPopup.events({
|
||||
'click .js-select-visibility'() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const visibility = this.currentData();
|
||||
currentBoard.setVisibility(visibility);
|
||||
Popup.back();
|
||||
const visibility = Template.currentData();
|
||||
if (typeof visibility === 'string') {
|
||||
currentBoard.setVisibility(visibility);
|
||||
Popup.back();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-select-visibility': this.selectBoardVisibility,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('boardChangeVisibilityPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.boardChangeWatchPopup.helpers({
|
||||
watchLevel() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return currentBoard.getWatchLevel(Meteor.userId());
|
||||
},
|
||||
|
||||
watchCheck() {
|
||||
return this.currentData() === this.watchLevel();
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return this === currentBoard.getWatchLevel(Meteor.userId());
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-select-watch'() {
|
||||
const level = this.currentData();
|
||||
Meteor.call(
|
||||
'watch',
|
||||
'board',
|
||||
Session.get('currentBoard'),
|
||||
level,
|
||||
(err, ret) => {
|
||||
if (!err && ret) Popup.back();
|
||||
},
|
||||
);
|
||||
Template.boardChangeWatchPopup.events({
|
||||
'click .js-select-watch'() {
|
||||
const level = Template.currentData();
|
||||
if (typeof level === 'string') {
|
||||
Meteor.call(
|
||||
'watch',
|
||||
'board',
|
||||
Session.get('currentBoard'),
|
||||
level,
|
||||
(err, ret) => {
|
||||
if (!err && ret) Popup.back();
|
||||
},
|
||||
},
|
||||
];
|
||||
);
|
||||
}
|
||||
},
|
||||
}).register('boardChangeWatchPopup');
|
||||
});
|
||||
|
||||
/*
|
||||
BlazeComponent.extendComponent({
|
||||
// BlazeComponent.extendComponent was removed - this code is unused.
|
||||
// Original listsortPopup component:
|
||||
// {
|
||||
onCreated() {
|
||||
//this.sortBy = new ReactiveVar();
|
||||
////this.sortDirection = new ReactiveVar();
|
||||
@@ -495,46 +491,40 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('listsortPopup');
|
||||
// }.register('listsortPopup');
|
||||
*/
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-sort-due'() {
|
||||
const sortBy = {
|
||||
dueAt: 1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('due-date'));
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-sort-title'() {
|
||||
const sortBy = {
|
||||
title: 1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('title'));
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-sort-created-asc'() {
|
||||
const sortBy = {
|
||||
createdAt: 1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('date-created-newest-first'));
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-sort-created-desc'() {
|
||||
const sortBy = {
|
||||
createdAt: -1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('date-created-oldest-first'));
|
||||
Popup.back();
|
||||
},
|
||||
},
|
||||
];
|
||||
Template.cardsSortPopup.events({
|
||||
'click .js-sort-due'() {
|
||||
const sortBy = {
|
||||
dueAt: 1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('due-date'));
|
||||
Popup.back();
|
||||
},
|
||||
}).register('cardsSortPopup');
|
||||
'click .js-sort-title'() {
|
||||
const sortBy = {
|
||||
title: 1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('title'));
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-sort-created-asc'() {
|
||||
const sortBy = {
|
||||
createdAt: 1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('date-created-newest-first'));
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-sort-created-desc'() {
|
||||
const sortBy = {
|
||||
createdAt: -1,
|
||||
};
|
||||
Session.set('sortBy', sortBy);
|
||||
sortCardsBy.set(TAPi18n.__('date-created-oldest-first'));
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
@import url("../../../css/reset.css") print, screen;
|
||||
@import url("../../../public/css/reset.css") print, screen;
|
||||
|
||||
/* Board List Header with Zoom Controls */
|
||||
.board-list-header {
|
||||
@@ -69,12 +69,14 @@
|
||||
font-weight: bold;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.workspaces-header .js-add-space {
|
||||
.workspaces-header .js-add-space,
|
||||
.workspaces-header .js-add-workspace {
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
border: 1px solid #ccc;
|
||||
border: none;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.workspace-tree {
|
||||
@@ -130,8 +132,23 @@
|
||||
}
|
||||
|
||||
.workspace-node .workspace-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-icon .workspace-icon-value,
|
||||
.workspace-node .workspace-icon .workspace-icon-value p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-icon .emoji-icon {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-name {
|
||||
@@ -165,6 +182,55 @@
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.workspace-node .js-open-workspace-menu {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.workspace-node .js-open-workspace-menu:hover {
|
||||
opacity: 1;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.workspace-actions-form label,
|
||||
.workspace-subspace-form label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.workspace-actions-form input[type='text'],
|
||||
.workspace-subspace-form input[type='text'] {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.workspace-actions-form .workspace-color-label {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.workspace-actions-form .workspace-color-list {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.workspace-delete-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.workspace-delete-confirm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workspace-node.active > .workspace-node-content .js-select-space,
|
||||
.workspace-node > .workspace-node-content:hover .js-select-space {
|
||||
background: #f0f0f0;
|
||||
@@ -436,10 +502,17 @@
|
||||
background-color: #999; /* Darker background for better text contrast */
|
||||
border-radius: 3px;
|
||||
padding: 36px 8px 32px 8px;
|
||||
color: #fff; /* White text */
|
||||
}
|
||||
.board-list .js-add-board .label i {
|
||||
color: #fff; /* White icon */
|
||||
}
|
||||
.board-list .js-add-board .label:hover {
|
||||
background-color: #808080; /* Even darker on hover */
|
||||
}
|
||||
.board-list .js-add-board .label:hover i {
|
||||
color: #fff; /* Keep icon white on hover */
|
||||
}
|
||||
.board-list .is-star-active,
|
||||
.board-list .is-not-star-active {
|
||||
top: 0;
|
||||
|
||||
@@ -9,24 +9,28 @@ template(name="boardList")
|
||||
li(class="menu-item {{#if isSelectedMenu 'starred'}}active{{/if}}")
|
||||
a.js-select-menu(data-type="starred")
|
||||
span.menu-label
|
||||
span.emoji-icon ⭐
|
||||
span.emoji-icon
|
||||
i.fa.fa-star
|
||||
| {{_ 'allboards.starred'}}
|
||||
span.menu-count {{menuItemCount 'starred'}}
|
||||
li(class="menu-item {{#if isSelectedMenu 'templates'}}active{{/if}}")
|
||||
a.js-select-menu(data-type="templates")
|
||||
span.menu-label
|
||||
span.emoji-icon 📋
|
||||
span.emoji-icon
|
||||
i.fa.fa-clipboard
|
||||
| {{_ 'allboards.templates'}}
|
||||
span.menu-count {{menuItemCount 'templates'}}
|
||||
li(class="menu-item {{#if isSelectedMenu 'remaining'}}active{{/if}}")
|
||||
a.js-select-menu(data-type="remaining")
|
||||
span.menu-label
|
||||
span.emoji-icon 📂
|
||||
span.emoji-icon
|
||||
i.fa.fa-folder
|
||||
| {{_ 'allboards.remaining'}}
|
||||
span.menu-count {{menuItemCount 'remaining'}}
|
||||
.workspaces-header
|
||||
span
|
||||
span.emoji-icon 🗂️
|
||||
span.emoji-icon
|
||||
i.fa.fa-folder-open
|
||||
| {{_ 'allboards.workspaces'}}
|
||||
a.js-add-workspace(title="{{_ 'allboards.add-workspace'}}") +
|
||||
// Workspaces tree
|
||||
@@ -51,11 +55,13 @@ template(name="boardList")
|
||||
li.AllBoardBtns
|
||||
div.AllBoardButtonsContainer
|
||||
if userHasOrgsOrTeams
|
||||
span.emoji-icon 🔍
|
||||
span.emoji-icon
|
||||
i.fa.fa-search
|
||||
input#filterBtn(type="button" value="{{_ 'filter'}}")
|
||||
button#resetBtn.filter-reset-btn
|
||||
span.reset-icon
|
||||
span.emoji-icon ❌
|
||||
span.emoji-icon
|
||||
i.fa.fa-times-thin
|
||||
span {{_ 'filter-clear'}}
|
||||
|
||||
// Right boards grid
|
||||
@@ -66,34 +72,41 @@ template(name="boardList")
|
||||
span.path-text {{currentMenuPath.text}}
|
||||
if BoardMultiSelection.isActive
|
||||
span.multiselection-hint
|
||||
span.emoji-icon 📌
|
||||
span.emoji-icon
|
||||
i.fa.fa-thumb-tack
|
||||
| {{_ 'multi-selection-active'}}
|
||||
.path-right
|
||||
if canModifyBoards
|
||||
if hasBoardsSelected
|
||||
button.js-archive-selected-boards.board-header-btn
|
||||
span.emoji-icon 📦
|
||||
span.emoji-icon
|
||||
i.fa.fa-archive
|
||||
span {{_ 'archive-board'}}
|
||||
button.js-duplicate-selected-boards.board-header-btn
|
||||
span.emoji-icon 📋
|
||||
span.emoji-icon
|
||||
i.fa.fa-clipboard
|
||||
span {{_ 'duplicate-board'}}
|
||||
a.board-header-btn.js-multiselection-activate(
|
||||
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
|
||||
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
|
||||
span.emoji-icon ☑️
|
||||
span.emoji-icon
|
||||
i.fa.fa-check-square-o
|
||||
if BoardMultiSelection.isActive
|
||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||
span.emoji-icon ✖
|
||||
span.emoji-icon
|
||||
i.fa.fa-times
|
||||
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
|
||||
li.js-add-board
|
||||
if isSelectedMenu 'templates'
|
||||
a.board-list-item.label(title="{{_ 'add-template-container'}}")
|
||||
span.emoji-icon ➕
|
||||
| {{_ 'add-template-container'}}
|
||||
span.emoji-icon
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-template-container'}}
|
||||
else
|
||||
a.board-list-item.label(title="{{_ 'add-board'}}")
|
||||
span.emoji-icon ➕
|
||||
| {{_ 'add-board'}}
|
||||
span.emoji-icon
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-board'}}
|
||||
each boards
|
||||
li.js-board(class="{{_id}} {{#if isStarred}}starred{{/if}} {{colorClass}} {{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}", draggable="true")
|
||||
if isInvited
|
||||
@@ -118,8 +131,9 @@ template(name="boardList")
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
span.board-handle(title="{{_ 'drag-board'}}")
|
||||
span.emoji-icon ↕️
|
||||
|
||||
span.emoji-icon
|
||||
i.fa.fa-arrows
|
||||
|
||||
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
span.details
|
||||
span.board-list-item-name(title="{{_ 'template-container'}}")
|
||||
@@ -132,20 +146,22 @@ template(name="boardList")
|
||||
span.js-has-spenttime-cards(
|
||||
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
span.emoji-icon ⏱️
|
||||
span.emoji-icon
|
||||
i.fa.fa-clock-o
|
||||
span.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
span.emoji-icon
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
else
|
||||
.board-list-item
|
||||
if BoardMultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
span.board-handle(title="{{_ 'drag-board'}}")
|
||||
span.emoji-icon ↕️
|
||||
|
||||
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'}}")
|
||||
@@ -170,22 +186,29 @@ template(name="boardList")
|
||||
span.js-has-spenttime-cards(
|
||||
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
span.emoji-icon ⏱️
|
||||
span.emoji-icon
|
||||
i.fa.fa-clock-o
|
||||
a.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
span.emoji-icon
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
|
||||
template(name="boardListHeaderBar")
|
||||
h1 {{_ title }}
|
||||
//.board-header-btns.right
|
||||
// a.board-header-btn.js-open-archived-board
|
||||
// i.fa.fa-archive
|
||||
// span {{_ 'archives'}}
|
||||
// a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
|
||||
// i.fa.fa-clone
|
||||
// span {{_ 'templates'}}
|
||||
//
|
||||
a.board-header-btn.js-open-archived-board
|
||||
//
|
||||
i.fa.fa-archive
|
||||
//
|
||||
span {{_ 'archives'}}
|
||||
//
|
||||
a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
|
||||
//
|
||||
i.fa.fa-clone
|
||||
//
|
||||
span {{_ 'templates'}}
|
||||
|
||||
// Recursive template for workspaces tree
|
||||
template(name="workspaceTree")
|
||||
@@ -195,19 +218,42 @@ template(name="workspaceTree")
|
||||
li.workspace-node(class="{{#if $eq id selectedWorkspaceId}}active{{/if}}" data-workspace-id="{{id}}" draggable="true")
|
||||
.workspace-node-content
|
||||
span.workspace-drag-handle
|
||||
span.emoji-icon ↕️
|
||||
|
||||
a.js-select-workspace(data-id="{{id}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-arrows
|
||||
|
||||
a.js-select-workspace.js-select-space(data-id="{{id}}")
|
||||
span.workspace-icon
|
||||
if icon
|
||||
+viewer
|
||||
= icon
|
||||
span.workspace-icon-value(style="{{#if iconColor}}color: {{iconColor}};{{/if}}")
|
||||
+viewer
|
||||
= icon
|
||||
else
|
||||
span.emoji-icon 📁
|
||||
span.emoji-icon(style="{{#if iconColor}}color: {{iconColor}};{{/if}}")
|
||||
i.fa.fa-folder
|
||||
span.workspace-name= name
|
||||
a.js-edit-workspace(data-id="{{id}}" title="{{_ 'allboards.edit-workspace'}}")
|
||||
span.emoji-icon ✏️
|
||||
a.js-open-workspace-menu(data-id="{{id}}" title="{{_ 'allboards.workspace-menu'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-bars
|
||||
span.workspace-count {{workspaceCount id}}
|
||||
a.js-add-subworkspace(data-id="{{id}}" title="{{_ 'allboards.add-subworkspace'}}") +
|
||||
if children
|
||||
+workspaceTree(nodes=children selectedWorkspaceId=selectedWorkspaceId)
|
||||
|
||||
template(name="workspaceActionsPopup")
|
||||
form.workspace-actions-form.js-workspace-actions-form(data-id="{{id}}")
|
||||
label
|
||||
| {{_ 'allboards.edit-workspace-name'}}
|
||||
input.js-workspace-name(type="text" value="{{workspaceName}}" required)
|
||||
label
|
||||
| {{_ 'allboards.edit-workspace-icon'}}
|
||||
input.js-workspace-icon(type="text" value="{{workspaceIcon}}")
|
||||
input.primary.wide(type="submit" value="{{_ 'save'}}")
|
||||
hr
|
||||
form.workspace-subspace-form.js-workspace-subspace-form(data-id="{{id}}")
|
||||
label
|
||||
| {{_ 'allboards.add-subworkspace-prompt'}}
|
||||
input.js-subworkspace-name(type="text")
|
||||
button.primary.wide(type="submit") {{_ 'allboards.add-subworkspace'}}
|
||||
hr
|
||||
.workspace-delete-block
|
||||
span.js-workspace-delete-error(style="color: #c00; display: block; margin: 4px 0;")
|
||||
button.js-delete-workspace.negate.wide(type="button" data-id="{{id}}") {{_ 'delete'}}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,43 @@
|
||||
.calendar-view .fc {
|
||||
--fc-button-text-color: #333;
|
||||
--fc-button-bg-color: #f5f5f5;
|
||||
--fc-button-border-color: rgba(0, 0, 0, 0.2);
|
||||
--fc-button-hover-bg-color: #e6e6e6;
|
||||
--fc-button-hover-border-color: rgba(0, 0, 0, 0.25);
|
||||
--fc-button-active-bg-color: #d9d9d9;
|
||||
--fc-button-active-border-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.calendar-view .fc .fc-button-primary {
|
||||
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||
0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
background-image: linear-gradient(to bottom, #fff 0%, #e6e6e6 100%);
|
||||
}
|
||||
|
||||
.calendar-view .fc .fc-button-primary:focus,
|
||||
.calendar-view .fc .fc-button-primary:not(:disabled).fc-button-active,
|
||||
.calendar-view .fc .fc-button-primary:not(:disabled):active {
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),
|
||||
0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.calendar-create-close {
|
||||
min-height: auto !important;
|
||||
min-width: auto !important;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
color: #666 !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.calendar-create-close:hover,
|
||||
.calendar-create-close:focus {
|
||||
background: transparent !important;
|
||||
color: #111 !important;
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ template(name="miniboard")
|
||||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
span.drag-handle(title="{{_ 'dragBoard'}}") ↕️
|
||||
span.drag-handle(title="{{_ 'dragBoard'}}")
|
||||
i.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
||||
@@ -164,32 +164,32 @@
|
||||
margin: 5px 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
.original-positions-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
.original-positions-header .btn {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.original-positions-filters .btn-group {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.original-position-item-header {
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
|
||||
.entity-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
||||
.original-position-item-details {
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
@@ -203,60 +203,60 @@
|
||||
border-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
|
||||
.original-positions-content {
|
||||
background-color: #1a202c;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
|
||||
.original-position-item {
|
||||
background-color: #2d3748;
|
||||
border-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
|
||||
.original-position-item:hover {
|
||||
background-color: #4a5568;
|
||||
border-color: #718096;
|
||||
}
|
||||
|
||||
|
||||
.original-position-item-header {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
|
||||
.original-position-item-header i {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
|
||||
.entity-name {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
|
||||
.entity-id {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
|
||||
.original-position-description {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
|
||||
.original-title {
|
||||
background-color: #4a5568;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
|
||||
.original-title strong {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
|
||||
.original-position-date {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
|
||||
.no-original-positions {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
|
||||
.no-original-positions i {
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
<template name="originalPositionsView">
|
||||
<div class="original-positions-view">
|
||||
<div class="original-positions-header">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="{{toggleOriginalPositions}}">
|
||||
<i class="fa fa-history"></i>
|
||||
{{#if isShowingOriginalPositions}}Hide{{else}}Show{{/if}} Original Positions
|
||||
</button>
|
||||
|
||||
{{#if isShowingOriginalPositions}}
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="{{refreshHistory}}">
|
||||
<i class="fa fa-refresh"></i> Refresh
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if isShowingOriginalPositions}}
|
||||
<div class="original-positions-content">
|
||||
{{#if isLoading}}
|
||||
<div class="original-positions-loading">
|
||||
<i class="fa fa-spinner fa-spin"></i> Loading original positions...
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="original-positions-filters">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'all'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'all'}}">
|
||||
All
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'swimlane'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'swimlane'}}">
|
||||
<i class="fa fa-bars"></i> Swimlanes
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'list'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'list'}}">
|
||||
<i class="fa fa-columns"></i> Lists
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'card'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'card'}}">
|
||||
<i class="fa fa-sticky-note"></i> Cards
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="original-positions-list">
|
||||
{{#each getFilteredHistory}}
|
||||
<div class="original-position-item">
|
||||
<div class="original-position-item-header">
|
||||
<i class="fa {{getEntityTypeIcon entityType}}"></i>
|
||||
<span class="entity-type">{{getEntityTypeLabel entityType}}</span>
|
||||
<span class="entity-name">{{getEntityDisplayName this}}</span>
|
||||
<span class="entity-id">({{entityId}})</span>
|
||||
</div>
|
||||
<div class="original-position-item-details">
|
||||
<div class="original-position-description">
|
||||
{{getEntityOriginalPositionDescription this}}
|
||||
</div>
|
||||
{{#if originalTitle}}
|
||||
<div class="original-title">
|
||||
<strong>Original title:</strong> {{originalTitle}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="original-position-date">
|
||||
<small class="text-muted">Created: {{formatDate createdAt}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="no-original-positions">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
No original position data available for this board.
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,57 @@
|
||||
template(name="originalPositionsView")
|
||||
.original-positions-view
|
||||
.original-positions-header
|
||||
button.btn.btn-sm.btn-outline-secondary.js-toggle-original-positions
|
||||
i.fa.fa-history
|
||||
if isShowingOriginalPositions
|
||||
| Hide Original Positions
|
||||
else
|
||||
| Show Original Positions
|
||||
|
||||
if isShowingOriginalPositions
|
||||
button.btn.btn-sm.btn-outline-primary.js-refresh-history
|
||||
i.fa.fa-refresh
|
||||
| Refresh
|
||||
|
||||
if isShowingOriginalPositions
|
||||
.original-positions-content
|
||||
if isLoading
|
||||
.original-positions-loading
|
||||
i.fa.fa-spinner.fa-spin
|
||||
| Loading original positions...
|
||||
else
|
||||
.original-positions-filters
|
||||
.btn-group.btn-group-sm(role="group")
|
||||
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'all'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="all")
|
||||
| All
|
||||
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'swimlane'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="swimlane")
|
||||
i.fa.fa-bars
|
||||
| Swimlanes
|
||||
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'list'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="list")
|
||||
i.fa.fa-columns
|
||||
| Lists
|
||||
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'card'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="card")
|
||||
i.fa.fa-sticky-note
|
||||
| Cards
|
||||
|
||||
.original-positions-list
|
||||
each getFilteredHistory
|
||||
.original-position-item
|
||||
.original-position-item-header
|
||||
i.fa(class="{{getEntityTypeIcon entityType}}")
|
||||
span.entity-type {{getEntityTypeLabel entityType}}
|
||||
span.entity-name {{getEntityDisplayName this}}
|
||||
span.entity-id ({{entityId}})
|
||||
.original-position-item-details
|
||||
.original-position-description
|
||||
| {{getEntityOriginalPositionDescription this}}
|
||||
if originalTitle
|
||||
.original-title
|
||||
strong Original title:
|
||||
| {{originalTitle}}
|
||||
.original-position-date
|
||||
small.text-muted Created: {{formatDate createdAt}}
|
||||
else
|
||||
.no-original-positions
|
||||
i.fa.fa-info-circle
|
||||
| No original position data available for this board.
|
||||
@@ -1,99 +1,78 @@
|
||||
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import './originalPositionsView.html';
|
||||
|
||||
/**
|
||||
* Component to display original positions for all entities on a board
|
||||
*/
|
||||
class OriginalPositionsViewComponent extends BlazeComponent {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
this.showOriginalPositions = new ReactiveVar(false);
|
||||
this.boardHistory = new ReactiveVar([]);
|
||||
this.isLoading = new ReactiveVar(false);
|
||||
this.filterType = new ReactiveVar('all'); // 'all', 'swimlane', 'list', 'card'
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
super.onRendered();
|
||||
this.loadBoardHistory();
|
||||
}
|
||||
Template.originalPositionsView.onCreated(function () {
|
||||
this.showOriginalPositions = new ReactiveVar(false);
|
||||
this.boardHistory = new ReactiveVar([]);
|
||||
this.isLoading = new ReactiveVar(false);
|
||||
this.filterType = new ReactiveVar('all'); // 'all', 'swimlane', 'list', 'card'
|
||||
|
||||
loadBoardHistory() {
|
||||
const tpl = this;
|
||||
|
||||
this.loadBoardHistory = function () {
|
||||
const boardId = Session.get('currentBoard');
|
||||
if (!boardId) return;
|
||||
|
||||
this.isLoading.set(true);
|
||||
|
||||
tpl.isLoading.set(true);
|
||||
|
||||
Meteor.call('positionHistory.getBoardHistory', boardId, (error, result) => {
|
||||
this.isLoading.set(false);
|
||||
tpl.isLoading.set(false);
|
||||
if (error) {
|
||||
console.error('Error loading board history:', error);
|
||||
this.boardHistory.set([]);
|
||||
tpl.boardHistory.set([]);
|
||||
} else {
|
||||
this.boardHistory.set(result);
|
||||
tpl.boardHistory.set(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
toggleOriginalPositions() {
|
||||
this.showOriginalPositions.set(!this.showOriginalPositions.get());
|
||||
}
|
||||
Template.originalPositionsView.onRendered(function () {
|
||||
this.loadBoardHistory();
|
||||
});
|
||||
|
||||
Template.originalPositionsView.helpers({
|
||||
isShowingOriginalPositions() {
|
||||
return this.showOriginalPositions.get();
|
||||
}
|
||||
return Template.instance().showOriginalPositions.get();
|
||||
},
|
||||
|
||||
isLoading() {
|
||||
return this.isLoading.get();
|
||||
}
|
||||
return Template.instance().isLoading.get();
|
||||
},
|
||||
|
||||
getBoardHistory() {
|
||||
return this.boardHistory.get();
|
||||
}
|
||||
return Template.instance().boardHistory.get();
|
||||
},
|
||||
|
||||
getFilteredHistory() {
|
||||
const history = this.getBoardHistory();
|
||||
const filterType = this.filterType.get();
|
||||
|
||||
const tpl = Template.instance();
|
||||
const history = tpl.boardHistory.get();
|
||||
const filterType = tpl.filterType.get();
|
||||
|
||||
if (filterType === 'all') {
|
||||
return history;
|
||||
}
|
||||
|
||||
|
||||
return history.filter(item => item.entityType === filterType);
|
||||
}
|
||||
},
|
||||
|
||||
getSwimlanesHistory() {
|
||||
return this.getBoardHistory().filter(item => item.entityType === 'swimlane');
|
||||
}
|
||||
|
||||
getListsHistory() {
|
||||
return this.getBoardHistory().filter(item => item.entityType === 'list');
|
||||
}
|
||||
|
||||
getCardsHistory() {
|
||||
return this.getBoardHistory().filter(item => item.entityType === 'card');
|
||||
}
|
||||
|
||||
setFilterType(type) {
|
||||
this.filterType.set(type);
|
||||
}
|
||||
|
||||
getFilterType() {
|
||||
return this.filterType.get();
|
||||
}
|
||||
isFilterType(type) {
|
||||
return Template.instance().filterType.get() === type;
|
||||
},
|
||||
|
||||
getEntityDisplayName(entity) {
|
||||
const position = entity.originalPosition || {};
|
||||
return position.title || `Entity ${entity.entityId}`;
|
||||
}
|
||||
},
|
||||
|
||||
getEntityOriginalPositionDescription(entity) {
|
||||
const position = entity.originalPosition || {};
|
||||
let description = `Position: ${position.sort || 0}`;
|
||||
|
||||
|
||||
if (entity.entityType === 'list' && entity.originalSwimlaneId) {
|
||||
description += ` in swimlane ${entity.originalSwimlaneId}`;
|
||||
} else if (entity.entityType === 'card') {
|
||||
@@ -104,9 +83,9 @@ class OriginalPositionsViewComponent extends BlazeComponent {
|
||||
description += ` in list ${entity.originalListId}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return description;
|
||||
}
|
||||
},
|
||||
|
||||
getEntityTypeIcon(entityType) {
|
||||
switch (entityType) {
|
||||
@@ -119,7 +98,7 @@ class OriginalPositionsViewComponent extends BlazeComponent {
|
||||
default:
|
||||
return 'fa-question';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getEntityTypeLabel(entityType) {
|
||||
switch (entityType) {
|
||||
@@ -132,17 +111,24 @@ class OriginalPositionsViewComponent extends BlazeComponent {
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
return new Date(date).toLocaleString();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
refreshHistory() {
|
||||
this.loadBoardHistory();
|
||||
}
|
||||
}
|
||||
Template.originalPositionsView.events({
|
||||
'click .js-toggle-original-positions'(evt, tpl) {
|
||||
tpl.showOriginalPositions.set(!tpl.showOriginalPositions.get());
|
||||
},
|
||||
|
||||
OriginalPositionsViewComponent.register('originalPositionsView');
|
||||
'click .js-refresh-history'(evt, tpl) {
|
||||
tpl.loadBoardHistory();
|
||||
},
|
||||
|
||||
export default OriginalPositionsViewComponent;
|
||||
'click .js-filter-type'(evt, tpl) {
|
||||
const type = evt.currentTarget.dataset.filterType;
|
||||
tpl.filterType.set(type);
|
||||
},
|
||||
});
|
||||
@@ -55,6 +55,12 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.attachment-actions a {
|
||||
margin-left: 16px;
|
||||
}
|
||||
.attachment-actions a:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
.add-attachment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -106,6 +112,9 @@
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 4em;
|
||||
position: absolute;
|
||||
right: 50px;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
/* Upload progress indicators for drag-and-drop uploads */
|
||||
@@ -241,10 +250,6 @@
|
||||
.js-card-details.is-dragging-over {
|
||||
border: 2px dashed #007bff !important;
|
||||
background: rgba(0, 123, 255, 0.05) !important;
|
||||
}
|
||||
top: 0;
|
||||
right: 8px;
|
||||
position: absolute;
|
||||
}
|
||||
.attachment-arrow {
|
||||
font-size: 4em;
|
||||
@@ -253,6 +258,20 @@
|
||||
align-self: center;
|
||||
margin: 0 20px;
|
||||
}
|
||||
#prev-attachment {
|
||||
font-size: 4em;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
margin-left: 70px;
|
||||
}
|
||||
#next-attachment {
|
||||
font-size: 4em;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
margin-right: 70px;
|
||||
}
|
||||
#viewer-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -266,6 +285,13 @@
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
#video-viewer {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
#audio-viewer {
|
||||
max-width: 100%;
|
||||
}
|
||||
#pdf-viewer {
|
||||
width: 40vw;
|
||||
height: 100%;
|
||||
@@ -300,9 +326,19 @@
|
||||
}
|
||||
#prev-attachment {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
bottom: 2.2em;
|
||||
font-size: 1.6em;
|
||||
padding: 16px;
|
||||
margin-left: 0;
|
||||
}
|
||||
#next-attachment {
|
||||
right: 0;
|
||||
position: absolute;
|
||||
bottom: 2.2em;
|
||||
font-size: 1.6em;
|
||||
padding: 16px;
|
||||
margin-right: 0;
|
||||
}
|
||||
#pdf-viewer {
|
||||
width: 100%;
|
||||
|
||||
@@ -34,10 +34,11 @@ template(name="attachmentViewer")
|
||||
#viewer-overlay.hidden
|
||||
#viewer-top-bar
|
||||
span#attachment-name
|
||||
a#viewer-close ❌
|
||||
a#viewer-close
|
||||
i.fa.fa-times-thin
|
||||
|
||||
#viewer-container
|
||||
| ◀️
|
||||
i.fa.fa-caret-left#prev-attachment
|
||||
#viewer-content
|
||||
img#image-viewer.hidden
|
||||
video#video-viewer.hidden(controls="true")
|
||||
@@ -45,7 +46,7 @@ template(name="attachmentViewer")
|
||||
object#pdf-viewer.hidden(type="application/pdf")
|
||||
span.pdf-preview-error {{_ 'preview-pdf-not-supported' }}
|
||||
object#txt-viewer.hidden(type="text/plain")
|
||||
| ▶️
|
||||
i.fa.fa-caret-right#next-attachment
|
||||
|
||||
template(name="attachmentGallery")
|
||||
|
||||
@@ -53,7 +54,7 @@ template(name="attachmentGallery")
|
||||
|
||||
if canModifyCard
|
||||
a.attachment-item.add-attachment.js-add-attachment
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
|
||||
each attachments
|
||||
|
||||
@@ -85,24 +86,27 @@ 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
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isWorker
|
||||
a.js-rename(title="{{_ 'rename'}}")
|
||||
| ✏️
|
||||
i.fa.fa-pencil-square-o
|
||||
a.js-confirm-delete(title="{{_ 'delete'}}")
|
||||
| 🗑️
|
||||
i.fa.fa-trash
|
||||
a.js-open-attachment-menu(data-attachment-link="{{link}}", title="{{_ 'attachmentActionsPopup-title'}}")
|
||||
| ☰
|
||||
|
||||
i.fa.fa-bars
|
||||
// Migration spinner overlay
|
||||
if isAttachmentMigrating _id
|
||||
.attachment-migration-overlay
|
||||
.migration-spinner
|
||||
| ⚙️
|
||||
i.fa.fa-cog.fa-spin
|
||||
.migration-text {{_ 'migrating-attachment'}}
|
||||
|
||||
template(name="attachmentActionsPopup")
|
||||
@@ -110,16 +114,12 @@ template(name="attachmentActionsPopup")
|
||||
li
|
||||
if isImage
|
||||
a(class="{{#if isCover}}js-remove-cover{{else}}js-add-cover{{/if}}")
|
||||
| 📖
|
||||
| 🖼️
|
||||
if isCover
|
||||
| {{_ 'remove-cover'}}
|
||||
else
|
||||
| {{_ 'add-cover'}}
|
||||
i.fa.fa-picture-o
|
||||
| {{#if isCover}}{{_ 'remove-cover'}}{{else}}{{_ 'add-cover'}}{{/if}}
|
||||
if currentUser.isBoardAdmin
|
||||
if isImage
|
||||
a(class="{{#if isBackgroundImage}}js-remove-background-image{{else}}js-add-background-image{{/if}}")
|
||||
| 🖼️
|
||||
i.fa.fa-picture-o
|
||||
if isBackgroundImage
|
||||
| {{_ 'remove-background-image'}}
|
||||
else
|
||||
@@ -127,19 +127,19 @@ template(name="attachmentActionsPopup")
|
||||
|
||||
if $neq versions.original.storage "fs"
|
||||
a.js-move-storage-fs
|
||||
| ▶️
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'attachment-move-storage-fs'}}
|
||||
|
||||
if $neq versions.original.storage "gridfs"
|
||||
if versions.original.storage
|
||||
a.js-move-storage-gridfs
|
||||
| ▶️
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'attachment-move-storage-gridfs'}}
|
||||
|
||||
if $neq versions.original.storage "s3"
|
||||
if versions.original.storage
|
||||
a.js-move-storage-s3
|
||||
| ▶️
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'attachment-move-storage-s3'}}
|
||||
|
||||
template(name="attachmentRenamePopup")
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
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';
|
||||
|
||||
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 +22,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 +43,8 @@ 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() {
|
||||
await Attachments.removeAsync(this._id);
|
||||
Popup.back();
|
||||
}),
|
||||
});
|
||||
@@ -196,8 +199,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 +218,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
|
||||
@@ -273,6 +272,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 +313,12 @@ Template.cardAttachmentsPopup.helpers({
|
||||
});
|
||||
|
||||
Template.cardAttachmentsPopup.events({
|
||||
'change .js-attach-file'(event, templateInstance) {
|
||||
async 'change .js-attach-file'(event, templateInstance) {
|
||||
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() {
|
||||
@@ -328,7 +347,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 +380,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,
|
||||
@@ -385,7 +404,7 @@ export function handleFileUpload(card, files) {
|
||||
config.meta.fileId = fileId;
|
||||
|
||||
try {
|
||||
const uploader = Attachments.insert(
|
||||
const uploader = await Attachments.insertAsync(
|
||||
config,
|
||||
false,
|
||||
);
|
||||
@@ -460,12 +479,12 @@ 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,
|
||||
@@ -474,7 +493,7 @@ Template.previewClipboardImagePopup.events({
|
||||
chunkSize: 'dynamic',
|
||||
};
|
||||
config.meta.fileId = fileId;
|
||||
const uploader = Attachments.insert(
|
||||
const uploader = await Attachments.insertAsync(
|
||||
config,
|
||||
false,
|
||||
);
|
||||
@@ -495,9 +514,9 @@ Template.previewClipboardImagePopup.events({
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.attachmentActionsPopup.helpers({
|
||||
isCover() {
|
||||
const ret = ReactiveCache.getCard(this.data().meta.cardId).coverId == this.data()._id;
|
||||
const ret = ReactiveCache.getCard(this.meta.cardId).coverId == this._id;
|
||||
return ret;
|
||||
},
|
||||
isBackgroundImage() {
|
||||
@@ -505,78 +524,72 @@ BlazeComponent.extendComponent({
|
||||
//return currentBoard.backgroundImageURL === $(".attachment-thumbnail-img").attr("src");
|
||||
return false;
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-add-cover'() {
|
||||
ReactiveCache.getCard(this.data().meta.cardId).setCover(this.data()._id);
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-remove-cover'() {
|
||||
ReactiveCache.getCard(this.data().meta.cardId).unsetCover();
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-add-background-image'() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
currentBoard.setBackgroundImageURL(attachmentActionsLink);
|
||||
Utils.setBackgroundImage(attachmentActionsLink);
|
||||
Popup.back();
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-remove-background-image'() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
currentBoard.setBackgroundImageURL("");
|
||||
Utils.setBackgroundImage("");
|
||||
Popup.back();
|
||||
Utils.reload();
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-move-storage-fs'() {
|
||||
Meteor.call('moveAttachmentToStorage', this.data()._id, "fs");
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-move-storage-gridfs'() {
|
||||
Meteor.call('moveAttachmentToStorage', this.data()._id, "gridfs");
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-move-storage-s3'() {
|
||||
Meteor.call('moveAttachmentToStorage', this.data()._id, "s3");
|
||||
Popup.back();
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}).register('attachmentActionsPopup');
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.attachmentActionsPopup.events({
|
||||
'click .js-add-cover'() {
|
||||
ReactiveCache.getCard(this.meta.cardId).setCover(this._id);
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-remove-cover'() {
|
||||
ReactiveCache.getCard(this.meta.cardId).unsetCover();
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-add-background-image'(event) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
currentBoard.setBackgroundImageURL(attachmentActionsLink);
|
||||
Utils.setBackgroundImage(attachmentActionsLink);
|
||||
Popup.back();
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-remove-background-image'(event) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
currentBoard.setBackgroundImageURL("");
|
||||
Utils.setBackgroundImage("");
|
||||
Popup.back();
|
||||
Utils.reload();
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-move-storage-fs'() {
|
||||
Meteor.call('moveAttachmentToStorage', this._id, "fs");
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-move-storage-gridfs'() {
|
||||
Meteor.call('moveAttachmentToStorage', this._id, "gridfs");
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-move-storage-s3'() {
|
||||
Meteor.call('moveAttachmentToStorage', this._id, "s3");
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
Template.attachmentRenamePopup.helpers({
|
||||
getNameWithoutExtension() {
|
||||
const ret = this.data().name.replace(new RegExp("\." + this.data().extension + "$"), "");
|
||||
const ret = this.name.replace(new RegExp("\." + this.extension + "$"), "");
|
||||
return ret;
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'keydown input.js-edit-attachment-name'(evt) {
|
||||
// enter = save
|
||||
if (evt.keyCode === 13) {
|
||||
this.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
'click button.js-submit-edit-attachment-name'(event) {
|
||||
// save button pressed
|
||||
event.preventDefault();
|
||||
const name = this.$('.js-edit-attachment-name')[0]
|
||||
.value
|
||||
.trim() + this.data().extensionWithDot;
|
||||
if (name === sanitizeText(name)) {
|
||||
Meteor.call('renameAttachment', this.data()._id, name);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}).register('attachmentRenamePopup');
|
||||
});
|
||||
|
||||
Template.attachmentRenamePopup.events({
|
||||
'keydown input.js-edit-attachment-name'(evt, tpl) {
|
||||
// enter = save
|
||||
if (evt.keyCode === 13) {
|
||||
tpl.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
'click button.js-submit-edit-attachment-name'(event, tpl) {
|
||||
// save button pressed
|
||||
event.preventDefault();
|
||||
const name = tpl.$('.js-edit-attachment-name')[0]
|
||||
.value
|
||||
.trim() + this.extensionWithDot;
|
||||
if (name === sanitizeText(name)) {
|
||||
Meteor.call('renameAttachment', this._id, name);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
// Template helpers for attachment migration status
|
||||
Template.registerHelper('attachmentMigrationStatus', function(attachmentId) {
|
||||
|
||||
@@ -6,10 +6,10 @@ template(name="cardCustomFieldsPopup")
|
||||
span.full-name
|
||||
= name
|
||||
if hasCustomField
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
hr
|
||||
a.quiet-button.full.js-settings
|
||||
| ⚙️
|
||||
i.fa.fa-cog
|
||||
span {{_ 'settings'}}
|
||||
|
||||
template(name="cardCustomField")
|
||||
@@ -55,9 +55,11 @@ template(name="cardCustomField-number")
|
||||
template(name="cardCustomField-checkbox")
|
||||
.js-checklist-item.checklist-item(class="{{#if data.value }}is-checked{{/if}}")
|
||||
if canModifyCard
|
||||
span.check-box-unicode {{#if data.value }}✅{{else}}⬜{{/if}}
|
||||
span.check-box-unicode
|
||||
i.fa(class="{{#if data.value}}fa-check-square{{else}}fa-square-o{{/if}}")
|
||||
else
|
||||
span.check-box-unicode {{#if data.value }}✅{{else}}⬜{{/if}}
|
||||
span.check-box-unicode
|
||||
i.fa(class="{{#if data.value}}fa-check-square{{else}}fa-square-o{{/if}}")
|
||||
|
||||
template(name="cardCustomField-currency")
|
||||
if canModifyCard
|
||||
@@ -97,6 +99,21 @@ template(name="cardCustomField-date")
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardCustomField-datePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="cardCustomField-dropdown")
|
||||
if canModifyCard
|
||||
+inlinedForm(classNames="js-card-customfield-dropdown")
|
||||
|
||||
@@ -1,33 +1,41 @@
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { DatePicker } from '/client/lib/datepicker';
|
||||
import {
|
||||
setupDatePicker,
|
||||
datePickerRendered,
|
||||
datePickerHelpers,
|
||||
datePickerEvents,
|
||||
} from '/client/lib/datepicker';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatDateByUserPreference,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
import Cards from '/models/cards';
|
||||
import { CustomFieldStringTemplate } from '/client/lib/customFields'
|
||||
import { getCurrentCardFromContext } from '/client/lib/currentCard';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { getSidebarInstance } from '/client/features/sidebar/service';
|
||||
|
||||
Template.cardCustomFieldsPopup.helpers({
|
||||
hasCustomField() {
|
||||
const card = Utils.getCurrentCard();
|
||||
const card = getCurrentCardFromContext();
|
||||
if (!card) return false;
|
||||
const customFieldId = this._id;
|
||||
return card.customFieldIndex(customFieldId) > -1;
|
||||
},
|
||||
@@ -35,318 +43,297 @@ Template.cardCustomFieldsPopup.helpers({
|
||||
|
||||
Template.cardCustomFieldsPopup.events({
|
||||
'click .js-select-field'(event) {
|
||||
const card = Utils.getCurrentCard();
|
||||
const card = getCurrentCardFromContext();
|
||||
if (!card) return;
|
||||
const customFieldId = this._id;
|
||||
card.toggleCustomField(customFieldId);
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-settings'(event) {
|
||||
EscapeActions.executeUpTo('detailsPane');
|
||||
Sidebar.setView('customFields');
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar) {
|
||||
sidebar.setView('customFields');
|
||||
}
|
||||
event.preventDefault();
|
||||
},
|
||||
});
|
||||
|
||||
// cardCustomField
|
||||
const CardCustomField = BlazeComponent.extendComponent({
|
||||
Template.cardCustomField.helpers({
|
||||
getTemplate() {
|
||||
return `cardCustomField-${this.data().definition.type}`;
|
||||
},
|
||||
|
||||
onCreated() {
|
||||
const self = this;
|
||||
self.card = Utils.getCurrentCard();
|
||||
self.customFieldId = this.data()._id;
|
||||
return `cardCustomField-${this.definition.type}`;
|
||||
},
|
||||
});
|
||||
CardCustomField.register('cardCustomField');
|
||||
|
||||
Template.cardCustomField.onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
});
|
||||
|
||||
// cardCustomField-text
|
||||
(class extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
}
|
||||
Template['cardCustomField-text'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-customfield-text'(event) {
|
||||
event.preventDefault();
|
||||
const value = this.currentComponent().getValue();
|
||||
this.card.setCustomField(this.customFieldId, value);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}.register('cardCustomField-text'));
|
||||
Template['cardCustomField-text'].events({
|
||||
'submit .js-card-customfield-text'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const value = tpl.currentComponent ? tpl.currentComponent().getValue() : tpl.$('textarea').val();
|
||||
tpl.card.setCustomField(tpl.customFieldId, value);
|
||||
},
|
||||
});
|
||||
|
||||
// cardCustomField-number
|
||||
(class extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
}
|
||||
Template['cardCustomField-number'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-customfield-number'(event) {
|
||||
event.preventDefault();
|
||||
const value = parseInt(this.find('input').value, 10);
|
||||
this.card.setCustomField(this.customFieldId, value);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}.register('cardCustomField-number'));
|
||||
Template['cardCustomField-number'].events({
|
||||
'submit .js-card-customfield-number'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const value = parseInt(tpl.find('input').value, 10);
|
||||
tpl.card.setCustomField(tpl.customFieldId, value);
|
||||
},
|
||||
});
|
||||
|
||||
// cardCustomField-checkbox
|
||||
(class extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
}
|
||||
Template['cardCustomField-checkbox'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
});
|
||||
|
||||
toggleItem() {
|
||||
this.card.setCustomField(this.customFieldId, !this.data().value);
|
||||
}
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-checklist-item .check-box-unicode': this.toggleItem,
|
||||
'click .js-checklist-item .check-box-container': this.toggleItem,
|
||||
},
|
||||
];
|
||||
}
|
||||
}.register('cardCustomField-checkbox'));
|
||||
Template['cardCustomField-checkbox'].events({
|
||||
'click .js-checklist-item .check-box-unicode'(event, tpl) {
|
||||
tpl.card.setCustomField(tpl.customFieldId, !Template.currentData().value);
|
||||
},
|
||||
'click .js-checklist-item .check-box-container'(event, tpl) {
|
||||
tpl.card.setCustomField(tpl.customFieldId, !Template.currentData().value);
|
||||
},
|
||||
});
|
||||
|
||||
// cardCustomField-currency
|
||||
(class extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
|
||||
this.currencyCode = this.data().definition.settings.currencyCode;
|
||||
}
|
||||
Template['cardCustomField-currency'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
this.currencyCode = Template.currentData().definition.settings.currencyCode;
|
||||
});
|
||||
|
||||
Template['cardCustomField-currency'].helpers({
|
||||
formattedValue() {
|
||||
const locale = TAPi18n.getLanguage();
|
||||
|
||||
const tpl = Template.instance();
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: this.currencyCode,
|
||||
}).format(this.data().value);
|
||||
}
|
||||
currency: tpl.currencyCode,
|
||||
}).format(this.value);
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-customfield-currency'(event) {
|
||||
event.preventDefault();
|
||||
// To allow input separated by comma, the comma is replaced by a period.
|
||||
const value = Number(this.find('input').value.replace(/,/i, '.'), 10);
|
||||
this.card.setCustomField(this.customFieldId, value);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}.register('cardCustomField-currency'));
|
||||
Template['cardCustomField-currency'].events({
|
||||
'submit .js-card-customfield-currency'(event, tpl) {
|
||||
event.preventDefault();
|
||||
// To allow input separated by comma, the comma is replaced by a period.
|
||||
const value = Number(tpl.find('input').value.replace(/,/i, '.'), 10);
|
||||
tpl.card.setCustomField(tpl.customFieldId, value);
|
||||
},
|
||||
});
|
||||
|
||||
// cardCustomField-date
|
||||
(class extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.date = ReactiveVar();
|
||||
self.now = ReactiveVar(now());
|
||||
window.setInterval(() => {
|
||||
self.now.set(now());
|
||||
}, 60000);
|
||||
Template['cardCustomField-date'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
const self = this;
|
||||
self.date = ReactiveVar();
|
||||
self.now = ReactiveVar(now());
|
||||
window.setInterval(() => {
|
||||
self.now.set(now());
|
||||
}, 60000);
|
||||
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(self.data().value));
|
||||
});
|
||||
}
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().value));
|
||||
});
|
||||
});
|
||||
|
||||
Template['cardCustomField-date'].helpers({
|
||||
showWeek() {
|
||||
return getISOWeek(this.date.get()).toString();
|
||||
}
|
||||
|
||||
return getISOWeek(Template.instance().date.get()).toString();
|
||||
},
|
||||
showWeekOfYear() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (!user) {
|
||||
// For non-logged-in users, week of year is not shown
|
||||
return false;
|
||||
}
|
||||
return user.isShowWeekOfYear();
|
||||
}
|
||||
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||
}
|
||||
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
showISODate() {
|
||||
return this.date.get().toISOString();
|
||||
}
|
||||
|
||||
return Template.instance().date.get().toISOString();
|
||||
},
|
||||
classes() {
|
||||
const tpl = Template.instance();
|
||||
if (
|
||||
isBefore(this.date.get(), this.now.get(), 'minute') &&
|
||||
isBefore(this.now.get(), this.data().value, 'minute')
|
||||
isBefore(tpl.date.get(), tpl.now.get(), 'minute') &&
|
||||
isBefore(tpl.now.get(), this.value, 'minute')
|
||||
) {
|
||||
return 'current';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
},
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-start-on')} ${this.date.get().toLocaleString()}`;
|
||||
}
|
||||
return `${TAPi18n.__('card-start-on')} ${Template.instance().date.get().toLocaleString()}`;
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-edit-date': Popup.open('cardCustomField-date'),
|
||||
},
|
||||
];
|
||||
}
|
||||
}.register('cardCustomField-date'));
|
||||
Template['cardCustomField-date'].events({
|
||||
'click .js-edit-date': Popup.open('cardCustomField-date'),
|
||||
});
|
||||
|
||||
// cardCustomField-datePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.card = Utils.getCurrentCard();
|
||||
self.customFieldId = this.data()._id;
|
||||
this.data().value && this.date.set(new Date(this.data().value));
|
||||
}
|
||||
Template['cardCustomField-datePopup'].onCreated(function () {
|
||||
const data = Template.currentData();
|
||||
setupDatePicker(this, {
|
||||
initialDate: data.value ? data.value : undefined,
|
||||
});
|
||||
// Override card and store customFieldId for store/delete callbacks
|
||||
this.datePicker.card = getCurrentCardFromContext();
|
||||
this.customFieldId = data._id;
|
||||
});
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setCustomField(this.customFieldId, date);
|
||||
}
|
||||
Template['cardCustomField-datePopup'].onRendered(function () {
|
||||
datePickerRendered(this);
|
||||
});
|
||||
|
||||
_deleteDate() {
|
||||
this.card.setCustomField(this.customFieldId, '');
|
||||
}
|
||||
}.register('cardCustomField-datePopup'));
|
||||
Template['cardCustomField-datePopup'].helpers(datePickerHelpers());
|
||||
|
||||
Template['cardCustomField-datePopup'].events(datePickerEvents({
|
||||
storeDate(date) {
|
||||
this.datePicker.card.setCustomField(this.customFieldId, date);
|
||||
},
|
||||
deleteDate() {
|
||||
this.datePicker.card.setCustomField(this.customFieldId, '');
|
||||
},
|
||||
}));
|
||||
|
||||
// cardCustomField-dropdown
|
||||
(class extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
this._items = this.data().definition.settings.dropdownItems;
|
||||
this.items = this._items.slice(0);
|
||||
this.items.unshift({
|
||||
_id: '',
|
||||
name: TAPi18n.__('custom-field-dropdown-none'),
|
||||
});
|
||||
}
|
||||
Template['cardCustomField-dropdown'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
this._items = Template.currentData().definition.settings.dropdownItems;
|
||||
this.items = this._items.slice(0);
|
||||
this.items.unshift({
|
||||
_id: '',
|
||||
name: TAPi18n.__('custom-field-dropdown-none'),
|
||||
});
|
||||
});
|
||||
|
||||
Template['cardCustomField-dropdown'].helpers({
|
||||
items() {
|
||||
return Template.instance().items;
|
||||
},
|
||||
selectedItem() {
|
||||
const selected = this._items.find(item => {
|
||||
return item._id === this.data().value;
|
||||
const tpl = Template.instance();
|
||||
const selected = tpl._items.find(item => {
|
||||
return item._id === this.value;
|
||||
});
|
||||
return selected
|
||||
? selected.name
|
||||
: TAPi18n.__('custom-field-dropdown-unknown');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-customfield-dropdown'(event) {
|
||||
event.preventDefault();
|
||||
const value = this.find('select').value;
|
||||
this.card.setCustomField(this.customFieldId, value);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}.register('cardCustomField-dropdown'));
|
||||
Template['cardCustomField-dropdown'].events({
|
||||
'submit .js-card-customfield-dropdown'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const value = tpl.find('select').value;
|
||||
tpl.card.setCustomField(tpl.customFieldId, value);
|
||||
},
|
||||
});
|
||||
|
||||
// cardCustomField-stringtemplate
|
||||
class CardCustomFieldStringTemplate extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
|
||||
this.customField = new CustomFieldStringTemplate(this.data().definition);
|
||||
|
||||
this.stringtemplateItems = new ReactiveVar(this.data().value ?? []);
|
||||
}
|
||||
Template['cardCustomField-stringtemplate'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
this.customField = new CustomFieldStringTemplate(Template.currentData().definition);
|
||||
this.stringtemplateItems = new ReactiveVar(Template.currentData().value ?? []);
|
||||
});
|
||||
|
||||
Template['cardCustomField-stringtemplate'].helpers({
|
||||
formattedValue() {
|
||||
const ret = this.customField.getFormattedValue(this.data().value);
|
||||
const tpl = Template.instance();
|
||||
const ret = tpl.customField.getFormattedValue(this.value);
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
stringtemplateItems() {
|
||||
return Template.instance().stringtemplateItems.get();
|
||||
},
|
||||
});
|
||||
|
||||
getItems() {
|
||||
return Array.from(this.findAll('input'))
|
||||
.map(input => input.value)
|
||||
.filter(value => !!value.trim());
|
||||
}
|
||||
Template['cardCustomField-stringtemplate'].events({
|
||||
'submit .js-card-customfield-stringtemplate'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const items = tpl.stringtemplateItems.get();
|
||||
tpl.card.setCustomField(tpl.customFieldId, items);
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-customfield-stringtemplate'(event) {
|
||||
event.preventDefault();
|
||||
const items = this.stringtemplateItems.get();
|
||||
this.card.setCustomField(this.customFieldId, items);
|
||||
},
|
||||
'keydown .js-card-customfield-stringtemplate-item'(event, tpl) {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
|
||||
'keydown .js-card-customfield-stringtemplate-item'(event) {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
if (event.target.value.trim() || event.metaKey || event.ctrlKey) {
|
||||
const inputLast = tpl.find('input.last');
|
||||
|
||||
if (event.target.value.trim() || event.metaKey || event.ctrlKey) {
|
||||
const inputLast = this.find('input.last');
|
||||
let items = Array.from(tpl.findAll('input'))
|
||||
.map(input => input.value)
|
||||
.filter(value => !!value.trim());
|
||||
|
||||
let items = this.getItems();
|
||||
if (event.target === inputLast) {
|
||||
inputLast.value = '';
|
||||
} else if (event.target.nextSibling === inputLast) {
|
||||
inputLast.focus();
|
||||
} else {
|
||||
event.target.blur();
|
||||
|
||||
if (event.target === inputLast) {
|
||||
inputLast.value = '';
|
||||
} else if (event.target.nextSibling === inputLast) {
|
||||
inputLast.focus();
|
||||
} else {
|
||||
event.target.blur();
|
||||
const idx = Array.from(tpl.findAll('input')).indexOf(
|
||||
event.target,
|
||||
);
|
||||
items.splice(idx + 1, 0, '');
|
||||
|
||||
const idx = Array.from(this.findAll('input')).indexOf(
|
||||
event.target,
|
||||
);
|
||||
items.splice(idx + 1, 0, '');
|
||||
Tracker.afterFlush(() => {
|
||||
const element = tpl.findAll('input')[idx + 1];
|
||||
element.focus();
|
||||
element.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
Tracker.afterFlush(() => {
|
||||
const element = this.findAll('input')[idx + 1];
|
||||
element.focus();
|
||||
element.value = '';
|
||||
});
|
||||
}
|
||||
tpl.stringtemplateItems.set(items);
|
||||
}
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
tpl.find('button[type=submit]').click();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
this.stringtemplateItems.set(items);
|
||||
}
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
this.find('button[type=submit]').click();
|
||||
}
|
||||
}
|
||||
},
|
||||
'blur .js-card-customfield-stringtemplate-item'(event, tpl) {
|
||||
if (
|
||||
!event.target.value.trim() ||
|
||||
event.target === tpl.find('input.last')
|
||||
) {
|
||||
const items = Array.from(tpl.findAll('input'))
|
||||
.map(input => input.value)
|
||||
.filter(value => !!value.trim());
|
||||
tpl.stringtemplateItems.set(items);
|
||||
tpl.find('input.last').value = '';
|
||||
}
|
||||
},
|
||||
|
||||
'blur .js-card-customfield-stringtemplate-item'(event) {
|
||||
if (
|
||||
!event.target.value.trim() ||
|
||||
event.target === this.find('input.last')
|
||||
) {
|
||||
const items = this.getItems();
|
||||
this.stringtemplateItems.set(items);
|
||||
this.find('input.last').value = '';
|
||||
}
|
||||
},
|
||||
|
||||
'click .js-close-inlined-form'(event) {
|
||||
this.stringtemplateItems.set(this.data().value ?? []);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
CardCustomFieldStringTemplate.register('cardCustomField-stringtemplate');
|
||||
'click .js-close-inlined-form'(event, tpl) {
|
||||
tpl.stringtemplateItems.set(Template.currentData().value ?? []);
|
||||
},
|
||||
});
|
||||
@@ -14,6 +14,70 @@ template(name="dateBadge")
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardReceivedDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardStartDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardDueDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardEndDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="dateCustomField")
|
||||
a(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
@@ -22,6 +86,46 @@ template(name="dateCustomField")
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardCustomFieldDate")
|
||||
a(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="voteEndDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="pokerEndDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="minicardReceivedDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date.received-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
@@ -95,58 +199,91 @@ template(name="minicardCustomFieldDate")
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="editCardReceivedDatePopup")
|
||||
form.edit-card-received-date
|
||||
.datepicker
|
||||
// Date input field (existing)
|
||||
// Insert calendar selector right after date input
|
||||
.calendar-selector
|
||||
label(for="calendar-received") 🗓️
|
||||
input#calendar-received.js-calendar-date(type="date")
|
||||
// Time input field (if present)
|
||||
.clear-date
|
||||
a.js-clear-date {{_ 'clear'}}
|
||||
.datepicker-actions
|
||||
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="editCardStartDatePopup")
|
||||
form.edit-card-start-date
|
||||
.datepicker
|
||||
// Date input field (existing)
|
||||
.calendar-selector
|
||||
label(for="calendar-start") 🗓️
|
||||
input#calendar-start.js-calendar-date(type="date")
|
||||
// Time input field (if present)
|
||||
.clear-date
|
||||
a.js-clear-date {{_ 'clear'}}
|
||||
.datepicker-actions
|
||||
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="editCardDueDatePopup")
|
||||
form.edit-card-due-date
|
||||
.datepicker
|
||||
// Date input field (existing)
|
||||
.calendar-selector
|
||||
label(for="calendar-due") 🗓️
|
||||
input#calendar-due.js-calendar-date(type="date")
|
||||
// Time input field (if present)
|
||||
.clear-date
|
||||
a.js-clear-date {{_ 'clear'}}
|
||||
.datepicker-actions
|
||||
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="editCardEndDatePopup")
|
||||
form.edit-card-end-date
|
||||
.datepicker
|
||||
// Date input field (existing)
|
||||
.calendar-selector
|
||||
label(for="calendar-end") 🗓️
|
||||
input#calendar-end.js-calendar-date(type="date")
|
||||
// Time input field (if present)
|
||||
.clear-date
|
||||
a.js-clear-date {{_ 'clear'}}
|
||||
.datepicker-actions
|
||||
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="editVoteEndDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="editPokerEndDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
+534
-402
File diff suppressed because it is too large.
Load diff
@@ -1,37 +1,29 @@
|
||||
const descriptionFormIsOpen = new ReactiveVar(false);
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onDestroyed() {
|
||||
descriptionFormIsOpen.set(false);
|
||||
$('.note-popover').hide();
|
||||
},
|
||||
Template.descriptionForm.onDestroyed(function () {
|
||||
descriptionFormIsOpen.set(false);
|
||||
$('.note-popover').hide();
|
||||
});
|
||||
|
||||
Template.descriptionForm.helpers({
|
||||
descriptionFormIsOpen() {
|
||||
return descriptionFormIsOpen.get();
|
||||
},
|
||||
});
|
||||
|
||||
getInput() {
|
||||
return this.$('.js-new-description-input');
|
||||
Template.descriptionForm.events({
|
||||
async 'submit .js-card-description'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const description = tpl.currentComponent ? tpl.currentComponent().getValue() : tpl.$('textarea').val();
|
||||
await this.setDescription(description);
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-description'(event) {
|
||||
event.preventDefault();
|
||||
const description = this.currentComponent().getValue();
|
||||
this.data().setDescription(description);
|
||||
},
|
||||
// Pressing Ctrl+Enter should submit the form
|
||||
'keydown form textarea'(evt) {
|
||||
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
|
||||
const submitButton = this.find('button[type=submit]');
|
||||
if (submitButton) {
|
||||
submitButton.click();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
// Pressing Ctrl+Enter should submit the form
|
||||
'keydown form textarea'(evt, tpl) {
|
||||
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
|
||||
const submitButton = tpl.find('button[type=submit]');
|
||||
if (submitButton) {
|
||||
submitButton.click();
|
||||
}
|
||||
}
|
||||
},
|
||||
}).register('descriptionForm');
|
||||
});
|
||||
@@ -1,23 +1,25 @@
|
||||
/* Date Format Selector */
|
||||
.card-details-item-date-format {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-details-item-date-format .card-details-item-title {
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 6px;
|
||||
color: #333;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.card-details-item-date-format .js-date-format-selector {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.card-details-item-date-format .js-date-format-selector:focus {
|
||||
@@ -27,18 +29,18 @@
|
||||
}
|
||||
|
||||
.assignee {
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
position: relative;
|
||||
float: left;
|
||||
height: clamp(24px, 3.5vw, 36px);
|
||||
width: clamp(24px, 3.5vw, 36px);
|
||||
margin: .3vh;
|
||||
margin: 0.3vh;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
z-index: 1;
|
||||
text-decoration: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.04);
|
||||
}
|
||||
.assignee .avatar {
|
||||
overflow: hidden;
|
||||
@@ -51,12 +53,18 @@
|
||||
background-color: #dbdbdb;
|
||||
color: #444;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
.assignee .avatar.avatar-image {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.assignee .assignee-presence-status {
|
||||
background-color: #b3b3b3;
|
||||
@@ -67,7 +75,6 @@
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
border: 1px solid #fff;
|
||||
z-index: 15;
|
||||
}
|
||||
.assignee .assignee-presence-status.active {
|
||||
@@ -91,6 +98,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 0 2px #bfbfbf inset;
|
||||
transition: box-shadow 0.12s;
|
||||
}
|
||||
.assignee.add-assignee:hover,
|
||||
.assignee.add-assignee.is-active {
|
||||
@@ -102,20 +110,22 @@
|
||||
background-color: rgba(0,0,0,0.875);
|
||||
color: #fff;
|
||||
border-radius: 0.7vw;
|
||||
font-size: 0.98em;
|
||||
}
|
||||
|
||||
.card-details {
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: min(600px, 80vw);
|
||||
will-change: flex-basis;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: #f7f7f7;
|
||||
border-radius: bottom 0.4vw;
|
||||
border-radius: 0 0 0.4vw 0.4vw;
|
||||
z-index: 30;
|
||||
animation: flexGrowIn 0.1s;
|
||||
box-shadow: 0 0 0.9vh 0 #b3b3b3;
|
||||
transition: flex-basis 0.1s;
|
||||
transition: flex-basis 0.1s, box-shadow 0.15s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -126,12 +136,12 @@ body.desktop-mode .card-details:not(.card-details-popup) {
|
||||
max-width: 800px;
|
||||
flex-basis: auto;
|
||||
border-radius: 8px;
|
||||
z-index: 100;
|
||||
z-index: 2001;
|
||||
}
|
||||
|
||||
/* Default position for first card or when dragged */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):not([style*="left"]):not([style*="top"]) {
|
||||
top: 50px;
|
||||
top: 8px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
@@ -139,23 +149,23 @@ body.desktop-mode .card-details:not(.card-details-popup):not([style*="left"]):no
|
||||
|
||||
/* Stagger positions for multiple cards using nth-of-type */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(1) {
|
||||
top: 50px;
|
||||
top: 8px;
|
||||
left: 20px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(2) {
|
||||
top: 80px;
|
||||
top: 38px;
|
||||
left: 50px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(3) {
|
||||
top: 110px;
|
||||
top: 68px;
|
||||
left: 80px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(4) {
|
||||
top: 140px;
|
||||
top: 98px;
|
||||
left: 110px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(5) {
|
||||
top: 170px;
|
||||
top: 128px;
|
||||
left: 140px;
|
||||
}
|
||||
|
||||
@@ -167,7 +177,7 @@ body.desktop-mode .card-details:not(.card-details-popup):not(.card-details-colla
|
||||
|
||||
/* Collapsed card state - hide content and set height to title row only */
|
||||
.card-details.card-details-collapsed .card-details-canvas > *:not(.card-details-header) {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
.card-details.card-details-collapsed {
|
||||
height: auto !important;
|
||||
@@ -186,17 +196,19 @@ body.desktop-mode .card-details.card-details-collapsed {
|
||||
}
|
||||
.card-details .card-details-header {
|
||||
margin: 0 -20px 5px;
|
||||
padding: 7px 20px;
|
||||
padding: 8px 20px;
|
||||
background: #ededed;
|
||||
border-bottom: 1px solid #dbdbdb;
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
z-index: 500;
|
||||
display: flow-root;
|
||||
min-height: 44px;
|
||||
}
|
||||
.card-details .card-details-header .card-number {
|
||||
color: #b3b3b3;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* Collapse toggle triangle */
|
||||
@@ -209,9 +221,10 @@ body.desktop-mode .card-details.card-details-collapsed {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: #000;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Drag handle */
|
||||
.card-details .card-details-header .card-drag-handle {
|
||||
font-size: 20px;
|
||||
padding: 8px 10px;
|
||||
@@ -220,6 +233,8 @@ body.desktop-mode .card-details.card-details-collapsed {
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-details .card-details-header .close-card-details,
|
||||
@@ -241,6 +256,9 @@ body.desktop-mode .card-details.card-details-collapsed {
|
||||
margin-right: -8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
transition: color 0.13s;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details-mobile-web,
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
@@ -263,6 +281,8 @@ body.desktop-mode .card-details.card-details-collapsed {
|
||||
.card-details .card-details-header .card-details-menu {
|
||||
font-size: 17px;
|
||||
padding: 10px;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.card-details .card-details-header .card-details-menu-mobile-web {
|
||||
font-size: 17px;
|
||||
@@ -297,7 +317,7 @@ body.desktop-mode .card-details.card-details-collapsed {
|
||||
.card-details .card-label,
|
||||
.card-details .viewer {
|
||||
font-size: inherit;
|
||||
line-height: 1.4;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.card-details .card-details-header .card-details-watch {
|
||||
font-size: 17px;
|
||||
@@ -306,9 +326,13 @@ body.desktop-mode .card-details.card-details-collapsed {
|
||||
}
|
||||
.card-details .card-details-header .card-details-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.33em;
|
||||
font-size: 1.35em;
|
||||
margin: 7px 0 0;
|
||||
padding: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.card-details .card-details-header .linked-card-location {
|
||||
font-style: italic;
|
||||
@@ -323,10 +347,10 @@ body.desktop-mode .card-details.card-details-collapsed {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.card-details .card-details-header form.inlined-form .copied-tooltip {
|
||||
padding: 0px 10px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.card-details .card-details-header .card-details-list {
|
||||
font-size: 0.85em;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.card-details .card-details-header .card-details-list a.card-details-list-title {
|
||||
@@ -336,7 +360,7 @@ body.desktop-mode .card-details.card-details-collapsed {
|
||||
display: inline-block;
|
||||
background: #e6e6e6;
|
||||
border-radius: 3px;
|
||||
padding: 0px 5px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
.card-details .card-details-header .copied-tooltip {
|
||||
margin-right: 10px;
|
||||
@@ -347,11 +371,13 @@ body.desktop-mode .card-details.card-details-collapsed {
|
||||
}
|
||||
.card-details .card-description textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
.card-details .card-details-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 15px 0;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.card-details .card-details-items .card-details-item {
|
||||
margin-right: 0.5em;
|
||||
@@ -402,7 +428,7 @@ body.desktop-mode .card-details.card-details-collapsed {
|
||||
position: fixed;
|
||||
resize: both;
|
||||
}
|
||||
|
||||
|
||||
/* Override for mobile mode even on larger screens */
|
||||
body.mobile-mode .card-details {
|
||||
width: 100vw !important;
|
||||
@@ -414,16 +440,16 @@ body.desktop-mode .card-details.card-details-collapsed {
|
||||
max-height: 100vh !important;
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
|
||||
.card-details-maximized {
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: calc(100% - 20px);
|
||||
will-change: flex-basis;
|
||||
overflow-y: scroll;
|
||||
overflow-x: scroll;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
background: #f7f7f7;
|
||||
border-radius: bottom 3px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
z-index: 100;
|
||||
animation: flexGrowIn 0.1s;
|
||||
box-shadow: 0 0 7px 0 #b3b3b3;
|
||||
@@ -467,12 +493,11 @@ input[type="submit"].attachment-add-link-submit {
|
||||
@media screen and (max-width: 800px) {
|
||||
.card-details {
|
||||
width: 100% !important;
|
||||
padding: 0px 0px 0px 0px !important;
|
||||
margin: 0px !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
transition: none;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
/* iOS Safari specific fixes */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
@@ -485,7 +510,7 @@ input[type="submit"].attachment-add-link-submit {
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* Ensure card details are above everything on mobile */
|
||||
body.mobile-mode .card-details {
|
||||
z-index: 100 !important;
|
||||
@@ -702,13 +727,15 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
|
||||
.vote-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.vote-title .js-edit-date {
|
||||
align-self: baseline;
|
||||
margin-left: 5px;
|
||||
align-self: flex-start;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.vote-result {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.js-show-positive-votes {
|
||||
cursor: pointer;
|
||||
@@ -719,29 +746,33 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
|
||||
.poker-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.poker-title .js-edit-date {
|
||||
align-self: baseline;
|
||||
margin-left: 5px;
|
||||
align-self: flex-start;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.poker-result {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
}
|
||||
.js-show-positive-poker-votes {
|
||||
cursor: pointer;
|
||||
}
|
||||
.poker-deck {
|
||||
display: grid;
|
||||
flex-direction: column;
|
||||
grid-auto-flow: row;
|
||||
text-align: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.poker-card-result {
|
||||
width: 32px;
|
||||
width: 34px;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
padding: 4px 2px 4px 2px;
|
||||
padding: 4px 2px;
|
||||
cursor: default;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.winner {
|
||||
font-weight: bold;
|
||||
@@ -752,6 +783,7 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
|
||||
}
|
||||
.responsive-table {
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
}
|
||||
.poker-table {
|
||||
display: table;
|
||||
@@ -814,11 +846,15 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
|
||||
margin: auto;
|
||||
margin-right: 10px;
|
||||
width: 100px;
|
||||
border-radius: 2px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
.estimation-add button {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin: auto;
|
||||
border-radius: 2px;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
.poker-card {
|
||||
width: 48px;
|
||||
@@ -837,6 +873,7 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
|
||||
text-align: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.12s;
|
||||
}
|
||||
.poker-card .inner {
|
||||
display: table-cell;
|
||||
|
||||
@@ -11,58 +11,55 @@ template(name="cardDetails")
|
||||
+editCardTitleForm
|
||||
else
|
||||
unless isMiniScreen
|
||||
unless isPopup
|
||||
span.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
|
||||
if cardCollapsed
|
||||
| ▶
|
||||
else
|
||||
| 🔽
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
| ❌
|
||||
if cardMaximized
|
||||
a.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.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
| 🔼
|
||||
a.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
| ☰
|
||||
a.card-copy-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.emoji-icon 🔗
|
||||
if canModifyCard
|
||||
span.card-drag-handle.js-card-drag-handle(title="Drag card")
|
||||
| ↕️
|
||||
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
|
||||
a.card-zoom-out.js-card-zoom-out(title="{{_ 'zoom-out'}}")
|
||||
| 🔍➖
|
||||
i.fa.fa-search-minus
|
||||
a.card-zoom-in.js-card-zoom-in(title="{{_ 'zoom-in'}}")
|
||||
| 🔍➕
|
||||
i.fa.fa-search-plus
|
||||
a.card-mobile-desktop-toggle.js-card-mobile-desktop-toggle(title="{{_ 'mobile-desktop-toggle'}}")
|
||||
if mobileMode
|
||||
| 🖥️
|
||||
i.fa.fa-desktop
|
||||
else
|
||||
| 📱
|
||||
i.fa.fa-mobile
|
||||
if cardMaximized
|
||||
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
| 🔽
|
||||
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
else
|
||||
a.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
| 🔼
|
||||
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
| ☰
|
||||
i.fa.fa-bars
|
||||
a.card-copy-mobile-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.emoji-icon 🔗
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
h2.card-details-title.js-card-title(
|
||||
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{else}}js-card-title-drag-handle{{/if}}")
|
||||
@@ -73,7 +70,7 @@ template(name="cardDetails")
|
||||
= getTitle
|
||||
if isWatching
|
||||
i.card-details-watch
|
||||
| 👁️
|
||||
i.fa.fa-eye
|
||||
.card-details-path
|
||||
each parentList
|
||||
| >
|
||||
@@ -95,7 +92,7 @@ template(name="cardDetails")
|
||||
if hasActiveUploads
|
||||
.card-details-upload-progress
|
||||
.upload-progress-header
|
||||
| 📤
|
||||
i.fa.fa-upload
|
||||
span {{_ 'uploading-files'}} ({{uploadCount}})
|
||||
each uploads
|
||||
.upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
|
||||
@@ -104,11 +101,11 @@ template(name="cardDetails")
|
||||
.upload-progress-fill(style="width: {{progress}}%")
|
||||
if $eq status 'error'
|
||||
.upload-progress-error
|
||||
| ⚠️
|
||||
i.fa.fa-exclamation-triangle
|
||||
span {{_ 'upload-failed'}}
|
||||
else if $eq status 'completed'
|
||||
.upload-progress-success
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
span {{_ 'upload-completed'}}
|
||||
|
||||
.card-details-left
|
||||
@@ -117,7 +114,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsLabels
|
||||
.card-details-item.card-details-item-labels
|
||||
h3.card-details-item-title
|
||||
| 🏷️
|
||||
i.fa.fa-tags
|
||||
| {{_ 'labels'}}
|
||||
a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
|
||||
each labels
|
||||
@@ -127,14 +124,14 @@ template(name="cardDetails")
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
|
||||
if currentBoard.hasAnyAllowsDate
|
||||
hr
|
||||
|
||||
.card-details-item.card-details-item-date-format
|
||||
h3.card-details-item-title
|
||||
| 📅
|
||||
i.fa.fa-calendar
|
||||
| {{_ 'date-format'}}
|
||||
.card-details-item-content
|
||||
select.js-date-format-selector
|
||||
@@ -145,7 +142,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsReceivedDate
|
||||
.card-details-item.card-details-item-received
|
||||
h3.card-details-item-title
|
||||
| 📥
|
||||
i.fa.fa-sign-out
|
||||
| {{_ 'card-received'}}
|
||||
if getReceived
|
||||
+cardReceivedDate
|
||||
@@ -153,12 +150,12 @@ template(name="cardDetails")
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-received-date
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
|
||||
if currentBoard.allowsStartDate
|
||||
.card-details-item.card-details-item-start
|
||||
h3.card-details-item-title
|
||||
| 🚀
|
||||
i.fa.fa-hourglass-start
|
||||
| {{_ 'card-start'}}
|
||||
if getStart
|
||||
+cardStartDate
|
||||
@@ -166,12 +163,12 @@ template(name="cardDetails")
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-start-date
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
|
||||
if currentBoard.allowsDueDate
|
||||
.card-details-item.card-details-item-due
|
||||
h3.card-details-item-title
|
||||
| ⏰
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'card-due'}}
|
||||
if getDue
|
||||
+cardDueDate
|
||||
@@ -179,12 +176,12 @@ template(name="cardDetails")
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-due-date
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
|
||||
if currentBoard.allowsEndDate
|
||||
.card-details-item.card-details-item-end
|
||||
h3.card-details-item-title
|
||||
| 🏁
|
||||
i.fa.fa-hourglass-end
|
||||
| {{_ 'card-end'}}
|
||||
if getEnd
|
||||
+cardEndDate
|
||||
@@ -192,7 +189,7 @@ template(name="cardDetails")
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-end-date
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
|
||||
if currentBoard.hasAnyAllowsUser
|
||||
hr
|
||||
@@ -200,7 +197,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsCreator
|
||||
.card-details-item.card-details-item-creator
|
||||
h3.card-details-item-title
|
||||
| 👤
|
||||
i.fa.fa-user
|
||||
| {{_ 'creator'}}
|
||||
|
||||
+userAvatar(userId=userId noRemove=true)
|
||||
@@ -210,7 +207,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsMembers
|
||||
.card-details-item.card-details-item-members
|
||||
h3.card-details-item-title
|
||||
| 👥
|
||||
i.fa.fa-users
|
||||
| {{_ 'members'}}
|
||||
each userId in getMembers
|
||||
+userAvatar(userId=userId cardId=_id)
|
||||
@@ -218,30 +215,30 @@ template(name="cardDetails")
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
|
||||
//if assigneeSelected
|
||||
if currentBoard.allowsAssignee
|
||||
.card-details-item.card-details-item-assignees
|
||||
h3.card-details-item-title
|
||||
| 👤
|
||||
i.fa.fa-user
|
||||
| {{_ 'assignee'}}
|
||||
each userId in getAssignees
|
||||
+userAvatar(userId=userId cardId=_id assignee=true)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
if canModifyCard
|
||||
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
if currentUser.isWorker
|
||||
unless assigneeSelected
|
||||
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
|
||||
//.card-details-items
|
||||
if currentBoard.allowsRequestedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3.card-details-item-title
|
||||
| 🛒
|
||||
i.fa.fa-shopping-cart
|
||||
| {{_ 'requested-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
@@ -261,7 +258,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsAssignedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3.card-details-item-title
|
||||
| ✍️
|
||||
i.fa.fa-user-plus
|
||||
| {{_ 'assigned-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
@@ -284,7 +281,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsCardSortingByNumber
|
||||
.card-details-item.card-details-sort-order
|
||||
h3.card-details-item-title
|
||||
| 🔢
|
||||
i.fa.fa-sort-numeric-asc
|
||||
| {{_ 'sort'}}
|
||||
if canModifyCard
|
||||
+inlinedForm(classNames="js-card-details-sort")
|
||||
@@ -297,7 +294,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsShowLists
|
||||
.card-details-item.card-details-show-lists
|
||||
h3.card-details-item-title
|
||||
| 📋
|
||||
i.fa.fa-list
|
||||
| {{_ 'list'}}
|
||||
select.js-select-card-details-lists(disabled="{{#unless canModifyCard}}disabled{{/unless}}")
|
||||
each currentBoard.lists
|
||||
@@ -323,7 +320,7 @@ template(name="cardDetails")
|
||||
hr
|
||||
.card-details-item.card-details-item-customfield
|
||||
h3.card-details-item-title
|
||||
| 📋
|
||||
i.fa.fa-list
|
||||
= definition.name
|
||||
+cardCustomField
|
||||
|
||||
@@ -341,7 +338,7 @@ template(name="cardDetails")
|
||||
.vote-title
|
||||
div.flex
|
||||
h3
|
||||
| 👍
|
||||
i.fa.fa-thumbs-up
|
||||
| {{_ 'vote-question'}}
|
||||
if getVoteEnd
|
||||
+voteEndDate
|
||||
@@ -353,13 +350,14 @@ template(name="cardDetails")
|
||||
.card-label.card-label-green {{ voteCountPositive }}
|
||||
.card-label.card-label-red {{ voteCountNegative }}
|
||||
unless ($and currentBoard.isPublic voteAllowNonBoardMembers )
|
||||
.card-label.card-label-gray {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
.card-label.card-label-gray
|
||||
| {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
+viewer
|
||||
= getVoteQuestion
|
||||
if showVotingButtons
|
||||
button.card-details-green.js-vote.js-vote-positive(class="{{#if voteState}}voted{{/if}}")
|
||||
if voteState
|
||||
| 👍
|
||||
i.fa.fa-thumbs-up
|
||||
| {{_ 'vote-for-it'}}
|
||||
button.card-details-red.js-vote.js-vote-negative(class="{{#if $eq voteState false}}voted{{/if}}")
|
||||
if $eq voteState false
|
||||
@@ -371,7 +369,7 @@ template(name="cardDetails")
|
||||
.poker-title
|
||||
div.flex
|
||||
h3
|
||||
| 👍
|
||||
i.fa.fa-thumbs-up
|
||||
| {{_ 'poker-question'}}
|
||||
if getPokerEnd
|
||||
+pokerEndDate
|
||||
@@ -379,59 +377,60 @@ template(name="cardDetails")
|
||||
.poker-result
|
||||
if expiredPoker
|
||||
unless ($and currentBoard.isPublic pokerAllowNonBoardMembers )
|
||||
.card-label.card-label-gray {{ pokerCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
.card-label.card-label-gray
|
||||
| {{ pokerCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
if showPlanningPokerButtons
|
||||
.poker-result
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-one(class="{{#if $eq pokerState 'one'}}poker-voted{{/if}}") {{_ 'poker-one'}}
|
||||
if $eq pokerState "one"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-two(class="{{#if $eq pokerState 'two'}}poker-voted{{/if}}") {{_ 'poker-two'}}
|
||||
if $eq pokerState "two"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-three(class="{{#if $eq pokerState 'three'}}poker-voted{{/if}}") {{_ 'poker-three'}}
|
||||
if $eq pokerState "three"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-five(class="{{#if $eq pokerState 'five'}}poker-voted{{/if}}") {{_ 'poker-five'}}
|
||||
if $eq pokerState "five"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-eight(class="{{#if $eq pokerState 'eight'}}poker-voted{{/if}}") {{_ 'poker-eight'}}
|
||||
if $eq pokerState "eight"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-thirteen(class="{{#if $eq pokerState 'thirteen'}}poker-voted{{/if}}") {{_ 'poker-thirteen'}}
|
||||
if $eq pokerState "thirteen"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-twenty(class="{{#if $eq pokerState 'twenty'}}poker-voted{{/if}}") {{_ 'poker-twenty'}}
|
||||
if $eq pokerState "twenty"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-forty(class="{{#if $eq pokerState 'forty'}}poker-voted{{/if}}") {{_ 'poker-forty'}}
|
||||
if $eq pokerState "forty"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-one-hundred(class="{{#if $eq pokerState 'oneHundred'}}poker-voted{{/if}}") {{_ 'poker-oneHundred'}}
|
||||
if $eq pokerState "oneHundred"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-unsure(class="{{#if $eq pokerState 'unsure'}}poker-voted{{/if}}") {{_ 'poker-unsure'}}
|
||||
if $eq pokerState "unsure"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
|
||||
if currentUser.isBoardAdmin
|
||||
button.card-details-blue.js-poker-finish(class="{{#if $eq voteState false}}poker-voted{{/if}}") {{_ 'poker-finish'}}
|
||||
@@ -561,7 +560,7 @@ template(name="cardDetails")
|
||||
button.card-details-red.js-poker-replay(class="{{#if $eq voteState false}}voted{{/if}}") {{_ 'poker-replay'}}
|
||||
div.estimation-add
|
||||
button.js-poker-estimation
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
| {{_ 'set-estimation'}}
|
||||
input(type=text,autofocus value=getPokerEstimation,id="pokerEstimation")
|
||||
|
||||
@@ -571,7 +570,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsDescriptionTitle
|
||||
hr
|
||||
h3.card-details-item-title
|
||||
| 📝
|
||||
i.fa.fa-file-text-o
|
||||
| {{_ 'description'}}
|
||||
if currentBoard.allowsDescriptionText
|
||||
+inlinedCardDescription(classNames="card-description js-card-description")
|
||||
@@ -582,7 +581,7 @@ template(name="cardDetails")
|
||||
else
|
||||
if currentBoard.allowsDescriptionText
|
||||
a.js-open-inlined-form(title="{{_ 'edit'}}" value=title)
|
||||
| ✏️
|
||||
i.fa.fa-pencil-square-o
|
||||
a.js-open-inlined-form(title="{{_ 'edit'}}" value=title)
|
||||
if getDescription
|
||||
+viewer
|
||||
@@ -612,7 +611,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsAttachments
|
||||
hr
|
||||
h3.card-details-item-title
|
||||
| 📎
|
||||
i.fa.fa-paperclip
|
||||
| {{_ 'attachments'}}
|
||||
if Meteor.settings.public.attachmentsUploadMaxSize
|
||||
| {{_ 'max-upload-filesize'}} {{Meteor.settings.public.attachmentsUploadMaxSize}}
|
||||
@@ -628,7 +627,7 @@ template(name="cardDetails")
|
||||
unless currentUser.isNoComments
|
||||
.comment-title
|
||||
h3.card-details-item-title
|
||||
| 💬
|
||||
i.fa.fa-comment-o
|
||||
| {{_ 'comments'}}
|
||||
|
||||
if currentBoard.allowsComments
|
||||
@@ -645,7 +644,7 @@ template(name="cardDetails")
|
||||
if currentUser.isBoardAdmin
|
||||
.activity-title
|
||||
h3.card-details-item-title
|
||||
| 📜
|
||||
i.fa.fa-history
|
||||
| {{ _ 'activities'}}
|
||||
if currentUser.isBoardMember
|
||||
.material-toggle-switch(title="{{_ 'show-activities'}}")
|
||||
@@ -696,10 +695,10 @@ template(name="cardDetailsActionsPopup")
|
||||
li
|
||||
a.js-toggle-watch-card
|
||||
if isWatching
|
||||
| 👁️
|
||||
i.fa.fa-eye
|
||||
| {{_ 'unwatch'}}
|
||||
else
|
||||
| 👁️
|
||||
i.fa.fa-eye
|
||||
| {{_ 'watch'}}
|
||||
hr
|
||||
if canModifyCard
|
||||
@@ -710,16 +709,16 @@ template(name="cardDetailsActionsPopup")
|
||||
//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'}}
|
||||
@@ -727,19 +726,19 @@ template(name="cardDetailsActionsPopup")
|
||||
//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
|
||||
@@ -750,7 +749,7 @@ template(name="cardDetailsActionsPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-export-card
|
||||
| 📤
|
||||
i.fa.fa-upload
|
||||
| {{_ 'export-card'}}
|
||||
unless canModifyCard
|
||||
unless currentUser.isReadOnly
|
||||
@@ -759,119 +758,111 @@ template(name="cardDetailsActionsPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-move-card-to-top
|
||||
| ⬆️
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
| ⬇️
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-move-card
|
||||
| ➡️
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
li
|
||||
a.js-copy-card
|
||||
| 📋
|
||||
i.fa.fa-clipboard
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-copy-checklist-cards
|
||||
| 📋
|
||||
| 📋
|
||||
i.fa.fa-copy
|
||||
| {{_ 'copyManyCardsPopup-title'}}
|
||||
unless archived
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-archive
|
||||
| ➡️
|
||||
| 📦
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archive-card'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
span.emoji-icon 🔗
|
||||
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'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
span.emoji-icon 🔗
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
| {{_ 'cardMorePopup-title'}}
|
||||
|
||||
template(name="exportCardPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a(href="{{exportUrlCardPDF}}",, download="{{exportFilenameCardPDF}}")
|
||||
| 📤
|
||||
a(href="{{exportUrlCardPDF}}", download="{{exportFilenameCardPDF}}")
|
||||
i.fa.fa-file-pdf-o
|
||||
| {{_ 'export-card-pdf'}}
|
||||
hr
|
||||
p.quiet {{_ 'export-card-excel-fields'}}
|
||||
ul.pop-over-list
|
||||
each excelExportFields
|
||||
li(class="{{#if checked}}active{{/if}}")
|
||||
a.js-excel-field-toggle(data-field="{{field}}" href="#")
|
||||
i.fa.fa-check
|
||||
| {{_ label}}
|
||||
ul.pop-over-list
|
||||
li
|
||||
a(href="{{exportUrlCardExcel}}", download="{{exportFilenameCardExcel}}")
|
||||
i.fa.fa-file-excel-o
|
||||
| {{_ 'download'}}
|
||||
|
||||
template(name="moveCardPopup")
|
||||
+copyAndMoveCard
|
||||
|
||||
template(name="copyCardPopup")
|
||||
label(for='copy-card-title') {{_ 'title'}}:
|
||||
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
|
||||
= getTitle
|
||||
+copyAndMoveCard
|
||||
|
||||
template(name="copyManyCardsPopup")
|
||||
label(for='copy-checklist-cards-title') {{_ 'copyManyCardsPopup-instructions'}}:
|
||||
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
|
||||
| {{_ 'copyManyCardsPopup-format'}}
|
||||
+copyAndMoveCard
|
||||
|
||||
template(name="convertChecklistItemToCardPopup")
|
||||
label(for='convert-checklist-item-to-card-title') {{_ 'title'}}:
|
||||
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
|
||||
= item.title
|
||||
+copyAndMoveCard
|
||||
|
||||
template(name="copyAndMoveCard")
|
||||
label(for='move-card-title') {{_ 'title'}}:
|
||||
input#move-card-title.full-line(type="text" value="{{getTitle}}" autofocus)
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
@@ -881,7 +872,112 @@ template(name="copyAndMoveCard")
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
||||
template(name="copyCardPopup")
|
||||
label(for='copy-card-title') {{_ 'title'}}:
|
||||
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
|
||||
= getTitle
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
||||
template(name="copyManyCardsPopup")
|
||||
label(for='copy-checklist-cards-title') {{_ 'copyManyCardsPopup-instructions'}}:
|
||||
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
|
||||
| {{_ 'copyManyCardsPopup-format'}}
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
||||
template(name="convertChecklistItemToCardPopup")
|
||||
label(for='convert-checklist-item-to-card-title') {{_ 'title'}}:
|
||||
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
|
||||
= item.title
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
@@ -915,8 +1011,7 @@ template(name="cardMembersPopup")
|
||||
if userData.username
|
||||
| (#{userData.username})
|
||||
if isCardMember
|
||||
| ✅
|
||||
|
||||
i.fa.fa-check
|
||||
template(name="cardAssigneesPopup")
|
||||
input.card-assignees-filter(type="text" placeholder="{{_ 'search'}}")
|
||||
unless currentUser.isWorker
|
||||
@@ -930,7 +1025,7 @@ template(name="cardAssigneesPopup")
|
||||
if userData.username
|
||||
| (#{userData.username})
|
||||
if isCardAssignee
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
if currentUser.isWorker
|
||||
ul.pop-over-list.js-card-assignee-list
|
||||
li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
|
||||
@@ -941,8 +1036,7 @@ template(name="cardAssigneesPopup")
|
||||
if currentUser.username
|
||||
| (#{currentUser.username})
|
||||
if currentUser.isCardAssignee
|
||||
| ✅
|
||||
|
||||
i.fa.fa-check
|
||||
template(name="cardAssigneePopup")
|
||||
.board-assignee-menu
|
||||
.mini-profile-info
|
||||
@@ -965,7 +1059,7 @@ template(name="cardMorePopup")
|
||||
span.clearfix
|
||||
span {{_ 'link-card'}}
|
||||
= ' '
|
||||
| {{#if board.isPublic}}🌐{{else}}🔒{{/if}}
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
input.inline-input(type="text" id="cardURL" readonly value="{{ originRelativeUrl }}" autofocus="autofocus")
|
||||
button.js-copy-card-link-to-clipboard(class="btn" id="clipboard") {{_ 'copy-card-link-to-clipboard'}}
|
||||
.copied-tooltip {{_ 'copied'}}
|
||||
@@ -997,19 +1091,20 @@ template(name="cardMorePopup")
|
||||
option(value="{{_id}}") {{title}}
|
||||
br
|
||||
| {{_ 'added'}}
|
||||
span.date(title=card.createdAt) {{ moment createdAt 'LLL' }}
|
||||
span.date(title=card.createdAt) {{ displayDate createdAt 'LLL' }}
|
||||
if currentUser.isBoardAdmin
|
||||
a.js-delete(title="{{_ 'card-delete-notice'}}") {{_ 'delete'}}
|
||||
|
||||
template(name="setCardColorPopup")
|
||||
form.edit-label
|
||||
.palette-colors: each colors
|
||||
unless $eq color 'white'
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
| ✅
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
.palette-colors
|
||||
each colors
|
||||
unless $eq color 'white'
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
|
||||
template(name="cardDeletePopup")
|
||||
p {{_ "card-delete-pop"}}
|
||||
@@ -1041,12 +1136,12 @@ template(name="cardStartVotingPopup")
|
||||
.materialCheckBox#vote-public(name="vote-public" class="{{#if votePublic}}is-checked{{/if}}")
|
||||
span {{_ 'vote-public'}}
|
||||
.check-div.flex
|
||||
| ⏰
|
||||
i.fa.fa-clock-o
|
||||
a.js-end-date
|
||||
span
|
||||
| {{_ 'card-end'}}
|
||||
unless getVoteEnd
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
if getVoteEnd
|
||||
+voteEndDate
|
||||
|
||||
@@ -1087,12 +1182,12 @@ template(name="cardStartPlanningPokerPopup")
|
||||
.materialCheckBox#poker-allow-non-members(name="poker-allow-non-members" class="{{#if pokerAllowNonBoardMembers}}is-checked{{/if}}")
|
||||
span {{_ 'allowNonBoardMembers'}}
|
||||
.check-div.flex
|
||||
| ⏰
|
||||
i.fa.fa-clock-o
|
||||
a.js-end-date
|
||||
span
|
||||
| {{_ 'card-end'}}
|
||||
unless getPokerEnd
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
if getPokerEnd
|
||||
+pokerEndDate
|
||||
|
||||
|
||||
+1436
-1463
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,4 @@
|
||||
template(name="editCardSpentTime")
|
||||
template(name="editCardSpentTimePopup")
|
||||
.edit-card-time
|
||||
form.edit-time
|
||||
.fields
|
||||
@@ -13,7 +13,7 @@ template(name="editCardSpentTime")
|
||||
button.primary.wide.left.js-submit-time(type="submit") {{_ 'save'}}
|
||||
button.js-delete-time.negate.wide.right {{_ 'delete'}}
|
||||
|
||||
template(name="timeBadge")
|
||||
template(name="cardSpentTime")
|
||||
if canModifyCard
|
||||
a.js-edit-time.card-time(title="{{_ 'time'}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
|
||||
| ⏱️ {{showTime}}
|
||||
|
||||
@@ -1,85 +1,91 @@
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import Cards from '/models/cards';
|
||||
import { getCurrentCardIdFromContext } from '/client/lib/currentCard';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'editCardSpentTime';
|
||||
function getCardId() {
|
||||
return getCurrentCardIdFromContext();
|
||||
}
|
||||
|
||||
Template.editCardSpentTimePopup.onCreated(function () {
|
||||
this.error = new ReactiveVar('');
|
||||
this.card = Cards.findOne(getCardId());
|
||||
});
|
||||
|
||||
Template.editCardSpentTimePopup.helpers({
|
||||
error() {
|
||||
return Template.instance().error;
|
||||
},
|
||||
onCreated() {
|
||||
this.error = new ReactiveVar('');
|
||||
this.card = this.data();
|
||||
card() {
|
||||
return Cards.findOne(getCardId());
|
||||
},
|
||||
toggleOvertime() {
|
||||
this.card.setIsOvertime(!this.card.getIsOvertime());
|
||||
getIsOvertime() {
|
||||
const card = Cards.findOne(getCardId());
|
||||
return card?.getIsOvertime ? card.getIsOvertime() : false;
|
||||
},
|
||||
});
|
||||
|
||||
Template.editCardSpentTimePopup.events({
|
||||
//TODO : need checking this portion
|
||||
'submit .edit-time'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const card = Cards.findOne(getCardId());
|
||||
if (!card) return;
|
||||
|
||||
const spentTime = parseFloat(evt.target.time.value);
|
||||
let isOvertime = false;
|
||||
if ($('#overtime').attr('class').indexOf('is-checked') >= 0) {
|
||||
isOvertime = true;
|
||||
}
|
||||
if (spentTime >= 0) {
|
||||
card.setSpentTime(spentTime);
|
||||
card.setIsOvertime(isOvertime);
|
||||
Popup.back();
|
||||
} else {
|
||||
tpl.error.set('invalid-time');
|
||||
evt.target.time.focus();
|
||||
}
|
||||
},
|
||||
'click .js-delete-time'(evt) {
|
||||
evt.preventDefault();
|
||||
const card = Cards.findOne(getCardId());
|
||||
if (!card) return;
|
||||
card.setSpentTime(null);
|
||||
card.setIsOvertime(false);
|
||||
Popup.back();
|
||||
},
|
||||
'click a.js-toggle-overtime'(evt) {
|
||||
const card = Cards.findOne(getCardId());
|
||||
if (!card) return;
|
||||
card.setIsOvertime(!card.getIsOvertime());
|
||||
$('#overtime .materialCheckBox').toggleClass('is-checked');
|
||||
$('#overtime').toggleClass('is-checked');
|
||||
},
|
||||
storeTime(spentTime, isOvertime) {
|
||||
this.card.setSpentTime(spentTime);
|
||||
this.card.setIsOvertime(isOvertime);
|
||||
},
|
||||
deleteTime() {
|
||||
this.card.setSpentTime(null);
|
||||
this.card.setIsOvertime(false);
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
//TODO : need checking this portion
|
||||
'submit .edit-time'(evt) {
|
||||
evt.preventDefault();
|
||||
});
|
||||
|
||||
const spentTime = parseFloat(evt.target.time.value);
|
||||
//const isOvertime = this.card.getIsOvertime();
|
||||
let isOvertime = false;
|
||||
if ($('#overtime').attr('class').indexOf('is-checked') >= 0) {
|
||||
isOvertime = true;
|
||||
}
|
||||
if (spentTime >= 0) {
|
||||
this.storeTime(spentTime, isOvertime);
|
||||
Popup.back();
|
||||
} else {
|
||||
this.error.set('invalid-time');
|
||||
evt.target.time.focus();
|
||||
}
|
||||
},
|
||||
'click .js-delete-time'(evt) {
|
||||
evt.preventDefault();
|
||||
this.deleteTime();
|
||||
Popup.back();
|
||||
},
|
||||
'click a.js-toggle-overtime': this.toggleOvertime,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('editCardSpentTimePopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'timeBadge';
|
||||
},
|
||||
onCreated() {
|
||||
const self = this;
|
||||
self.time = ReactiveVar();
|
||||
},
|
||||
Template.cardSpentTime.helpers({
|
||||
showTitle() {
|
||||
if (this.data().getIsOvertime()) {
|
||||
const card = Cards.findOne(this._id) || this;
|
||||
if (card.getIsOvertime && card.getIsOvertime()) {
|
||||
return `${TAPi18n.__(
|
||||
'overtime',
|
||||
)} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
|
||||
} else {
|
||||
)} ${card.getSpentTime()} ${TAPi18n.__('hours')}`;
|
||||
} else if (card.getSpentTime) {
|
||||
return `${TAPi18n.__(
|
||||
'card-spent',
|
||||
)} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
|
||||
)} ${card.getSpentTime()} ${TAPi18n.__('hours')}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
showTime() {
|
||||
return this.data().getSpentTime();
|
||||
const card = Cards.findOne(this._id) || this;
|
||||
return card.getSpentTime ? card.getSpentTime() : '';
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-edit-time': Popup.open('editCardSpentTime'),
|
||||
},
|
||||
];
|
||||
getIsOvertime() {
|
||||
const card = Cards.findOne(this._id) || this;
|
||||
return card.getIsOvertime ? card.getIsOvertime() : false;
|
||||
},
|
||||
}).register('cardSpentTime');
|
||||
});
|
||||
|
||||
Template.cardSpentTime.events({
|
||||
'click .js-edit-time': Popup.open('editCardSpentTime'),
|
||||
});
|
||||
@@ -37,23 +37,16 @@ textarea.js-edit-checklist-item {
|
||||
.checklist-progress-bar-container .checklist-progress-bar {
|
||||
width: 80%;
|
||||
height: 10px;
|
||||
background-color: #d6ebff !important;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
|
||||
color: #fff !important;
|
||||
background-color: #3cb500 !important;
|
||||
color: #fff;
|
||||
background-color: #666;
|
||||
padding: 0.01em 16px;
|
||||
border-radius: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
/* Grey progress bar when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-bar {
|
||||
background-color: #d9d9d9;
|
||||
}
|
||||
body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
|
||||
background-color: #7a7a7a !important;
|
||||
}
|
||||
.checklist-title {
|
||||
padding: 10px;
|
||||
}
|
||||
@@ -76,14 +69,10 @@ body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-ba
|
||||
.checklist-title .checklist-stat.is-finished {
|
||||
color: #3cb500;
|
||||
}
|
||||
.checklist-title span.checklist-handle {
|
||||
.checklist-title span.fa.checklist-handle {
|
||||
padding-right: 20px;
|
||||
padding-top: 3px;
|
||||
float: left;
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
#card-details-overlay {
|
||||
top: 0;
|
||||
@@ -114,25 +103,6 @@ body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-ba
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* iPhone mobile: larger checklist titles and more spacing between items */
|
||||
body.mobile-mode.iphone-device .checklist-title .title {
|
||||
font-size: 1.3em !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
body.mobile-mode.iphone-device .checklist-item {
|
||||
margin-top: 12px !important;
|
||||
margin-bottom: 8px !important;
|
||||
padding: 8px 4px !important;
|
||||
min-height: 44px; /* iOS recommended touch target size */
|
||||
}
|
||||
|
||||
body.mobile-mode.iphone-device .checklist-item span.checklistitem-handle {
|
||||
font-size: 1.5em !important;
|
||||
padding-right: 15px !important;
|
||||
width: 1.5em !important;
|
||||
}
|
||||
.checklist-item.is-checked.invisible {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
@@ -162,27 +132,6 @@ body.mobile-mode.iphone-device .checklist-item span.checklistitem-handle {
|
||||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
}
|
||||
/* Unicode checkbox icons styling */
|
||||
.checklist-item .check-box-unicode,
|
||||
.cardCustomField-checkbox .check-box-unicode {
|
||||
font-size: 1.3em;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .checklist-item .check-box.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
body.grey-icons-enabled .checklist-item .check-box-unicode,
|
||||
body.grey-icons-enabled .cardCustomField-checkbox .check-box-unicode {
|
||||
filter: grayscale(100%);
|
||||
-webkit-filter: grayscale(100%);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.checklist-item .item-title {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -197,14 +146,9 @@ body.grey-icons-enabled .cardCustomField-checkbox .check-box-unicode {
|
||||
word-wrap: break-word;
|
||||
max-width: 420px;
|
||||
}
|
||||
.checklist-item span.checklistitem-handle {
|
||||
.checklist-item span.fa.checklistitem-handle {
|
||||
padding-top: 2px;
|
||||
padding-right: 10px;
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
.js-delete-checklist-item,
|
||||
.js-convert-checklist-item-to-card {
|
||||
|
||||
@@ -1,25 +1,35 @@
|
||||
template(name="checklists")
|
||||
.checklists-title
|
||||
h3.card-details-item-title
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
| {{_ 'checklists'}}
|
||||
if canModifyCard
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId position="top")
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId
|
||||
position="top")
|
||||
+addChecklistItemForm
|
||||
else
|
||||
a.add-checklist-top.js-open-inlined-form(title="{{_ 'add-checklist'}}")
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
if currentUser.isBoardMember
|
||||
.material-toggle-switch(title="{{_ 'hide-finished-checklist'}}")
|
||||
//span.toggle-switch-title
|
||||
if card.hideFinishedChecklistIfItemsAreHidden
|
||||
input.toggle-switch(type="checkbox" id="toggleHideFinishedChecklist" checked="checked")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleHideFinishedChecklist")
|
||||
label.toggle-label(for="toggleHideFinishedChecklist")
|
||||
|
||||
.card-checklist-items
|
||||
each checklist in checklists
|
||||
+checklistDetail(checklist = checklist card = card)
|
||||
if checklist.showChecklist card.hideFinishedChecklistIfItemsAreHidden
|
||||
+checklistDetail(checklist = checklist card = card)
|
||||
|
||||
if canModifyCard
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId)
|
||||
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=false)
|
||||
else
|
||||
a.add-checklist.js-open-inlined-form(title="{{_ 'add-checklist'}}")
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
|
||||
template(name="checklistDetail")
|
||||
.js-checklist.checklist.nodragscroll
|
||||
@@ -29,12 +39,12 @@ template(name="checklistDetail")
|
||||
.checklist-title
|
||||
span
|
||||
if canModifyCard
|
||||
a.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}") ☰
|
||||
a.fa.fa-navicon.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
|
||||
|
||||
if canModifyCard
|
||||
h4.title.js-open-inlined-form.is-editable
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
span.checklist-handle(title="{{_ 'dragChecklist'}}") ↕️
|
||||
span.fa.checklist-handle(class="fa-arrows" title="{{_ 'dragChecklist'}}")
|
||||
+viewer
|
||||
= checklist.title
|
||||
else
|
||||
@@ -54,17 +64,16 @@ template(name="checklistDeletePopup")
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="checklistItemDeletePopup")
|
||||
p {{_ 'confirm-checklist-delete-popup'}}
|
||||
p {{_ 'confirm-checklist-item-delete-popup'}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="addChecklistItemForm")
|
||||
a(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
textarea.js-add-checklist-item(rows='1' autofocus)
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
|
||||
a.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
|
||||
| ❌
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
|
||||
if showNewlineBecomesNewChecklistItem
|
||||
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItem'}}")
|
||||
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItem")
|
||||
@@ -77,7 +86,7 @@ template(name="addChecklistItemForm")
|
||||
| {{_ 'originOrder'}}
|
||||
|
||||
template(name="editChecklistItemForm")
|
||||
a(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
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'
|
||||
@@ -86,17 +95,13 @@ template(name="editChecklistItemForm")
|
||||
= checklist.title
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
|
||||
a.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
|
||||
| ❌
|
||||
span(title=createdAt) {{ moment createdAt }}
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
|
||||
span(title=createdAt) {{ displayDate createdAt }}
|
||||
if canModifyCard
|
||||
if $eq type 'item'
|
||||
a.js-delete-checklist-item {{_ "delete"}}...
|
||||
a.js-convert-checklist-item-to-card
|
||||
| 📋
|
||||
| {{_ 'convertChecklistItemToCardPopup-title'}}
|
||||
else
|
||||
a.js-delete-checklist {{_ "delete"}}...
|
||||
a.js-delete-checklist-item {{_ "delete"}}...
|
||||
a.js-convert-checklist-item-to-card
|
||||
i.fa.fa-copy
|
||||
| {{_ 'convertChecklistItemToCardPopup-title'}}
|
||||
|
||||
template(name="checklistItems")
|
||||
if checklist.items.length
|
||||
@@ -105,7 +110,7 @@ template(name="checklistItems")
|
||||
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true position="top")
|
||||
else
|
||||
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
.checklist-items.js-checklist-items
|
||||
each item in checklist.items
|
||||
+inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
|
||||
@@ -117,20 +122,21 @@ template(name="checklistItems")
|
||||
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true)
|
||||
else
|
||||
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
|
||||
template(name='checklistItemDetail')
|
||||
.js-checklist-item.checklist-item(class="{{#if item.isFinished }}is-checked{{#if checklist.hideCheckedChecklistItems}} invisible{{/if}}{{/if}}{{#if checklist.hideAllChecklistItems}} is-checked invisible{{/if}}"
|
||||
role="checkbox" aria-checked="{{#if item.isFinished }}true{{else}}false{{/if}}" tabindex="0")
|
||||
if canModifyCard
|
||||
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
|
||||
.check-box-container
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
span.checklistitem-handle(title="{{_ 'dragChecklistItem'}}") ↕️
|
||||
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
|
||||
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
else
|
||||
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
|
||||
.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
@@ -139,16 +145,16 @@ template(name="checklistActionsPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-delete-checklist.delete-checklist
|
||||
| 🗑️
|
||||
i.fa.fa-trash
|
||||
| {{_ "delete"}} ...
|
||||
a.js-move-checklist.move-checklist
|
||||
| ➡️
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ "moveChecklist"}} ...
|
||||
a.js-copy-checklist.copy-checklist
|
||||
| 📋
|
||||
i.fa.fa-copy
|
||||
| {{_ "copyChecklist"}} ...
|
||||
a.js-hide-checked-checklist-items
|
||||
| 🙈
|
||||
i.fa.fa-eye-slash
|
||||
| {{_ "hideCheckedChecklistItems"}} ...
|
||||
.material-toggle-switch(title="{{_ 'hide-checked-items'}}")
|
||||
if checklist.hideCheckedChecklistItems
|
||||
@@ -157,7 +163,7 @@ template(name="checklistActionsPopup")
|
||||
input.toggle-switch(type="checkbox" id="toggleHideCheckedChecklistItems_{{checklist._id}}")
|
||||
label.toggle-label(for="toggleHideCheckedChecklistItems_{{checklist._id}}")
|
||||
a.js-hide-all-checklist-items
|
||||
| 🚫
|
||||
i.fa.fa-ban
|
||||
| {{_ "hideAllChecklistItems"}} ...
|
||||
.material-toggle-switch(title="{{_ 'hideAllChecklistItems'}}")
|
||||
if checklist.hideAllChecklistItems
|
||||
@@ -165,62 +171,34 @@ template(name="checklistActionsPopup")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleHideAllChecklistItems_{{checklist._id}}")
|
||||
label.toggle-label(for="toggleHideAllChecklistItems_{{checklist._id}}")
|
||||
a.js-toggle-show-checklist-at-minicard
|
||||
| 📋
|
||||
| {{_ "showChecklistAtMinicard"}} ...
|
||||
.material-toggle-switch(title="{{_ 'showChecklistAtMinicard'}}")
|
||||
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")
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'card'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
+copyAndMoveChecklist
|
||||
|
||||
template(name="moveChecklistPopup")
|
||||
+copyAndMoveChecklist
|
||||
|
||||
template(name="copyAndMoveChecklist")
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
label {{_ 'card'}}:
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
@@ -2,10 +2,15 @@ import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import Cards from '/models/cards';
|
||||
import Boards from '/models/boards';
|
||||
import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard';
|
||||
import ChecklistItems from '/models/checklistItems';
|
||||
import Checklists from '/models/checklists';
|
||||
import { BoardSwimlaneListCardDialog } from '/client/lib/dialogWithBoardSwimlaneListCard';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import autosize from 'autosize';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
const { calculateIndexData, capitalize } = Utils;
|
||||
// SubsManager removed for Meteor 3 migration
|
||||
const { calculateIndexData } = Utils;
|
||||
|
||||
function initSorting(items) {
|
||||
items.sortable({
|
||||
@@ -45,55 +50,66 @@ function initSorting(items) {
|
||||
});
|
||||
}
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
const self = this;
|
||||
self.itemsDom = this.$('.js-checklist-items');
|
||||
initSorting(self.itemsDom);
|
||||
self.itemsDom.mousedown(function (evt) {
|
||||
evt.stopPropagation();
|
||||
});
|
||||
Template.checklistDetail.onRendered(function () {
|
||||
const tpl = this;
|
||||
tpl.itemsDom = this.$('.js-checklist-items');
|
||||
initSorting(tpl.itemsDom);
|
||||
tpl.itemsDom.mousedown(function (evt) {
|
||||
evt.stopPropagation();
|
||||
});
|
||||
|
||||
function userIsMember() {
|
||||
return ReactiveCache.getCurrentUser()?.isBoardMember();
|
||||
}
|
||||
function userIsMember() {
|
||||
return ReactiveCache.getCurrentUser()?.isBoardMember();
|
||||
}
|
||||
|
||||
// Disable sorting if the current user is not a board member
|
||||
self.autorun(() => {
|
||||
const $itemsDom = $(self.itemsDom);
|
||||
if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
|
||||
$(self.itemsDom).sortable('option', 'disabled', !userIsMember());
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$(self.itemsDom).sortable({
|
||||
handle: 'span.checklistitem-handle',
|
||||
});
|
||||
}
|
||||
// Disable sorting if the current user is not a board member
|
||||
tpl.autorun(() => {
|
||||
const $itemsDom = $(tpl.itemsDom);
|
||||
if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
|
||||
$(tpl.itemsDom).sortable('option', 'disabled', !userIsMember());
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$(tpl.itemsDom).sortable({
|
||||
handle: 'span.fa.checklistitem-handle',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.checklistDetail.helpers({
|
||||
/** returns the finished percent of the checklist */
|
||||
finishedPercent() {
|
||||
const ret = this.data().checklist.finishedPercent();
|
||||
const ret = this.checklist.finishedPercent();
|
||||
return ret;
|
||||
},
|
||||
}).register('checklistDetail');
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
addChecklist(event) {
|
||||
Template.checklists.helpers({
|
||||
checklists() {
|
||||
const card = ReactiveCache.getCard(this.cardId);
|
||||
if (!card || typeof card.checklists !== 'function') {
|
||||
return [];
|
||||
}
|
||||
const ret = card.checklists();
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
|
||||
Template.checklists.events({
|
||||
'click .js-open-checklist-details-menu': Popup.open('checklistActions'),
|
||||
'submit .js-add-checklist'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-add-checklist-item');
|
||||
const textarea = tpl.find('textarea.js-add-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
let cardId = this.currentData().cardId;
|
||||
let cardId = Template.currentData().cardId;
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
//if (card.isLinked()) cardId = card.linkedId;
|
||||
if (card.isLinkedCard()) {
|
||||
cardId = card.linkedId;
|
||||
}
|
||||
|
||||
let sortIndex;
|
||||
let checklistItemIndex;
|
||||
if (this.currentData().position === 'top') {
|
||||
if (Template.currentData().position === 'top') {
|
||||
sortIndex = Utils.calculateIndexData(null, card.firstChecklist()).base;
|
||||
checklistItemIndex = 0;
|
||||
} else {
|
||||
@@ -107,35 +123,60 @@ BlazeComponent.extendComponent({
|
||||
title,
|
||||
sort: sortIndex,
|
||||
});
|
||||
this.closeAllInlinedForms();
|
||||
tpl.$('.js-close-inlined-form').click();
|
||||
setTimeout(() => {
|
||||
this.$('.add-checklist-item')
|
||||
tpl.$('.add-checklist-item')
|
||||
.eq(checklistItemIndex)
|
||||
.click();
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
addChecklistItem(event) {
|
||||
'submit .js-edit-checklist-title'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-add-checklist-item');
|
||||
const newlineBecomesNewChecklistItem = this.find('input#toggleNewlineBecomesNewChecklistItem');
|
||||
const newlineBecomesNewChecklistItemOriginOrder = this.find('input#toggleNewlineBecomesNewChecklistItemOriginOrder');
|
||||
const textarea = tpl.find('textarea.js-edit-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
const checklist = this.currentData().checklist;
|
||||
const checklist = Template.currentData().checklist;
|
||||
checklist.setTitle(title);
|
||||
},
|
||||
'submit .js-add-checklist-item'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = tpl.find('textarea.js-add-checklist-item');
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
const newlineBecomesNewChecklistItem = tpl.find('input#toggleNewlineBecomesNewChecklistItem');
|
||||
const newlineBecomesNewChecklistItemOriginOrder = tpl.find('input#toggleNewlineBecomesNewChecklistItemOriginOrder');
|
||||
const title = textarea.value.trim();
|
||||
const currentData = Template.currentData() || {};
|
||||
let resolvedData = currentData;
|
||||
let checklist = currentData.checklist;
|
||||
if (!checklist) {
|
||||
const form = event.currentTarget?.closest
|
||||
? event.currentTarget.closest('form')
|
||||
: $(event.target).closest('form').get(0);
|
||||
const formData = form ? Blaze.getData(form) : null;
|
||||
if (formData) {
|
||||
resolvedData = formData;
|
||||
}
|
||||
checklist = formData?.checklist;
|
||||
}
|
||||
if (!checklist) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (title) {
|
||||
let checklistItems = [title];
|
||||
if (newlineBecomesNewChecklistItem.checked) {
|
||||
if (newlineBecomesNewChecklistItem?.checked) {
|
||||
checklistItems = title.split('\n').map(_value => _value.trim());
|
||||
if (this.currentData().position === 'top') {
|
||||
if (newlineBecomesNewChecklistItemOriginOrder.checked === false) {
|
||||
if (resolvedData.position === 'top') {
|
||||
if (newlineBecomesNewChecklistItemOriginOrder?.checked === false) {
|
||||
checklistItems = checklistItems.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
let addIndex;
|
||||
let sortIndex;
|
||||
if (this.currentData().position === 'top') {
|
||||
if (resolvedData.position === 'top') {
|
||||
sortIndex = Utils.calculateIndexData(null, checklist.firstItem()).base;
|
||||
addIndex = -1;
|
||||
} else {
|
||||
@@ -156,25 +197,33 @@ BlazeComponent.extendComponent({
|
||||
textarea.value = '';
|
||||
textarea.focus();
|
||||
},
|
||||
|
||||
editChecklist(event) {
|
||||
'submit .js-edit-checklist-item'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-edit-checklist-item');
|
||||
const textarea = tpl.find('textarea.js-edit-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
const checklist = this.currentData().checklist;
|
||||
checklist.setTitle(title);
|
||||
const formData = Blaze.getData(event.currentTarget) || Blaze.getData(event.target);
|
||||
const item = formData?.item;
|
||||
if (item) {
|
||||
item.setTitle(title);
|
||||
}
|
||||
},
|
||||
|
||||
editChecklistItem(event) {
|
||||
'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
|
||||
'click .js-delete-checklist-item': Popup.afterConfirm('checklistItemDelete', function () {
|
||||
Popup.back();
|
||||
const item = this?.item || this;
|
||||
if (item && item._id) {
|
||||
ChecklistItems.remove(item._id);
|
||||
}
|
||||
}),
|
||||
// add and delete checklist / checklist-item
|
||||
'click .js-open-inlined-form'(event, tpl) {
|
||||
tpl.$('.js-close-inlined-form').click();
|
||||
},
|
||||
'click #toggleHideFinishedChecklist'(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const textarea = this.find('textarea.js-edit-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
const item = this.currentData().item;
|
||||
item.setTitle(title);
|
||||
Template.currentData().card.toggleHideFinishedChecklist();
|
||||
},
|
||||
|
||||
pressKey(event) {
|
||||
'keydown textarea.js-add-checklist-item'(event) {
|
||||
//If user press enter key inside a form, submit it
|
||||
//Unless the user is also holding down the 'shift' key
|
||||
if (event.keyCode === 13 && !event.shiftKey) {
|
||||
@@ -183,230 +232,201 @@ BlazeComponent.extendComponent({
|
||||
$form.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
focusChecklistItem(event) {
|
||||
// If a new checklist is created, pre-fill the title and select it.
|
||||
const checklist = this.currentData().checklist;
|
||||
if (!checklist) {
|
||||
const textarea = event.target;
|
||||
textarea.value = capitalize(TAPi18n.__('r-checklist'));
|
||||
textarea.select();
|
||||
}
|
||||
},
|
||||
// NOTE: boardsSwimlanesAndLists template was removed from jade but JS was left behind.
|
||||
// This is dead code — the template no longer exists in any jade file.
|
||||
|
||||
/** closes all inlined forms (checklist and checklist-item input fields) */
|
||||
closeAllInlinedForms() {
|
||||
this.$('.js-close-inlined-form').click();
|
||||
},
|
||||
Template.addChecklistItemForm.onRendered(function () {
|
||||
autosize(this.$('textarea.js-add-checklist-item'));
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-open-checklist-details-menu': Popup.open('checklistActions'),
|
||||
'submit .js-add-checklist': this.addChecklist,
|
||||
'submit .js-edit-checklist-title': this.editChecklist,
|
||||
'submit .js-add-checklist-item': this.addChecklistItem,
|
||||
'submit .js-edit-checklist-item': this.editChecklistItem,
|
||||
'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
|
||||
'click .js-delete-checklist-item'(event) {
|
||||
const item = this.currentData().item;
|
||||
const confirmFunc = Popup.afterConfirm('checklistItemDelete', function () {
|
||||
if (item && item._id) {
|
||||
ChecklistItems.remove(item._id);
|
||||
}
|
||||
});
|
||||
confirmFunc.call(this, event);
|
||||
},
|
||||
'click .js-delete-checklist'(event) {
|
||||
const checklist = this.currentData().checklist;
|
||||
const confirmFunc = Popup.afterConfirm('checklistDelete', function () {
|
||||
Popup.back(2);
|
||||
if (checklist && checklist._id) {
|
||||
Checklists.remove(checklist._id);
|
||||
}
|
||||
});
|
||||
confirmFunc.call(this, event);
|
||||
},
|
||||
'focus .js-add-checklist-item': this.focusChecklistItem,
|
||||
// add and delete checklist / checklist-item
|
||||
'click .js-open-inlined-form': this.closeAllInlinedForms,
|
||||
keydown: this.pressKey,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('checklists');
|
||||
Template.addChecklistItemForm.events({
|
||||
'click a.fa.fa-copy'(event, tpl) {
|
||||
const $editor = tpl.$('textarea');
|
||||
const promise = Utils.copyTextToClipboard($editor[0].value);
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
subManager.subscribe('board', Session.get('currentBoard'), false);
|
||||
this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
|
||||
},
|
||||
|
||||
boards() {
|
||||
const ret = ReactiveCache.getBoards(
|
||||
{
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
_id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() },
|
||||
},
|
||||
{
|
||||
sort: { sort: 1 /* boards default sorting */ },
|
||||
},
|
||||
);
|
||||
return ret;
|
||||
},
|
||||
|
||||
swimlanes() {
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
return board.swimlanes();
|
||||
},
|
||||
|
||||
aBoardLists() {
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
return board.lists();
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'change .js-select-boards'(event) {
|
||||
this.selectedBoardId.set($(event.currentTarget).val());
|
||||
subManager.subscribe('board', this.selectedBoardId.get(), false);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('boardsSwimlanesAndLists');
|
||||
|
||||
Template.checklists.helpers({
|
||||
checklists() {
|
||||
const card = ReactiveCache.getCard(this.cardId);
|
||||
if (!card || typeof card.checklists !== 'function') return [];
|
||||
return card.checklists();
|
||||
const $tooltip = tpl.$('.copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
autosize(this.$('textarea.js-add-checklist-item'));
|
||||
Template.checklistActionsPopup.events({
|
||||
'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () {
|
||||
Popup.back(2);
|
||||
const checklist = this.checklist;
|
||||
if (checklist && checklist._id) {
|
||||
Checklists.remove(checklist._id);
|
||||
}
|
||||
}),
|
||||
'click .js-move-checklist': Popup.open('moveChecklist'),
|
||||
'click .js-copy-checklist': Popup.open('copyChecklist'),
|
||||
'click .js-hide-checked-checklist-items'(event) {
|
||||
event.preventDefault();
|
||||
Template.currentData().checklist.toggleHideCheckedChecklistItems();
|
||||
Popup.back();
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click a.fa.fa-copy'(event) {
|
||||
const $editor = this.$('textarea');
|
||||
const promise = Utils.copyTextToClipboard($editor[0].value);
|
||||
|
||||
const $tooltip = this.$('.copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
}
|
||||
];
|
||||
}
|
||||
}).register('addChecklistItemForm');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-delete-checklist'(event) {
|
||||
const checklist = this.data().checklist;
|
||||
const confirmFunc = Popup.afterConfirm('checklistDelete', function () {
|
||||
Popup.back(2);
|
||||
if (checklist && checklist._id) {
|
||||
Checklists.remove(checklist._id);
|
||||
}
|
||||
});
|
||||
confirmFunc.call(this, event);
|
||||
},
|
||||
'click .js-move-checklist': Popup.open('moveChecklist'),
|
||||
'click .js-copy-checklist': Popup.open('copyChecklist'),
|
||||
'click .js-hide-checked-checklist-items'(event) {
|
||||
event.preventDefault();
|
||||
this.data().checklist.toggleHideCheckedChecklistItems();
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-hide-all-checklist-items'(event) {
|
||||
event.preventDefault();
|
||||
this.data().checklist.toggleHideAllChecklistItems();
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-toggle-show-checklist-at-minicard'(event) {
|
||||
event.preventDefault();
|
||||
const checklist = this.data().checklist;
|
||||
checklist.toggleShowChecklistAtMinicard();
|
||||
Popup.back();
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}).register('checklistActionsPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
autosize(this.$('textarea.js-edit-checklist-item'));
|
||||
'click .js-hide-all-checklist-items'(event) {
|
||||
event.preventDefault();
|
||||
Template.currentData().checklist.toggleHideAllChecklistItems();
|
||||
Popup.back();
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click a.fa.fa-copy'(event) {
|
||||
const $editor = this.$('textarea');
|
||||
const promise = Utils.copyTextToClipboard($editor[0].value);
|
||||
});
|
||||
|
||||
const $tooltip = this.$('.copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
}
|
||||
];
|
||||
}
|
||||
}).register('editChecklistItemForm');
|
||||
Template.editChecklistItemForm.onRendered(function () {
|
||||
autosize(this.$('textarea.js-edit-checklist-item'));
|
||||
});
|
||||
|
||||
Template.editChecklistItemForm.events({
|
||||
'click a.fa.fa-copy'(event, tpl) {
|
||||
const $editor = tpl.$('textarea');
|
||||
const promise = Utils.copyTextToClipboard($editor[0].value);
|
||||
|
||||
const $tooltip = tpl.$('.copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
});
|
||||
|
||||
Template.checklistItemDetail.helpers({
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
toggleItem() {
|
||||
const checklist = this.currentData().checklist;
|
||||
const item = this.currentData().item;
|
||||
Template.checklistItemDetail.events({
|
||||
'click .js-checklist-item .check-box-container'() {
|
||||
const checklist = Template.currentData().checklist;
|
||||
const item = Template.currentData().item;
|
||||
if (checklist && item && item._id) {
|
||||
item.toggleItem();
|
||||
}
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-checklist-item .check-box-unicode': this.toggleItem,
|
||||
'click .js-checklist-item .check-box-container': this.toggleItem,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to find the dialog instance from a parent popup template.
|
||||
* copyAndMoveChecklist is included inside moveChecklistPopup / copyChecklistPopup,
|
||||
* so we traverse up the view hierarchy to find the parent template's dialog.
|
||||
*/
|
||||
function getParentDialog(tpl) {
|
||||
let view = tpl.view.parentView;
|
||||
while (view) {
|
||||
if (view.templateInstance && view.templateInstance() && view.templateInstance().dialog) {
|
||||
return view.templateInstance().dialog;
|
||||
}
|
||||
view = view.parentView;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Shared helpers for copyAndMoveChecklist sub-template */
|
||||
Template.copyAndMoveChecklist.helpers({
|
||||
boards() {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.boards() : [];
|
||||
},
|
||||
}).register('checklistItemDetail');
|
||||
swimlanes() {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.swimlanes() : [];
|
||||
},
|
||||
lists() {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.lists() : [];
|
||||
},
|
||||
cards() {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.cards() : [];
|
||||
},
|
||||
isDialogOptionBoardId(boardId) {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.isDialogOptionBoardId(boardId) : false;
|
||||
},
|
||||
isDialogOptionSwimlaneId(swimlaneId) {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.isDialogOptionSwimlaneId(swimlaneId) : false;
|
||||
},
|
||||
isDialogOptionListId(listId) {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.isDialogOptionListId(listId) : false;
|
||||
},
|
||||
isDialogOptionCardId(cardId) {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.isDialogOptionCardId(cardId) : false;
|
||||
},
|
||||
isTitleDefault(title) {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.isTitleDefault(title) : title;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper: register standard card dialog events on a checklist popup template.
|
||||
* Events bubble up from the copyAndMoveChecklist sub-template to the parent popup.
|
||||
*/
|
||||
function registerChecklistDialogEvents(templateName) {
|
||||
Template[templateName].events({
|
||||
async 'click .js-done'(event, tpl) {
|
||||
const dialog = tpl.dialog;
|
||||
const boardSelect = tpl.$('.js-select-boards')[0];
|
||||
const boardId = boardSelect.options[boardSelect.selectedIndex].value;
|
||||
|
||||
const listSelect = tpl.$('.js-select-lists')[0];
|
||||
const listId = listSelect.options[listSelect.selectedIndex].value;
|
||||
|
||||
const swimlaneSelect = tpl.$('.js-select-swimlanes')[0];
|
||||
const swimlaneId = swimlaneSelect.options[swimlaneSelect.selectedIndex].value;
|
||||
|
||||
const cardSelect = tpl.$('.js-select-cards')[0];
|
||||
const cardId = cardSelect.options.length > 0
|
||||
? cardSelect.options[cardSelect.selectedIndex].value
|
||||
: null;
|
||||
|
||||
const options = { boardId, swimlaneId, listId, cardId };
|
||||
try {
|
||||
await dialog.setDone(cardId, options);
|
||||
} catch (e) {
|
||||
console.error('Error in card dialog operation:', e);
|
||||
}
|
||||
Popup.back(2);
|
||||
},
|
||||
'change .js-select-boards'(event, tpl) {
|
||||
tpl.dialog.getBoardData($(event.currentTarget).val());
|
||||
},
|
||||
'change .js-select-swimlanes'(event, tpl) {
|
||||
tpl.dialog.selectedSwimlaneId.set($(event.currentTarget).val());
|
||||
tpl.dialog.setFirstListId();
|
||||
},
|
||||
'change .js-select-lists'(event, tpl) {
|
||||
tpl.dialog.selectedListId.set($(event.currentTarget).val());
|
||||
tpl.dialog.selectedCardId.set('');
|
||||
},
|
||||
'change .js-select-cards'(event, tpl) {
|
||||
tpl.dialog.selectedCardId.set($(event.currentTarget).val());
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Move Checklist Dialog */
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
|
||||
const checklist = this.data().checklist;
|
||||
Meteor.call('moveChecklist', checklist._id, cardId, (error) => {
|
||||
if (error) {
|
||||
console.error('Error moving checklist:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}).register('moveChecklistPopup');
|
||||
Template.moveChecklistPopup.onCreated(function () {
|
||||
this.dialog = new BoardSwimlaneListCardDialog(this, {
|
||||
getDialogOptions() {
|
||||
return ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
|
||||
},
|
||||
async setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
|
||||
await Template.currentData().checklist.move(cardId);
|
||||
},
|
||||
});
|
||||
});
|
||||
registerChecklistDialogEvents('moveChecklistPopup');
|
||||
|
||||
/** Copy Checklist Dialog */
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
|
||||
this.data().checklist.copy(cardId);
|
||||
}
|
||||
}).register('copyChecklistPopup');
|
||||
Template.copyChecklistPopup.onCreated(function () {
|
||||
this.dialog = new BoardSwimlaneListCardDialog(this, {
|
||||
getDialogOptions() {
|
||||
return ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
|
||||
},
|
||||
async setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
|
||||
await Template.currentData().checklist.copy(cardId);
|
||||
},
|
||||
});
|
||||
});
|
||||
registerChecklistDialogEvents('copyChecklistPopup');
|
||||
@@ -0,0 +1,6 @@
|
||||
template(name='inlinedCardDescription')
|
||||
if isOpen.get
|
||||
form.inlined-form.js-inlined-form(id=id class=classNames)
|
||||
+Template.contentBlock
|
||||
else
|
||||
+Template.elseBlock
|
||||
@@ -6,7 +6,7 @@ template(name="formLabel")
|
||||
.palette-colors: each labels
|
||||
span.card-label.palette-color.js-palette-color(class="card-label-{{color}}")
|
||||
if(isSelected color)
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
|
||||
template(name="createLabelPopup")
|
||||
form.create-label
|
||||
@@ -28,14 +28,13 @@ template(name="cardLabelsPopup")
|
||||
ul.edit-labels-pop-over
|
||||
each board.labels
|
||||
li.js-card-label-item
|
||||
a.card-label-edit-button.js-edit-label
|
||||
| ✏️
|
||||
a.card-label-edit-button.fa.fa-pencil.js-edit-label
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
span.label-handle(title="{{_ 'dragLabel'}}") ↕️
|
||||
span.fa.label-handle(class="fa-arrows" title="{{_ 'dragLabel'}}")
|
||||
span.card-label.card-label-selectable.js-select-label.card-label-wrapper(class="card-label-{{color}}"
|
||||
class="{{# if isLabelSelected ../_id }}active{{/if}}")
|
||||
+viewer
|
||||
= name
|
||||
if(isLabelSelected ../_id)
|
||||
| ✅
|
||||
i.card-label-selectable-icon.fa.fa-check
|
||||
a.quiet-button.full.js-add-label {{_ 'label-create'}}
|
||||
+106
-120
@@ -1,119 +1,117 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { LABEL_COLORS } from '/models/metadata/colors';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
let labelColors;
|
||||
Meteor.startup(() => {
|
||||
labelColors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
|
||||
labelColors = LABEL_COLORS;
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentColor = new ReactiveVar(this.data().color);
|
||||
},
|
||||
const getFallbackLabelColor = () => {
|
||||
if (Array.isArray(labelColors) && labelColors.length > 0) {
|
||||
return labelColors[0];
|
||||
}
|
||||
return 'green';
|
||||
};
|
||||
|
||||
Template.formLabel.onCreated(function () {
|
||||
const initialColor = this.data?.color || getFallbackLabelColor();
|
||||
this.currentColor = new ReactiveVar(initialColor);
|
||||
});
|
||||
|
||||
Template.formLabel.helpers({
|
||||
labels() {
|
||||
return labelColors.map(color => ({ color, name: '' }));
|
||||
const colors = Array.isArray(labelColors) ? labelColors : [getFallbackLabelColor()];
|
||||
return colors.map(color => ({ color, name: '' }));
|
||||
},
|
||||
|
||||
isSelected(color) {
|
||||
return this.currentColor.get() === color;
|
||||
return Template.instance().currentColor.get() === color;
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-palette-color'() {
|
||||
this.currentColor.set(this.currentData().color);
|
||||
},
|
||||
},
|
||||
];
|
||||
Template.formLabel.events({
|
||||
'click .js-palette-color'(event, tpl) {
|
||||
const paletteData = Blaze.getData(event.currentTarget);
|
||||
const selectedColor = paletteData?.color || Template.currentData()?.color || getFallbackLabelColor();
|
||||
tpl.currentColor.set(selectedColor);
|
||||
},
|
||||
}).register('formLabel');
|
||||
});
|
||||
|
||||
Template.createLabelPopup.helpers({
|
||||
// This is the default color for a new label. We search the first color that
|
||||
// is not already used in the board (although it's not a problem if two
|
||||
// labels have the same color).
|
||||
defaultColor() {
|
||||
const labels = Utils.getCurrentBoard().labels;
|
||||
const usedColors = _.pluck(labels, 'color');
|
||||
const availableColors = _.difference(labelColors, usedColors);
|
||||
return availableColors.length > 1 ? availableColors[0] : labelColors[0];
|
||||
const board = Utils.getCurrentBoard();
|
||||
const colors = Array.isArray(labelColors) ? labelColors : [getFallbackLabelColor()];
|
||||
const labels = Array.isArray(board?.labels) ? board.labels : [];
|
||||
const usedColors = labels.map(l => l.color);
|
||||
const availableColors = colors.filter(c => !usedColors.includes(c));
|
||||
return availableColors.length > 0 ? availableColors[0] : colors[0];
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
const itemsSelector = 'li.js-card-label-item:not(.placeholder)';
|
||||
const $labels = this.$('.edit-labels-pop-over');
|
||||
Template.cardLabelsPopup.onRendered(function () {
|
||||
const tpl = this;
|
||||
const itemsSelector = 'li.js-card-label-item:not(.placeholder)';
|
||||
const $labels = tpl.$('.edit-labels-pop-over');
|
||||
|
||||
$labels.sortable({
|
||||
connectWith: '.edit-labels-pop-over',
|
||||
tolerance: 'pointer',
|
||||
appendTo: '.edit-labels-pop-over',
|
||||
helper(element, currentItem) {
|
||||
let ret = currentItem.clone();
|
||||
if (currentItem.closest('.popup-container-depth-0').length == 0)
|
||||
{ // only set css transform at every sub-popup, not at the main popup
|
||||
const content = currentItem.closest('.content')[0]
|
||||
const offsetLeft = content.offsetLeft;
|
||||
const offsetTop = $('.pop-over > .header').height() * -1;
|
||||
ret.css("transform", `translate(${offsetLeft}px, ${offsetTop}px)`);
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
distance: 7,
|
||||
items: itemsSelector,
|
||||
placeholder: 'card-label-wrapper placeholder',
|
||||
start(evt, ui) {
|
||||
ui.helper.css('z-index', 1000);
|
||||
ui.placeholder.height(ui.helper.height());
|
||||
EscapeActions.clickExecute(evt.target, 'inlinedForm');
|
||||
},
|
||||
stop(evt, ui) {
|
||||
const newLabelOrderOnlyIds = ui.item.parent().children().toArray().map(_element => Blaze.getData(_element)._id)
|
||||
const card = Blaze.getData(this);
|
||||
card.board().setNewLabelOrder(newLabelOrderOnlyIds);
|
||||
},
|
||||
});
|
||||
|
||||
// Disable drag-dropping if the current user is not a board member or is comment only
|
||||
this.autorun(() => {
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$labels.sortable({
|
||||
handle: '.label-handle',
|
||||
});
|
||||
$labels.sortable({
|
||||
connectWith: '.edit-labels-pop-over',
|
||||
tolerance: 'pointer',
|
||||
appendTo: '.edit-labels-pop-over',
|
||||
helper(element, currentItem) {
|
||||
let ret = currentItem.clone();
|
||||
if (currentItem.closest('.popup-container-depth-0').length == 0)
|
||||
{ // only set css transform at every sub-popup, not at the main popup
|
||||
const content = currentItem.closest('.content')[0]
|
||||
const offsetLeft = content.offsetLeft;
|
||||
const offsetTop = $('.pop-over > .header').height() * -1;
|
||||
ret.css("transform", `translate(${offsetLeft}px, ${offsetTop}px)`);
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
distance: 7,
|
||||
items: itemsSelector,
|
||||
placeholder: 'card-label-wrapper placeholder',
|
||||
start(evt, ui) {
|
||||
ui.helper.css('z-index', 1000);
|
||||
ui.placeholder.height(ui.helper.height());
|
||||
EscapeActions.clickExecute(evt.target, 'inlinedForm');
|
||||
},
|
||||
stop(evt, ui) {
|
||||
const newLabelOrderOnlyIds = ui.item.parent().children().toArray().map(_element => Blaze.getData(_element)._id)
|
||||
const card = Blaze.getData(this);
|
||||
card.board().setNewLabelOrder(newLabelOrderOnlyIds);
|
||||
},
|
||||
});
|
||||
|
||||
// Disable drag-dropping if the current user is not a board member or is comment only
|
||||
tpl.autorun(() => {
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$labels.sortable({
|
||||
handle: '.label-handle',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.cardLabelsPopup.helpers({
|
||||
isLabelSelected(cardId) {
|
||||
return (ReactiveCache.getCard(cardId).labelIds || []).includes(this._id);
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-select-label'(event) {
|
||||
const card = this.data();
|
||||
const labelId = this.currentData()._id;
|
||||
card.toggleLabel(labelId);
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-edit-label': Popup.open('editLabel'),
|
||||
'click .js-add-label': Popup.open('createLabel'),
|
||||
}
|
||||
];
|
||||
}
|
||||
}).register('cardLabelsPopup');
|
||||
});
|
||||
|
||||
Template.cardLabelsPopup.events({
|
||||
});
|
||||
|
||||
Template.formLabel.events({
|
||||
'click .js-palette-color'(event) {
|
||||
const $this = $(event.currentTarget);
|
||||
|
||||
// hide selected ll colors
|
||||
$('.js-palette-select').addClass('hide');
|
||||
|
||||
// show select color
|
||||
$this.find('.js-palette-select').removeClass('hide');
|
||||
'click .js-select-label'(event) {
|
||||
const card = Template.currentData();
|
||||
const labelId = this._id;
|
||||
card.toggleLabel(labelId);
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-edit-label': Popup.open('editLabel'),
|
||||
'click .js-add-label': Popup.open('createLabel'),
|
||||
});
|
||||
|
||||
Template.createLabelPopup.events({
|
||||
@@ -121,23 +119,20 @@ Template.createLabelPopup.events({
|
||||
'submit .create-label'(event, templateInstance) {
|
||||
event.preventDefault();
|
||||
const board = Utils.getCurrentBoard();
|
||||
if (!board) {
|
||||
return;
|
||||
}
|
||||
const name = templateInstance
|
||||
.$('#labelName')
|
||||
.val()
|
||||
.trim();
|
||||
|
||||
// Find the selected color by looking for the palette color that contains the checkmark
|
||||
let selectedColor = null;
|
||||
templateInstance.$('.js-palette-color').each(function() {
|
||||
if ($(this).text().includes('✅')) {
|
||||
selectedColor = Blaze.getData(this).color;
|
||||
return false; // break out of loop
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedColor) {
|
||||
board.addLabel(name, selectedColor);
|
||||
}
|
||||
const selectedColorIcon = templateInstance.find('.js-palette-color .fa-check');
|
||||
const selectedPaletteNode = selectedColorIcon?.closest
|
||||
? selectedColorIcon.closest('.js-palette-color')
|
||||
: null;
|
||||
const selectedColorData = selectedPaletteNode && Blaze.getData(selectedPaletteNode);
|
||||
const color = selectedColorData?.color || getFallbackLabelColor();
|
||||
board.addLabel(name, color);
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
@@ -151,29 +146,20 @@ Template.editLabelPopup.events({
|
||||
'submit .edit-label'(event, templateInstance) {
|
||||
event.preventDefault();
|
||||
const board = Utils.getCurrentBoard();
|
||||
if (!board) {
|
||||
return;
|
||||
}
|
||||
const name = templateInstance
|
||||
.$('#labelName')
|
||||
.val()
|
||||
.trim();
|
||||
|
||||
// Find the selected color by looking for the palette color that contains the checkmark
|
||||
let selectedColor = null;
|
||||
templateInstance.$('.js-palette-color').each(function() {
|
||||
if ($(this).text().includes('✅')) {
|
||||
selectedColor = Blaze.getData(this).color;
|
||||
return false; // break out of loop
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedColor) {
|
||||
board.editLabel(this._id, name, selectedColor);
|
||||
}
|
||||
const selectedColorIcon = templateInstance.find('.js-palette-color .fa-check');
|
||||
const selectedPaletteNode = selectedColorIcon?.closest
|
||||
? selectedColorIcon.closest('.js-palette-color')
|
||||
: null;
|
||||
const selectedColorData = selectedPaletteNode && Blaze.getData(selectedPaletteNode);
|
||||
const color = selectedColorData?.color || getFallbackLabelColor();
|
||||
board.editLabel(this._id, name, color);
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
Template.cardLabelsPopup.helpers({
|
||||
isLabelSelected(cardId) {
|
||||
return _.contains(ReactiveCache.getCard(cardId).labelIds, this._id);
|
||||
},
|
||||
});
|
||||
@@ -44,12 +44,11 @@
|
||||
}
|
||||
}
|
||||
.minicard-details-menu-with-handle {
|
||||
position: absolute;
|
||||
right: 0.7vw;
|
||||
top: 0.7vh;
|
||||
float: right;
|
||||
padding-left: 0.7vw;
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
padding: 0;
|
||||
z-index: 10;
|
||||
z-index: 1;
|
||||
}
|
||||
.minicard-details-menu {
|
||||
float: right;
|
||||
@@ -99,6 +98,7 @@
|
||||
}
|
||||
.minicard .minicard-labels {
|
||||
float: none;
|
||||
margin-right: 6vw;
|
||||
}
|
||||
.minicard .minicard-labels .minicard-label {
|
||||
width: clamp(12px, 1.5vw, 16px);
|
||||
@@ -113,6 +113,7 @@
|
||||
}
|
||||
.minicard .minicard-custom-fields {
|
||||
display: block;
|
||||
margin-right: 6vw;
|
||||
}
|
||||
.minicard .minicard-custom-field {
|
||||
display: flex;
|
||||
@@ -135,10 +136,10 @@
|
||||
width: clamp(20px, 2.5vw, 28px);
|
||||
height: clamp(20px, 2.5vw, 28px);
|
||||
position: absolute;
|
||||
right: 3vw;
|
||||
top: 0.7vh;
|
||||
right: 0vw;
|
||||
top: 4vh;
|
||||
display: none;
|
||||
z-index: 10;
|
||||
z-index: 1;
|
||||
}
|
||||
@media only screen {
|
||||
.minicard .handle {
|
||||
@@ -152,6 +153,9 @@
|
||||
width: 1.4em;
|
||||
text-align: center;
|
||||
}
|
||||
.minicard .minicard-title {
|
||||
margin-right: 1.5vw;
|
||||
}
|
||||
.minicard .minicard-title .card-number {
|
||||
color: #b3b3b3;
|
||||
display: inline-block;
|
||||
@@ -170,6 +174,10 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
margin-right: 6vw;
|
||||
clear: both;
|
||||
}
|
||||
.minicard .date {
|
||||
margin-right: 0.4vw;
|
||||
|
||||
@@ -6,21 +6,26 @@ template(name="minicard")
|
||||
if canMoveCard
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
.handle
|
||||
| ↕️
|
||||
i.fa.fa-arrows
|
||||
if canModifyCard
|
||||
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") ☰
|
||||
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
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
|
||||
@@ -32,7 +37,7 @@ template(name="minicard")
|
||||
if hasActiveUploads
|
||||
.minicard-upload-progress
|
||||
.upload-progress-header
|
||||
| 📤
|
||||
i.fa.fa-upload
|
||||
span {{_ 'uploading-files'}} ({{uploadCount}})
|
||||
each uploads
|
||||
.upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
|
||||
@@ -41,11 +46,11 @@ template(name="minicard")
|
||||
.upload-progress-fill(style="width: {{progress}}%")
|
||||
if $eq status 'error'
|
||||
.upload-progress-error
|
||||
| ⚠️
|
||||
i.fa.fa-warning
|
||||
span {{_ 'upload-failed'}}
|
||||
else if $eq status 'completed'
|
||||
.upload-progress-success
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
span {{_ 'upload-completed'}}
|
||||
|
||||
.minicard-title
|
||||
@@ -57,26 +62,30 @@ template(name="minicard")
|
||||
| {{ parentCardName }}
|
||||
if isLinkedBoard
|
||||
a.js-linked-link
|
||||
span.linked-icon | 📁
|
||||
span.linked-icon
|
||||
i.fa.fa-folder
|
||||
else if isLinkedCard
|
||||
a.js-linked-link
|
||||
span.linked-icon | 🃏
|
||||
span.linked-icon
|
||||
i.fa.fa-id-card
|
||||
if getArchived
|
||||
span.linked-icon.linked-archived | 📦
|
||||
span.linked-icon.linked-archived
|
||||
i.fa.fa-archive
|
||||
+viewer
|
||||
if currentBoard.allowsCardNumber
|
||||
if showCardNumber
|
||||
span.card-number
|
||||
| ##{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,40 +152,50 @@ template(name="minicard")
|
||||
if canModifyCard
|
||||
if comments.length
|
||||
.badge(title="{{_ 'card-comments-title' comments.length }}")
|
||||
span.badge-icon.badge-comment.badge-text 💬
|
||||
span.badge-icon.badge-comment.badge-text
|
||||
i.fa.fa-comment-o
|
||||
= ' '
|
||||
= comments.length
|
||||
//span.badge-comment.badge-text
|
||||
//| {{_ 'comment'}}
|
||||
//|
|
||||
{{_ 'comment'}}
|
||||
if getDescription
|
||||
unless currentBoard.allowsDescriptionTextOnMinicard
|
||||
.badge.badge-state-image-only(title=getDescription)
|
||||
span.badge-icon 📝
|
||||
span.badge-icon
|
||||
i.fa.fa-file-text-o
|
||||
if getVoteQuestion
|
||||
.badge.badge-state-image-only(title=getVoteQuestion)
|
||||
span.badge-icon(class="{{#if voteState}}text-green{{/if}}") 👍
|
||||
span.badge-icon(class="{{#if voteState}}text-green{{/if}}")
|
||||
i.fa.fa-thumbs-up
|
||||
span.badge-text {{ voteCountPositive }}
|
||||
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}") 👎
|
||||
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}")
|
||||
i.fa.fa-thumbs-down
|
||||
span.badge-text {{ voteCountNegative }}
|
||||
if getPokerQuestion
|
||||
.badge.badge-state-image-only(title=getPokerQuestion)
|
||||
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}") ✅
|
||||
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}")
|
||||
i.fa.fa-check-square
|
||||
if expiredPoker
|
||||
span.badge-text {{ getPokerEstimation }}
|
||||
if attachments.length
|
||||
if currentBoard.allowsBadgeAttachmentOnMinicard
|
||||
.badge
|
||||
span.badge-icon 📎
|
||||
span.badge-icon
|
||||
i.fa.fa-paperclip
|
||||
span.badge-text= attachments.length
|
||||
if allSubtasks.count
|
||||
.badge
|
||||
span.badge-icon 🌐
|
||||
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 🔢
|
||||
span.badge-icon
|
||||
i.fa.fa-sort-numeric-asc
|
||||
span.badge-text.check-list-sort {{ sort }}
|
||||
if shouldShowChecklistAtMinicard
|
||||
each shouldShowChecklistAtMinicard
|
||||
@@ -188,7 +207,7 @@ template(name="minicard")
|
||||
| {{ getDescription }}
|
||||
if shouldShowListOnMinicard
|
||||
.minicard-list-name
|
||||
| 📋
|
||||
i.fa.fa-list
|
||||
| {{ listName }}
|
||||
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
@@ -207,7 +226,8 @@ template(name="minicardChecklist")
|
||||
.checklist-header
|
||||
.checklist-title= checklist.title
|
||||
if canModifyCard
|
||||
a.checklist-menu.js-open-checklist-menu(title="{{_ 'checklistActionsPopup-title'}}") ☰
|
||||
a.checklist-menu.js-open-checklist-menu(title="{{_ 'checklistActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
each visibleItems
|
||||
+checklistItemDetail(item = . checklist = checklist card = card)
|
||||
|
||||
+174
-213
@@ -3,18 +3,15 @@ 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';
|
||||
|
||||
// Template.cards.events({
|
||||
// 'click .member': Popup.open('cardMember')
|
||||
// });
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'minicard';
|
||||
},
|
||||
|
||||
Template.minicard.helpers({
|
||||
formattedCurrencyCustomFieldValue(definition) {
|
||||
const customField = this.data()
|
||||
const customField = this
|
||||
.customFieldsWD()
|
||||
.find(f => f._id === definition._id);
|
||||
const customFieldTrueValue =
|
||||
@@ -28,7 +25,7 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
formattedStringtemplateCustomFieldValue(definition) {
|
||||
const customField = this.data()
|
||||
const customField = this
|
||||
.customFieldsWD()
|
||||
.find(f => f._id === definition._id);
|
||||
|
||||
@@ -41,7 +38,7 @@ BlazeComponent.extendComponent({
|
||||
|
||||
showCreatorOnMinicard() {
|
||||
// cache "board" to reduce the mini-mongodb access
|
||||
const board = this.data().board();
|
||||
const board = this.board();
|
||||
let ret = false;
|
||||
if (board) {
|
||||
ret = board.allowsCreatorOnMinicard ?? false;
|
||||
@@ -49,172 +46,55 @@ BlazeComponent.extendComponent({
|
||||
return ret;
|
||||
},
|
||||
isWatching() {
|
||||
const card = this.currentData();
|
||||
return card.findWatcher(Meteor.userId());
|
||||
return this.findWatcher(Meteor.userId());
|
||||
},
|
||||
|
||||
showMembers() {
|
||||
// cache "board" to reduce the mini-mongodb access
|
||||
const board = this.data().board();
|
||||
const board = this.board();
|
||||
let ret = false;
|
||||
if (board) {
|
||||
ret =
|
||||
board.allowsMembers === null ||
|
||||
board.allowsMembers === undefined ||
|
||||
board.allowsMembers
|
||||
;
|
||||
ret = board.allowsMembersOnMinicard ?? false;
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
showAssignee() {
|
||||
// cache "board" to reduce the mini-mongodb access
|
||||
const board = this.data().board();
|
||||
const board = this.board();
|
||||
let ret = false;
|
||||
if (board) {
|
||||
ret =
|
||||
board.allowsAssignee === null ||
|
||||
board.allowsAssignee === undefined ||
|
||||
board.allowsAssignee
|
||||
;
|
||||
ret = board.allowsAssigneeOnMinicard ?? false;
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
/** opens the card label popup only if clicked onto a label
|
||||
* <li> this is necessary to have the data context of the minicard.
|
||||
* if .js-card-label is used at click event, then only the data context of the label itself is available at this.currentData()
|
||||
*/
|
||||
cardLabelsPopup(event) {
|
||||
if (this.find('.js-card-label:hover')) {
|
||||
Popup.open("cardLabels")(event, {dataContextIfCurrentDataIsUndefined: this.currentData()});
|
||||
}
|
||||
showReceived() {
|
||||
const board = this.board();
|
||||
return board ? board.allowsReceivedDateOnMinicard : false;
|
||||
},
|
||||
showStart() {
|
||||
const board = this.board();
|
||||
return board ? board.allowsStartDateOnMinicard : false;
|
||||
},
|
||||
showDue() {
|
||||
const board = this.board();
|
||||
return board ? board.allowsDueDateOnMinicard : false;
|
||||
},
|
||||
showEnd() {
|
||||
const board = this.board();
|
||||
return board ? board.allowsEndDateOnMinicard : false;
|
||||
},
|
||||
showLabels() {
|
||||
const board = this.board();
|
||||
return board ? board.allowsLabelsOnMinicard : false;
|
||||
},
|
||||
showCardNumber() {
|
||||
const board = this.board();
|
||||
return board ? board.allowsCardNumberOnMinicard : false;
|
||||
},
|
||||
showSubtasks() {
|
||||
const board = this.board();
|
||||
return board ? board.allowsSubtasksOnMinicard : false;
|
||||
},
|
||||
|
||||
toggleChecklistItem() {
|
||||
const item = this.currentData();
|
||||
if (item && item._id) {
|
||||
item.toggleItem();
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-linked-link'() {
|
||||
if (this.data().isLinkedCard()) Utils.goCardId(this.data().linkedId);
|
||||
else if (this.data().isLinkedBoard())
|
||||
Utils.goBoardId(this.data().linkedId);
|
||||
},
|
||||
'click .js-toggle-minicard-label-text'() {
|
||||
if (window.localStorage.getItem('hiddenMinicardLabelText')) {
|
||||
window.localStorage.removeItem('hiddenMinicardLabelText'); //true
|
||||
} else {
|
||||
window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true
|
||||
}
|
||||
},
|
||||
'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
|
||||
'click .minicard-labels' : this.cardLabelsPopup,
|
||||
'click .js-open-minicard-details-menu': Popup.open('cardDetailsActions'),
|
||||
// Drag and drop file upload handlers
|
||||
'dragover .minicard'(event) {
|
||||
// Only prevent default for file drags to avoid interfering with sortable
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
'dragenter .minicard'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const card = this.data();
|
||||
const board = card.board();
|
||||
// Only allow drag-and-drop if user can modify card and board allows attachments
|
||||
if (Utils.canModifyCard() && board && board.allowsAttachments) {
|
||||
$(event.currentTarget).addClass('is-dragging-over');
|
||||
}
|
||||
}
|
||||
},
|
||||
'dragleave .minicard'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
}
|
||||
},
|
||||
'drop .minicard'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
|
||||
const card = this.data();
|
||||
const board = card.board();
|
||||
|
||||
// Check permissions
|
||||
if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a file drop (not a card reorder)
|
||||
if (!dataTransfer.files || dataTransfer.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileUpload(card, files);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
];
|
||||
},
|
||||
}).register('minicard');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'minicardChecklist';
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-open-checklist-menu'(event) {
|
||||
const data = this.currentData();
|
||||
const checklist = data.checklist || data;
|
||||
const card = data.card || this.data();
|
||||
const context = { currentData: () => ({ checklist, card }) };
|
||||
Popup.open('checklistActions').call(context, event);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
visibleItems() {
|
||||
const checklist = this.currentData().checklist || this.currentData();
|
||||
const items = checklist.items();
|
||||
|
||||
return items.filter(item => {
|
||||
// Hide finished items if hideCheckedChecklistItems is true
|
||||
if (item.isFinished && checklist.hideCheckedChecklistItems) {
|
||||
return false;
|
||||
}
|
||||
// Hide all items if hideAllChecklistItems is true
|
||||
if (checklist.hideAllChecklistItems) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
}).register('minicardChecklist');
|
||||
|
||||
Template.minicard.helpers({
|
||||
hiddenMinicardLabelText() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (currentUser) {
|
||||
@@ -225,15 +105,23 @@ Template.minicard.helpers({
|
||||
return false;
|
||||
}
|
||||
},
|
||||
cover() {
|
||||
if (!this.coverId) return null;
|
||||
const attachment = ReactiveCache.getAttachment(this.coverId);
|
||||
if (!attachment) return null;
|
||||
const coverId = this.coverId;
|
||||
return {
|
||||
link() {
|
||||
return `/cdn/storage/attachments/${coverId}`;
|
||||
},
|
||||
};
|
||||
},
|
||||
// XXX resolve this nasty hack for https://github.com/veliovgroup/Meteor-Files/issues/763
|
||||
sess() {
|
||||
return Meteor.connection && Meteor.connection._lastSessionId
|
||||
? Meteor.connection._lastSessionId
|
||||
: null;
|
||||
},
|
||||
isWatching() {
|
||||
return this.findWatcher(Meteor.userId());
|
||||
},
|
||||
// Upload progress helpers
|
||||
hasActiveUploads() {
|
||||
return uploadProgressManager.hasActiveUploads(this._id);
|
||||
@@ -279,62 +167,135 @@ Template.minicard.helpers({
|
||||
}
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'keydown input.js-edit-card-sort-popup'(evt) {
|
||||
// enter = save
|
||||
if (evt.keyCode === 13) {
|
||||
this.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
'click button.js-submit-edit-card-sort-popup'(event) {
|
||||
// save button pressed
|
||||
event.preventDefault();
|
||||
const sort = this.$('.js-edit-card-sort-popup')[0]
|
||||
.value
|
||||
.trim();
|
||||
if (!Number.isNaN(sort)) {
|
||||
let card = this.data();
|
||||
card.move(card.boardId, card.swimlaneId, card.listId, sort);
|
||||
Popup.back();
|
||||
}
|
||||
},
|
||||
Template.minicard.events({
|
||||
'click .js-linked-link'() {
|
||||
if (this.isLinkedCard()) Utils.goCardId(this.linkedId);
|
||||
else if (this.isLinkedBoard())
|
||||
Utils.goBoardId(this.linkedId);
|
||||
},
|
||||
'click .js-toggle-minicard-label-text'() {
|
||||
if (window.localStorage.getItem('hiddenMinicardLabelText')) {
|
||||
window.localStorage.removeItem('hiddenMinicardLabelText'); //true
|
||||
} else {
|
||||
window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true
|
||||
}
|
||||
},
|
||||
'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
|
||||
'click .minicard-labels'(event, tpl) {
|
||||
if (tpl.find('.js-card-label:hover')) {
|
||||
Popup.open("cardLabels")(event, {dataContextIfCurrentDataIsUndefined: Template.currentData()});
|
||||
}
|
||||
},
|
||||
'click .js-open-minicard-details-menu'(event, tpl) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const card = Template.currentData();
|
||||
Popup.open('cardDetailsActions').call({currentData: () => card}, event);
|
||||
},
|
||||
// Drag and drop file upload handlers
|
||||
'dragover .minicard'(event) {
|
||||
// Only prevent default for file drags to avoid interfering with sortable
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
'dragenter .minicard'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const card = this;
|
||||
const board = card.board();
|
||||
// Only allow drag-and-drop if user can modify card and board allows attachments
|
||||
if (Utils.canModifyCard() && board && board.allowsAttachments) {
|
||||
$(event.currentTarget).addClass('is-dragging-over');
|
||||
}
|
||||
]
|
||||
}
|
||||
}).register('editCardSortOrderPopup');
|
||||
}
|
||||
},
|
||||
'dragleave .minicard'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
}
|
||||
},
|
||||
'drop .minicard'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
|
||||
Template.cardDetailsActionsPopup.events({
|
||||
'click .js-due-date': Popup.open('editCardDueDate'),
|
||||
'click .js-move-card': Popup.open('moveCard'),
|
||||
'click .js-copy-card': Popup.open('copyCard'),
|
||||
'click .js-set-card-color': Popup.open('setCardColor'),
|
||||
'click .js-add-labels': Popup.open('cardLabels'),
|
||||
'click .js-link': Popup.open('linkCard'),
|
||||
'click .js-move-card-to-top'(event) {
|
||||
event.preventDefault();
|
||||
const minOrder = this.getMinSort();
|
||||
this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
|
||||
Popup.back();
|
||||
const card = this;
|
||||
const board = card.board();
|
||||
|
||||
// Check permissions
|
||||
if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a file drop (not a card reorder)
|
||||
if (!dataTransfer.files || dataTransfer.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileUpload(card, files);
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-move-card-to-bottom'(event) {
|
||||
event.preventDefault();
|
||||
const maxOrder = this.getMaxSort();
|
||||
this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1);
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-archive': Popup.afterConfirm('cardArchive', function () {
|
||||
Popup.close();
|
||||
this.archive();
|
||||
Utils.goBoardId(this.boardId);
|
||||
}),
|
||||
'click .js-toggle-watch-card'() {
|
||||
const currentCard = this;
|
||||
const level = currentCard.findWatcher(Meteor.userId()) ? null : 'watching';
|
||||
Meteor.call('watch', 'card', currentCard._id, level, (err, ret) => {
|
||||
if (!err && ret) Popup.back();
|
||||
});
|
||||
|
||||
Template.minicardChecklist.helpers({
|
||||
visibleItems() {
|
||||
const checklist = this.checklist || this;
|
||||
const items = checklist.items();
|
||||
|
||||
return items.filter(item => {
|
||||
// Hide finished items if hideCheckedChecklistItems is true
|
||||
if (item.isFinished && checklist.hideCheckedChecklistItems) {
|
||||
return false;
|
||||
}
|
||||
// Hide all items if hideAllChecklistItems is true
|
||||
if (checklist.hideAllChecklistItems) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Template.minicardChecklist.events({
|
||||
'click .js-open-checklist-menu'(event) {
|
||||
const data = Template.currentData();
|
||||
const checklist = data.checklist || data;
|
||||
const card = data.card || this;
|
||||
const context = { currentData: () => ({ checklist, card }) };
|
||||
Popup.open('checklistActions').call(context, event);
|
||||
},
|
||||
});
|
||||
|
||||
Template.editCardSortOrderPopup.events({
|
||||
'keydown input.js-edit-card-sort-popup'(evt, tpl) {
|
||||
// enter = save
|
||||
if (evt.keyCode === 13) {
|
||||
tpl.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
'click button.js-submit-edit-card-sort-popup'(event, tpl) {
|
||||
// save button pressed
|
||||
event.preventDefault();
|
||||
const sort = tpl.$('.js-edit-card-sort-popup')[0]
|
||||
.value
|
||||
.trim();
|
||||
if (!Number.isNaN(sort)) {
|
||||
let card = this;
|
||||
card.move(card.boardId, card.swimlaneId, card.listId, sort);
|
||||
Popup.back();
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -13,7 +13,7 @@ template(name="resultCard")
|
||||
.broken-cards-null
|
||||
| {{_ 'no-name'}}
|
||||
if getBoard.archived
|
||||
| 📦
|
||||
i.fa.fa-archive
|
||||
li.result-card-context.result-card-context-separator
|
||||
= ' '
|
||||
| {{_ 'context-separator'}}
|
||||
@@ -27,7 +27,7 @@ template(name="resultCard")
|
||||
.broken-cards-null
|
||||
| {{_ 'no-name'}}
|
||||
if getSwimlane.archived
|
||||
| 📦
|
||||
i.fa.fa-archive
|
||||
li.result-card-context.result-card-context-separator
|
||||
= ' '
|
||||
| {{_ 'context-separator'}}
|
||||
@@ -41,4 +41,4 @@ template(name="resultCard")
|
||||
.broken-cards-null
|
||||
| {{_ 'no-name'}}
|
||||
if getList.archived
|
||||
| 📦
|
||||
i.fa.fa-archive
|
||||
@@ -1,35 +1,67 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
|
||||
Template.resultCard.helpers({
|
||||
userId() {
|
||||
return Meteor.userId();
|
||||
},
|
||||
getBoard() {
|
||||
const card = Template.currentData();
|
||||
try {
|
||||
if (card && card.boardId) {
|
||||
const board = ReactiveCache.getBoard(card.boardId);
|
||||
return board || null;
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently handle errors
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getSwimlane() {
|
||||
const card = Template.currentData();
|
||||
try {
|
||||
if (card && card.swimlaneId) {
|
||||
const swimlane = ReactiveCache.getSwimlane(card.swimlaneId);
|
||||
return swimlane || null;
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently handle errors
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getList() {
|
||||
const card = Template.currentData();
|
||||
try {
|
||||
if (card && card.listId) {
|
||||
const list = ReactiveCache.getList(card.listId);
|
||||
return list || null;
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently handle errors
|
||||
}
|
||||
return null;
|
||||
},
|
||||
originRelativeUrl() {
|
||||
const card = Template.currentData();
|
||||
if (card && card.boardId && card._id) {
|
||||
return `/board/${card.boardId}?cardId=${card._id}`;
|
||||
}
|
||||
return '#';
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
clickOnMiniCard(evt) {
|
||||
evt.preventDefault();
|
||||
const this_ = this;
|
||||
const cardId = this.currentData()._id;
|
||||
const boardId = this.currentData().boardId;
|
||||
Template.resultCard.events({
|
||||
'click .js-minicard'(event) {
|
||||
event.preventDefault();
|
||||
const cardId = Template.currentData()._id;
|
||||
const boardId = Template.currentData().boardId;
|
||||
Meteor.subscribe('popupCardData', cardId, {
|
||||
onReady() {
|
||||
Session.set('popupCardId', cardId);
|
||||
Session.set('popupCardBoardId', boardId);
|
||||
this_.cardDetailsPopup(evt);
|
||||
if (!Popup.isOpen()) {
|
||||
Popup.open("cardDetails")(event);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
cardDetailsPopup(event) {
|
||||
if (!Popup.isOpen()) {
|
||||
Popup.open("cardDetails")(event);
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-minicard': this.clickOnMiniCard,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('resultCard');
|
||||
});
|
||||
@@ -15,7 +15,7 @@ textarea.js-edit-subtask-item {
|
||||
color: #8c8c8c;
|
||||
text-decoration: underline;
|
||||
word-wrap: break-word;
|
||||
float: right;
|
||||
flex-shrink: 0;
|
||||
padding-top: 6px;
|
||||
}
|
||||
.delete-text:hover,
|
||||
@@ -24,8 +24,13 @@ textarea.js-edit-subtask-item {
|
||||
.js-delete-subtask-item:hover {
|
||||
color: inherit;
|
||||
}
|
||||
.subtask-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.subtask-title .checkbox {
|
||||
float: left;
|
||||
flex-shrink: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 18px;
|
||||
@@ -34,10 +39,12 @@ textarea.js-edit-subtask-item {
|
||||
.subtask-title .title {
|
||||
font-size: 18px;
|
||||
line-height: 25px;
|
||||
flex: 1;
|
||||
order: 1;
|
||||
}
|
||||
.subtask-title .subtasks-stat {
|
||||
margin: 0 0.5em;
|
||||
float: right;
|
||||
flex-shrink: 0;
|
||||
padding-top: 6px;
|
||||
}
|
||||
.subtask-title .subtasks-stat.is-finished {
|
||||
@@ -46,6 +53,9 @@ textarea.js-edit-subtask-item {
|
||||
.subtask-title .js-delete-subtask {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
.subtask-title .js-view-subtask {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
.js-confirm-subtask-delete {
|
||||
background-color: #f7f7f7;
|
||||
position: absolute;
|
||||
@@ -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,6 +1,6 @@
|
||||
template(name="subtasks")
|
||||
h3.card-details-item-title
|
||||
| 🌐
|
||||
i.fa.fa-globe
|
||||
| {{_ 'subtasks'}}
|
||||
if currentUser.isBoardAdmin
|
||||
if toggleDeleteDialog.get
|
||||
@@ -16,7 +16,7 @@ template(name="subtasks")
|
||||
+addSubtaskItemForm
|
||||
else
|
||||
a.js-open-inlined-form(title="{{_ 'add-subtask'}}")
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
|
||||
template(name="subtaskDetail")
|
||||
.js-subtasks.subtask
|
||||
@@ -26,7 +26,7 @@ template(name="subtaskDetail")
|
||||
.subtask-title
|
||||
span
|
||||
if canModifyCard
|
||||
a.subtask-details-menu.js-open-subtask-details-menu(title="{{_ 'subtaskActionsPopup-title'}}")
|
||||
a.fa.fa-navicon.subtask-details-menu.js-open-subtask-details-menu(title="{{_ 'subtaskActionsPopup-title'}}")
|
||||
if canModifyCard
|
||||
h2.title.js-open-inlined-form.is-editable
|
||||
+viewer
|
||||
@@ -40,7 +40,7 @@ template(name="addSubtaskItemForm")
|
||||
textarea.js-add-subtask-item(rows='1' autofocus dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-add-subtask-item-form(type="submit") {{_ 'save'}}
|
||||
a.js-close-inlined-form
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
|
||||
|
||||
template(name="editSubtaskItemForm")
|
||||
textarea.js-edit-subtask-item(rows='1' autofocus dir="auto")
|
||||
@@ -50,8 +50,8 @@ template(name="editSubtaskItemForm")
|
||||
= subtask.title
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-subtask-item-form(type="submit") {{_ 'save'}}
|
||||
a.js-close-inlined-form
|
||||
span(title=createdAt) {{ moment createdAt }}
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
|
||||
span(title=createdAt) {{ displayDate createdAt }}
|
||||
if canModifyCard
|
||||
if currentUser.isBoardAdmin
|
||||
a.js-delete-subtask-item {{_ "delete"}}...
|
||||
@@ -68,18 +68,20 @@ template(name="subtasksItems")
|
||||
+addSubtaskItemForm
|
||||
else
|
||||
a.add-subtask-item.js-open-inlined-form
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-subtask-item'}}...
|
||||
|
||||
template(name='subtaskItemDetail')
|
||||
.js-subtasks-item.subtasks-item
|
||||
if canModifyCard
|
||||
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
|
||||
span.check-box-unicode
|
||||
i.fa(class="{{#if item.isFinished}}fa-check-square{{else}}fa-square-o{{/if}}")
|
||||
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
else
|
||||
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
|
||||
span.check-box-unicode
|
||||
i.fa(class="{{#if item.isFinished}}fa-check-square{{else}}fa-square-o{{/if}}")
|
||||
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
@@ -92,10 +94,14 @@ template(name="subtaskActionsPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-view-subtask(title="{{ subtask.title }}")
|
||||
| 👁️
|
||||
i.fa.fa-eye
|
||||
| {{_ "view-it"}}
|
||||
li
|
||||
a.js-go-to-subtask-board
|
||||
i.fa.fa-folder
|
||||
| {{_ "go-to-board"}}
|
||||
if currentUser.isBoardAdmin
|
||||
a.js-delete-subtask.delete-subtask
|
||||
| 🗑️
|
||||
i.fa.fa-trash
|
||||
| {{_ "delete"}} ...
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import Cards from '/models/cards';
|
||||
import { Filter } from '/client/lib/filter';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
addSubtask(event) {
|
||||
Template.subtasks.events({
|
||||
'click .js-open-subtask-details-menu'(event) {
|
||||
// Close any existing popup first to avoid accumulating content
|
||||
if (Popup.isOpen()) {
|
||||
Popup.close();
|
||||
}
|
||||
// Now open the popup for this specific subtask
|
||||
Popup.open('subtaskActions').call(this, event);
|
||||
},
|
||||
async 'submit .js-add-subtask'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-add-subtask-item');
|
||||
const textarea = tpl.find('textarea.js-add-subtask-item');
|
||||
const title = textarea.value.trim();
|
||||
const cardId = this.currentData().cardId;
|
||||
const cardId = Template.currentData().cardId;
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
const sortIndex = -1;
|
||||
const crtBoard = ReactiveCache.getBoard(card.boardId);
|
||||
const targetBoard = crtBoard.getDefaultSubtasksBoard();
|
||||
const listId = targetBoard.getDefaultSubtasksListId();
|
||||
const targetBoard = await crtBoard.getDefaultSubtasksBoardAsync();
|
||||
if (!targetBoard) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetList = await targetBoard.getDefaultSubtasksListAsync();
|
||||
if (!targetList) {
|
||||
return;
|
||||
}
|
||||
const listId = targetList._id;
|
||||
|
||||
//Get the full swimlane data for the parent task.
|
||||
const parentSwimlane = ReactiveCache.getSwimlane({
|
||||
@@ -19,20 +37,22 @@ BlazeComponent.extendComponent({
|
||||
_id: card.swimlaneId,
|
||||
});
|
||||
//find the swimlane of the same name in the target board.
|
||||
const targetSwimlane = ReactiveCache.getSwimlane({
|
||||
boardId: targetBoard._id,
|
||||
title: parentSwimlane.title,
|
||||
});
|
||||
const targetSwimlane = parentSwimlane
|
||||
? ReactiveCache.getSwimlane({
|
||||
boardId: targetBoard._id,
|
||||
title: parentSwimlane.title,
|
||||
})
|
||||
: undefined;
|
||||
//If no swimlane with a matching title exists in the target board, fall back to the default swimlane.
|
||||
const swimlaneId =
|
||||
targetSwimlane === undefined
|
||||
? targetBoard.getDefaultSwimline()._id
|
||||
: targetSwimlane._id;
|
||||
|
||||
const nextCardNumber = targetBoard.getNextCardNumber();
|
||||
const nextCardNumber = await targetBoard.getNextCardNumber();
|
||||
|
||||
if (title) {
|
||||
const _id = Cards.insert({
|
||||
const _id = await Cards.insertAsync({
|
||||
title,
|
||||
parentId: cardId,
|
||||
members: [],
|
||||
@@ -43,7 +63,7 @@ BlazeComponent.extendComponent({
|
||||
sort: sortIndex,
|
||||
swimlaneId,
|
||||
type: 'cardType-card',
|
||||
cardNumber: nextCardNumber
|
||||
cardNumber: nextCardNumber,
|
||||
});
|
||||
|
||||
// In case the filter is active we need to add the newly inserted card in
|
||||
@@ -53,7 +73,7 @@ BlazeComponent.extendComponent({
|
||||
Filter.addException(_id);
|
||||
|
||||
setTimeout(() => {
|
||||
this.$('.add-subtask-item')
|
||||
tpl.$('.add-subtask-item')
|
||||
.last()
|
||||
.click();
|
||||
}, 100);
|
||||
@@ -61,27 +81,20 @@ BlazeComponent.extendComponent({
|
||||
textarea.value = '';
|
||||
textarea.focus();
|
||||
},
|
||||
|
||||
deleteSubtask() {
|
||||
const subtask = this.currentData().subtask;
|
||||
if (subtask && subtask._id) {
|
||||
subtask.archive();
|
||||
}
|
||||
},
|
||||
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
|
||||
editSubtask(event) {
|
||||
'submit .js-edit-subtask-title'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-edit-subtask-item');
|
||||
const textarea = tpl.find('textarea.js-edit-subtask-item');
|
||||
const title = textarea.value.trim();
|
||||
const subtask = this.currentData().subtask;
|
||||
const subtask = Template.currentData().subtask;
|
||||
subtask.setTitle(title);
|
||||
},
|
||||
|
||||
pressKey(event) {
|
||||
async 'click .js-delete-subtask-item'() {
|
||||
const subtask = Template.currentData().subtask;
|
||||
if (subtask && subtask._id) {
|
||||
await subtask.archive();
|
||||
}
|
||||
},
|
||||
keydown(event) {
|
||||
//If user press enter key inside a form, submit it
|
||||
//Unless the user is also holding down the 'shift' key
|
||||
if (event.keyCode === 13 && !event.shiftKey) {
|
||||
@@ -90,65 +103,66 @@ BlazeComponent.extendComponent({
|
||||
$form.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-open-subtask-details-menu': Popup.open('subtaskActions'),
|
||||
'submit .js-add-subtask': this.addSubtask,
|
||||
'submit .js-edit-subtask-title': this.editSubtask,
|
||||
'click .js-delete-subtask-item': this.deleteSubtask,
|
||||
keydown: this.pressKey,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('subtasks');
|
||||
Template.subtasks.onCreated(function () {
|
||||
this.toggleDeleteDialog = new ReactiveVar(false);
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
toggleItem() {
|
||||
const item = this.currentData().item;
|
||||
if (item && item._id) {
|
||||
item.toggleItem();
|
||||
}
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-subtasks-item .check-box-unicode': this.toggleItem,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('subtaskItemDetail');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.subtasks.helpers({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-view-subtask'(event) {
|
||||
if ($(event.target).hasClass('js-view-subtask')) {
|
||||
const subtask = this.currentData().subtask;
|
||||
const board = subtask.board();
|
||||
FlowRouter.go('card', {
|
||||
boardId: board._id,
|
||||
slug: board.slug,
|
||||
cardId: subtask._id,
|
||||
});
|
||||
}
|
||||
},
|
||||
'click .js-delete-subtask' : Popup.afterConfirm('subtaskDelete', function () {
|
||||
Popup.back(2);
|
||||
const subtask = this.subtask;
|
||||
if (subtask && subtask._id) {
|
||||
subtask.archive();
|
||||
}
|
||||
}),
|
||||
}
|
||||
]
|
||||
}
|
||||
}).register('subtaskActionsPopup');
|
||||
toggleDeleteDialog() {
|
||||
return Template.instance().toggleDeleteDialog;
|
||||
},
|
||||
});
|
||||
|
||||
Template.subtaskItemDetail.events({
|
||||
async 'click .js-subtasks-item .check-box-unicode'() {
|
||||
const item = Template.currentData().item;
|
||||
if (item && item._id) {
|
||||
await item.toggleItem();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.subtaskActionsPopup.helpers({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
});
|
||||
|
||||
Template.subtaskActionsPopup.events({
|
||||
'click .js-view-subtask'(event) {
|
||||
if ($(event.target).hasClass('js-view-subtask')) {
|
||||
const subtask = Template.currentData().subtask;
|
||||
const board = subtask.board();
|
||||
FlowRouter.go('card', {
|
||||
boardId: board._id,
|
||||
slug: board.slug,
|
||||
cardId: subtask._id,
|
||||
swimlaneId: subtask.swimlaneId,
|
||||
listId: subtask.listId,
|
||||
});
|
||||
}
|
||||
},
|
||||
'click .js-go-to-subtask-board'() {
|
||||
const subtask = Template.currentData().subtask;
|
||||
const board = subtask.board();
|
||||
if (board) {
|
||||
Popup.close();
|
||||
FlowRouter.go('board', { id: board._id, slug: board.slug });
|
||||
}
|
||||
},
|
||||
'click .js-delete-subtask' : Popup.afterConfirm('subtaskDelete', async function () {
|
||||
Popup.back(2);
|
||||
const subtask = this.subtask;
|
||||
if (subtask && subtask._id) {
|
||||
await subtask.archive();
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
Template.editSubtaskItemForm.helpers({
|
||||
user() {
|
||||
@@ -158,5 +172,3 @@ Template.editSubtaskItemForm.helpers({
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -81,11 +81,11 @@
|
||||
font-size: 11px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
|
||||
.original-position-details {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
|
||||
.original-position-moved,
|
||||
.original-position-unchanged {
|
||||
padding: 3px 5px;
|
||||
@@ -99,24 +99,24 @@
|
||||
border-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
|
||||
.original-position-moved {
|
||||
background-color: #744210;
|
||||
border-color: #b7791f;
|
||||
color: #fbd38d;
|
||||
}
|
||||
|
||||
|
||||
.original-position-unchanged {
|
||||
background-color: #22543d;
|
||||
border-color: #38a169;
|
||||
color: #9ae6b4;
|
||||
}
|
||||
|
||||
|
||||
.original-title {
|
||||
color: #a0aec0;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
|
||||
.original-title strong {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<template name="originalPosition">
|
||||
<div class="original-position-info">
|
||||
{{#if isLoading}}
|
||||
<div class="original-position-loading">
|
||||
⏳ Loading original position...
|
||||
</div>
|
||||
{{else if showOriginalPosition}}
|
||||
<div class="original-position-details">
|
||||
{{#if hasMovedFromOriginal}}
|
||||
<div class="original-position-moved">
|
||||
<span class="original-position-text">ℹ️ {{getOriginalPositionDescription}}</span>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="original-position-unchanged">
|
||||
<span class="original-position-text">✅ In original position</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if getOriginalTitle}}
|
||||
<div class="original-title">
|
||||
<strong>Original title:</strong> {{getOriginalTitle}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
template(name="originalPosition")
|
||||
.original-position-info
|
||||
if isLoading
|
||||
.original-position-loading
|
||||
| ⏳ Loading original position...
|
||||
else
|
||||
if showOriginalPosition
|
||||
.original-position-details
|
||||
if hasMovedFromOriginal
|
||||
.original-position-moved
|
||||
span.original-position-text ℹ️ {{getOriginalPositionDescription}}
|
||||
else
|
||||
.original-position-unchanged
|
||||
span.original-position-text ✅ In original position
|
||||
|
||||
if getOriginalTitle
|
||||
.original-title
|
||||
strong Original title:
|
||||
| {{getOriginalTitle}}
|
||||
@@ -1,71 +1,71 @@
|
||||
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import './originalPosition.html';
|
||||
|
||||
/**
|
||||
* Component to display original position information for swimlanes, lists, and cards
|
||||
*/
|
||||
class OriginalPositionComponent extends BlazeComponent {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
this.originalPosition = new ReactiveVar(null);
|
||||
this.isLoading = new ReactiveVar(false);
|
||||
this.hasMoved = new ReactiveVar(false);
|
||||
|
||||
this.autorun(() => {
|
||||
const data = this.data();
|
||||
if (data && data.entityId && data.entityType) {
|
||||
this.loadOriginalPosition(data.entityId, data.entityType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadOriginalPosition(entityId, entityType) {
|
||||
this.isLoading.set(true);
|
||||
|
||||
Template.originalPosition.onCreated(function () {
|
||||
this.originalPosition = new ReactiveVar(null);
|
||||
this.isLoading = new ReactiveVar(false);
|
||||
this.hasMoved = new ReactiveVar(false);
|
||||
|
||||
const tpl = this;
|
||||
|
||||
function loadOriginalPosition(entityId, entityType) {
|
||||
tpl.isLoading.set(true);
|
||||
|
||||
const methodName = `positionHistory.get${entityType.charAt(0).toUpperCase() + entityType.slice(1)}OriginalPosition`;
|
||||
|
||||
|
||||
Meteor.call(methodName, entityId, (error, result) => {
|
||||
this.isLoading.set(false);
|
||||
tpl.isLoading.set(false);
|
||||
if (error) {
|
||||
console.error('Error loading original position:', error);
|
||||
this.originalPosition.set(null);
|
||||
tpl.originalPosition.set(null);
|
||||
} else {
|
||||
this.originalPosition.set(result);
|
||||
|
||||
tpl.originalPosition.set(result);
|
||||
|
||||
// Check if the entity has moved
|
||||
const movedMethodName = `positionHistory.has${entityType.charAt(0).toUpperCase() + entityType.slice(1)}Moved`;
|
||||
Meteor.call(movedMethodName, entityId, (movedError, movedResult) => {
|
||||
if (!movedError) {
|
||||
this.hasMoved.set(movedResult);
|
||||
tpl.hasMoved.set(movedResult);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.autorun(() => {
|
||||
const data = Template.currentData();
|
||||
if (data && data.entityId && data.entityType) {
|
||||
loadOriginalPosition(data.entityId, data.entityType);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.originalPosition.helpers({
|
||||
getOriginalPosition() {
|
||||
return this.originalPosition.get();
|
||||
}
|
||||
return Template.instance().originalPosition.get();
|
||||
},
|
||||
|
||||
isLoading() {
|
||||
return this.isLoading.get();
|
||||
}
|
||||
return Template.instance().isLoading.get();
|
||||
},
|
||||
|
||||
hasMovedFromOriginal() {
|
||||
return this.hasMoved.get();
|
||||
}
|
||||
return Template.instance().hasMoved.get();
|
||||
},
|
||||
|
||||
getOriginalPositionDescription() {
|
||||
const position = this.getOriginalPosition();
|
||||
const position = Template.instance().originalPosition.get();
|
||||
if (!position) return 'No original position data';
|
||||
|
||||
|
||||
if (position.originalPosition) {
|
||||
const entityType = this.data().entityType;
|
||||
const data = Template.currentData();
|
||||
const entityType = data.entityType;
|
||||
let description = `Original position: ${position.originalPosition.sort || 0}`;
|
||||
|
||||
|
||||
if (entityType === 'list' && position.originalSwimlaneId) {
|
||||
description += ` in swimlane ${position.originalSwimlaneId}`;
|
||||
} else if (entityType === 'card') {
|
||||
@@ -76,23 +76,19 @@ class OriginalPositionComponent extends BlazeComponent {
|
||||
description += ` in list ${position.originalListId}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
|
||||
return 'No original position data';
|
||||
}
|
||||
},
|
||||
|
||||
getOriginalTitle() {
|
||||
const position = this.getOriginalPosition();
|
||||
const position = Template.instance().originalPosition.get();
|
||||
return position ? position.originalTitle : '';
|
||||
}
|
||||
},
|
||||
|
||||
showOriginalPosition() {
|
||||
return this.getOriginalPosition() !== null;
|
||||
}
|
||||
}
|
||||
|
||||
OriginalPositionComponent.register('originalPosition');
|
||||
|
||||
export default OriginalPositionComponent;
|
||||
return Template.instance().originalPosition.get() !== null;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
setupDatePicker,
|
||||
datePickerRendered,
|
||||
datePickerHelpers,
|
||||
datePickerEvents,
|
||||
} from '/client/lib/datepicker';
|
||||
|
||||
Template.datepicker.onCreated(function () {
|
||||
setupDatePicker(this);
|
||||
});
|
||||
|
||||
Template.datepicker.onRendered(function () {
|
||||
datePickerRendered(this);
|
||||
});
|
||||
|
||||
Template.datepicker.helpers(datePickerHelpers());
|
||||
@@ -130,8 +130,8 @@ textarea.editor {
|
||||
}
|
||||
input[type="submit"],
|
||||
button {
|
||||
background: #cfcfcf;
|
||||
background: linear-gradient(#cfcfcf, #c2c2c2);
|
||||
background: #000;
|
||||
background: linear-gradient(#000, #000);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
@@ -139,6 +139,7 @@ button {
|
||||
line-height: 1.3;
|
||||
padding: 1vh 2.5vw;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
input[type="submit"] .wide,
|
||||
button .wide {
|
||||
@@ -149,14 +150,16 @@ input[type="submit"]:hover,
|
||||
button:hover,
|
||||
input[type="submit"]:focus,
|
||||
button:focus {
|
||||
background: #c2c2c2;
|
||||
background: linear-gradient(#c2c2c2, #b5b5b5);
|
||||
background: #222;
|
||||
background: linear-gradient(#222, #222);
|
||||
color: #fff;
|
||||
}
|
||||
input[type="submit"]:active,
|
||||
button:active {
|
||||
background: #b5b5b5;
|
||||
background: linear-gradient(#b5b5b5, #a8a8a8);
|
||||
box-shadow: inset 0 3px 6px rgba(0,0,0,0.1);
|
||||
background: #111;
|
||||
background: linear-gradient(#111, #111);
|
||||
box-shadow: inset 0 3px 6px rgba(0,0,0,0.3);
|
||||
color: #fff;
|
||||
}
|
||||
input[type="submit"]:active:hover,
|
||||
button:active:hover,
|
||||
@@ -183,6 +186,12 @@ input[type="submit"].primary:active,
|
||||
button.primary:active {
|
||||
background: #01628c;
|
||||
}
|
||||
input[type="submit"].negate,
|
||||
button.negate {
|
||||
background: #eb5a46;
|
||||
box-shadow: 0 1px 0 #4d4d4d;
|
||||
color: #fff;
|
||||
}
|
||||
input[type="submit"].negate:hover,
|
||||
button.negate:hover,
|
||||
input[type="submit"].negate:focus,
|
||||
@@ -217,10 +226,10 @@ input[type="submit"]:disabled:active,
|
||||
input[type="button"].disabled:active,
|
||||
button.disabled:active,
|
||||
.button.disabled:active {
|
||||
background: #cfcfcf;
|
||||
background: #555;
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
color: #a8a8a8;
|
||||
color: #999;
|
||||
}
|
||||
fieldset {
|
||||
border: 1px solid #bfbfbf;
|
||||
@@ -400,12 +409,12 @@ body.grey-icons-enabled .materialCheckBox.is-checked {
|
||||
.button-link.setting.disabled.primary,
|
||||
.button-link.setting.disabled.primary:hover,
|
||||
.button-link.setting.disabled.primary:active {
|
||||
background: #cfcfcf;
|
||||
border-color: #c2c2c2;
|
||||
border-bottom-color: #b5b5b5;
|
||||
background: #555;
|
||||
border-color: #444;
|
||||
border-bottom-color: #333;
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
color: #a8a8a8;
|
||||
color: #999;
|
||||
}
|
||||
.button-link.setting .label {
|
||||
color: #222;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
template(name='inlinedForm')
|
||||
if isOpen.get
|
||||
form.inlined-form.js-inlined-form(id=id class=classNames)
|
||||
+Template.contentBlock
|
||||
else
|
||||
+Template.elseBlock
|
||||
.js-inlined-form-wrapper(style='display: contents;' data-position=position)
|
||||
if isOpen.get
|
||||
form.inlined-form.js-inlined-form(id=id class=classNames)
|
||||
+Template.contentBlock
|
||||
else
|
||||
+Template.elseBlock
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,34 +1,30 @@
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
// Provide the expected parent component properties for cardDetails
|
||||
this.showOverlay = new ReactiveVar(false);
|
||||
this.mouseHasEnterCardDetails = false;
|
||||
},
|
||||
Template.ganttCard.onCreated(function () {
|
||||
// Provide the expected parent component properties for cardDetails
|
||||
this.showOverlay = new ReactiveVar(false);
|
||||
this.mouseHasEnterCardDetails = false;
|
||||
});
|
||||
|
||||
Template.ganttCard.helpers({
|
||||
selectedCard() {
|
||||
// The selected card is now passed as a parameter to the component
|
||||
return this.currentData();
|
||||
return Template.currentData();
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-close-card-details'(event) {
|
||||
event.preventDefault();
|
||||
// Find the ganttView template instance and clear selectedCardId
|
||||
let view = Blaze.currentView;
|
||||
while (view) {
|
||||
if (view.templateInstance && view.templateInstance().selectedCardId) {
|
||||
view.templateInstance().selectedCardId.set(null);
|
||||
break;
|
||||
}
|
||||
view = view.parentView;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
Template.ganttCard.events({
|
||||
'click .js-close-card-details'(event) {
|
||||
event.preventDefault();
|
||||
// Find the ganttView template instance and clear selectedCardId
|
||||
let view = Blaze.currentView;
|
||||
while (view) {
|
||||
if (view.templateInstance && view.templateInstance().selectedCardId) {
|
||||
view.templateInstance().selectedCardId.set(null);
|
||||
break;
|
||||
}
|
||||
view = view.parentView;
|
||||
}
|
||||
},
|
||||
}).register('ganttCard');
|
||||
});
|
||||
|
||||
// Add click handler to ganttView for card titles
|
||||
Template.ganttView.events({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
template(name="importHeaderBar")
|
||||
h1
|
||||
a.back-btn(href="{{pathFor 'home'}}")
|
||||
| ⬅️
|
||||
i.fa-arrow-left
|
||||
| {{_ title}}
|
||||
|
||||
template(name="import")
|
||||
@@ -36,7 +36,7 @@ template(name="importMapMembers")
|
||||
+userAvatar(userId=wekanId)
|
||||
else
|
||||
a.member.add-member
|
||||
| ➕
|
||||
i.fa.fa-plus
|
||||
//-
|
||||
Due to the way the flewbox layout is working, we need to set some
|
||||
invisible items so that the last row items have a consistent width.
|
||||
|
||||
+218
-220
@@ -3,39 +3,69 @@ import { trelloGetMembersToMap } from './trelloMembersMapper';
|
||||
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');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.importHeaderBar.helpers({
|
||||
title() {
|
||||
return `import-board-title-${Session.get('importSource')}`;
|
||||
},
|
||||
}).register('importHeaderBar');
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.error = new ReactiveVar('');
|
||||
this.steps = ['importTextarea', 'importMapMembers'];
|
||||
this._currentStepIndex = new ReactiveVar(0);
|
||||
this.importedData = new ReactiveVar();
|
||||
this.membersToMap = new ReactiveVar([]);
|
||||
this.importSource = Session.get('importSource');
|
||||
},
|
||||
// Helper to find the closest ancestor template instance by name
|
||||
function findParentTemplateInstance(childTemplateInstance, parentTemplateName) {
|
||||
let view = childTemplateInstance.view;
|
||||
while (view) {
|
||||
if (view.name === `Template.${parentTemplateName}` && view.templateInstance) {
|
||||
return view.templateInstance();
|
||||
}
|
||||
view = view.parentView;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
currentTemplate() {
|
||||
return this.steps[this._currentStepIndex.get()];
|
||||
},
|
||||
function _prepareAdditionalData(dataObject) {
|
||||
const importSource = Session.get('importSource');
|
||||
let membersToMap;
|
||||
switch (importSource) {
|
||||
case 'trello':
|
||||
membersToMap = trelloGetMembersToMap(dataObject);
|
||||
break;
|
||||
case 'wekan':
|
||||
membersToMap = wekanGetMembersToMap(dataObject);
|
||||
break;
|
||||
case 'csv':
|
||||
membersToMap = csvGetMembersToMap(dataObject);
|
||||
break;
|
||||
}
|
||||
return membersToMap;
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
Template.import.onCreated(function () {
|
||||
this.error = new ReactiveVar('');
|
||||
this.steps = ['importTextarea', 'importMapMembers'];
|
||||
this._currentStepIndex = new ReactiveVar(0);
|
||||
this.importedData = new ReactiveVar();
|
||||
this.membersToMap = new ReactiveVar([]);
|
||||
this.importSource = Session.get('importSource');
|
||||
|
||||
this.nextStep = () => {
|
||||
const nextStepIndex = this._currentStepIndex.get() + 1;
|
||||
if (nextStepIndex >= this.steps.length) {
|
||||
this.finishImport();
|
||||
} else {
|
||||
this._currentStepIndex.set(nextStepIndex);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
importData(evt, dataSource) {
|
||||
this.setError = (error) => {
|
||||
this.error.set(error);
|
||||
};
|
||||
|
||||
this.importData = (evt, dataSource) => {
|
||||
evt.preventDefault();
|
||||
const input = this.find('.js-import-json').value;
|
||||
if (dataSource === 'csv') {
|
||||
@@ -43,7 +73,7 @@ BlazeComponent.extendComponent({
|
||||
const ret = Papa.parse(csv);
|
||||
if (ret && ret.data && ret.data.length) this.importedData.set(ret.data);
|
||||
else throw new Meteor.Error('error-csv-schema');
|
||||
const membersToMap = this._prepareAdditionalData(ret.data);
|
||||
const membersToMap = _prepareAdditionalData(ret.data);
|
||||
this.membersToMap.set(membersToMap);
|
||||
this.nextStep();
|
||||
} else {
|
||||
@@ -51,7 +81,7 @@ BlazeComponent.extendComponent({
|
||||
const dataObject = JSON.parse(input);
|
||||
this.setError('');
|
||||
this.importedData.set(dataObject);
|
||||
const membersToMap = this._prepareAdditionalData(dataObject);
|
||||
const membersToMap = _prepareAdditionalData(dataObject);
|
||||
// store members data and mapping in Session
|
||||
// (we go deep and 2-way, so storing in data context is not a viable option)
|
||||
this.membersToMap.set(membersToMap);
|
||||
@@ -60,13 +90,9 @@ BlazeComponent.extendComponent({
|
||||
this.setError('error-json-malformed');
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
setError(error) {
|
||||
this.error.set(error);
|
||||
},
|
||||
|
||||
finishImport() {
|
||||
this.finishImport = () => {
|
||||
const additionalData = {};
|
||||
const membersMapping = this.membersToMap.get();
|
||||
if (membersMapping) {
|
||||
@@ -94,44 +120,27 @@ BlazeComponent.extendComponent({
|
||||
FlowRouter.go('board', {
|
||||
id: res,
|
||||
slug: title,
|
||||
})
|
||||
});
|
||||
//Utils.goBoardId(res);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
_prepareAdditionalData(dataObject) {
|
||||
const importSource = Session.get('importSource');
|
||||
let membersToMap;
|
||||
switch (importSource) {
|
||||
case 'trello':
|
||||
membersToMap = trelloGetMembersToMap(dataObject);
|
||||
break;
|
||||
case 'wekan':
|
||||
membersToMap = wekanGetMembersToMap(dataObject);
|
||||
break;
|
||||
case 'csv':
|
||||
membersToMap = csvGetMembersToMap(dataObject);
|
||||
break;
|
||||
}
|
||||
return membersToMap;
|
||||
Template.import.helpers({
|
||||
error() {
|
||||
return Template.instance().error;
|
||||
},
|
||||
|
||||
_screenAdditionalData() {
|
||||
return 'mapMembers';
|
||||
},
|
||||
}).register('import');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'importTextarea';
|
||||
currentTemplate() {
|
||||
return Template.instance().steps[Template.instance()._currentStepIndex.get()];
|
||||
},
|
||||
});
|
||||
|
||||
Template.importTextarea.helpers({
|
||||
instruction() {
|
||||
return `import-board-instruction-${Session.get('importSource')}`;
|
||||
},
|
||||
|
||||
importPlaceHolder() {
|
||||
const importSource = Session.get('importSource');
|
||||
if (importSource === 'csv') {
|
||||
@@ -140,81 +149,37 @@ BlazeComponent.extendComponent({
|
||||
return 'import-json-placeholder';
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
submit(evt) {
|
||||
return this.parentComponent().importData(
|
||||
evt,
|
||||
Session.get('importSource'),
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
Template.importTextarea.events({
|
||||
submit(evt, tpl) {
|
||||
const importTpl = findParentTemplateInstance(tpl, 'import');
|
||||
if (importTpl) {
|
||||
return importTpl.importData(evt, Session.get('importSource'));
|
||||
}
|
||||
},
|
||||
}).register('importTextarea');
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.usersLoaded = new ReactiveVar(false);
|
||||
// Module-level reference so popup children can access importMapMembers methods
|
||||
let _importMapMembersTpl = null;
|
||||
|
||||
this.autorun(() => {
|
||||
const handle = this.subscribe(
|
||||
'user-miniprofile',
|
||||
this.members().map(member => {
|
||||
return member.username;
|
||||
}),
|
||||
);
|
||||
Tracker.nonreactive(() => {
|
||||
Tracker.autorun(() => {
|
||||
if (
|
||||
handle.ready() &&
|
||||
!this.usersLoaded.get() &&
|
||||
this.members().length
|
||||
) {
|
||||
this._refreshMembers(
|
||||
this.members().map(member => {
|
||||
if (!member.wekanId) {
|
||||
let user = ReactiveCache.getUser({ username: member.username });
|
||||
if (!user) {
|
||||
user = ReactiveCache.getUser({ importUsernames: member.username });
|
||||
}
|
||||
if (user) {
|
||||
// eslint-disable-next-line no-console
|
||||
// console.log('found username:', user.username);
|
||||
member.wekanId = user._id;
|
||||
}
|
||||
}
|
||||
return member;
|
||||
}),
|
||||
);
|
||||
}
|
||||
this.usersLoaded.set(handle.ready());
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
Template.importMapMembers.onCreated(function () {
|
||||
_importMapMembersTpl = this;
|
||||
this.usersLoaded = new ReactiveVar(false);
|
||||
|
||||
members() {
|
||||
return this.parentComponent().membersToMap.get();
|
||||
},
|
||||
this.members = () => {
|
||||
const importTpl = findParentTemplateInstance(this, 'import');
|
||||
return importTpl ? importTpl.membersToMap.get() : [];
|
||||
};
|
||||
|
||||
_refreshMembers(listOfMembers) {
|
||||
return this.parentComponent().membersToMap.set(listOfMembers);
|
||||
},
|
||||
this._refreshMembers = (listOfMembers) => {
|
||||
const importTpl = findParentTemplateInstance(this, 'import');
|
||||
if (importTpl) {
|
||||
importTpl.membersToMap.set(listOfMembers);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Will look into the list of members to import for the specified memberId,
|
||||
* then set its property to the supplied value.
|
||||
* If unset is true, it will remove the property from the rest of the list as well.
|
||||
*
|
||||
* use:
|
||||
* - memberId = null to use selected member
|
||||
* - value = null to unset a property
|
||||
* - unset = true to ensure property is only set on 1 member at a time
|
||||
*/
|
||||
_setPropertyForMember(property, value, memberId, unset = false) {
|
||||
this._setPropertyForMember = (property, value, memberId, unset = false) => {
|
||||
const listOfMembers = this.members();
|
||||
let finder = null;
|
||||
if (memberId) {
|
||||
@@ -240,17 +205,13 @@ BlazeComponent.extendComponent({
|
||||
});
|
||||
// Session.get gives us a copy, we have to set it back so it sticks
|
||||
this._refreshMembers(listOfMembers);
|
||||
},
|
||||
};
|
||||
|
||||
setSelectedMember(memberId) {
|
||||
this.setSelectedMember = (memberId) => {
|
||||
return this._setPropertyForMember('selected', true, memberId, true);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* returns the member with specified id,
|
||||
* or the selected member if memberId is not specified
|
||||
*/
|
||||
getMember(memberId = null) {
|
||||
this.getMember = (memberId = null) => {
|
||||
const allMembers = this.members();
|
||||
let finder = null;
|
||||
if (memberId) {
|
||||
@@ -259,117 +220,154 @@ BlazeComponent.extendComponent({
|
||||
finder = user => user.selected;
|
||||
}
|
||||
return allMembers.find(finder);
|
||||
},
|
||||
};
|
||||
|
||||
mapSelectedMember(wekanId) {
|
||||
this.mapSelectedMember = (wekanId) => {
|
||||
return this._setPropertyForMember('wekanId', wekanId, null);
|
||||
},
|
||||
};
|
||||
|
||||
unmapMember(memberId) {
|
||||
this.unmapMember = (memberId) => {
|
||||
return this._setPropertyForMember('wekanId', null, memberId);
|
||||
},
|
||||
};
|
||||
|
||||
onSubmit(evt) {
|
||||
this.autorun(() => {
|
||||
const handle = this.subscribe(
|
||||
'user-miniprofile',
|
||||
this.members().map(member => {
|
||||
return member.username;
|
||||
}),
|
||||
);
|
||||
Tracker.nonreactive(() => {
|
||||
Tracker.autorun(() => {
|
||||
if (
|
||||
handle.ready() &&
|
||||
!this.usersLoaded.get() &&
|
||||
this.members().length
|
||||
) {
|
||||
this._refreshMembers(
|
||||
this.members().map(member => {
|
||||
if (!member.wekanId) {
|
||||
let user = ReactiveCache.getUser({ username: member.username });
|
||||
if (!user) {
|
||||
user = ReactiveCache.getUser({ importUsernames: member.username });
|
||||
}
|
||||
if (user) {
|
||||
member.wekanId = user._id;
|
||||
}
|
||||
}
|
||||
return member;
|
||||
}),
|
||||
);
|
||||
}
|
||||
this.usersLoaded.set(handle.ready());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Template.importMapMembers.onDestroyed(function () {
|
||||
if (_importMapMembersTpl === this) {
|
||||
_importMapMembersTpl = null;
|
||||
}
|
||||
});
|
||||
|
||||
Template.importMapMembers.helpers({
|
||||
usersLoaded() {
|
||||
return Template.instance().usersLoaded;
|
||||
},
|
||||
members() {
|
||||
return Template.instance().members();
|
||||
},
|
||||
});
|
||||
|
||||
Template.importMapMembers.events({
|
||||
submit(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
this.parentComponent().nextStep();
|
||||
const importTpl = findParentTemplateInstance(tpl, 'import');
|
||||
if (importTpl) {
|
||||
importTpl.nextStep();
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
submit: this.onSubmit,
|
||||
'click .js-select-member'(evt) {
|
||||
const memberToMap = this.currentData();
|
||||
if (memberToMap.wekan) {
|
||||
// todo xxx ask for confirmation?
|
||||
this.unmapMember(memberToMap.id);
|
||||
} else {
|
||||
this.setSelectedMember(memberToMap.id);
|
||||
Popup.open('importMapMembersAdd')(evt);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
'click .js-select-member'(evt, tpl) {
|
||||
const memberToMap = Template.currentData();
|
||||
if (memberToMap.wekan) {
|
||||
// todo xxx ask for confirmation?
|
||||
tpl.unmapMember(memberToMap.id);
|
||||
} else {
|
||||
tpl.setSelectedMember(memberToMap.id);
|
||||
Popup.open('importMapMembersAdd')(evt);
|
||||
}
|
||||
},
|
||||
}).register('importMapMembers');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
this.find('.js-map-member input').focus();
|
||||
},
|
||||
|
||||
onSelectUser() {
|
||||
Popup.getOpenerComponent(5).mapSelectedMember(this.currentData().__originalId);
|
||||
Popup.back();
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-select-import': this.onSelectUser,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('importMapMembersAddPopup');
|
||||
});
|
||||
|
||||
// Global reactive variables for import member popup
|
||||
const importMemberPopupState = {
|
||||
searching: new ReactiveVar(false),
|
||||
searchResults: new ReactiveVar([]),
|
||||
noResults: new ReactiveVar(false),
|
||||
searchTimeout: null
|
||||
searchTimeout: null,
|
||||
};
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
// Use global state
|
||||
this.searching = importMemberPopupState.searching;
|
||||
this.searchResults = importMemberPopupState.searchResults;
|
||||
this.noResults = importMemberPopupState.noResults;
|
||||
this.searchTimeout = importMemberPopupState.searchTimeout;
|
||||
},
|
||||
Template.importMapMembersAddPopup.onCreated(function () {
|
||||
this.searching = importMemberPopupState.searching;
|
||||
this.searchResults = importMemberPopupState.searchResults;
|
||||
this.noResults = importMemberPopupState.noResults;
|
||||
this.searchTimeout = null;
|
||||
|
||||
onRendered() {
|
||||
this.find('.js-search-member-input').focus();
|
||||
},
|
||||
this.searching.set(false);
|
||||
this.searchResults.set([]);
|
||||
this.noResults.set(false);
|
||||
});
|
||||
|
||||
performSearch(query) {
|
||||
if (!query || query.length < 2) {
|
||||
this.searchResults.set([]);
|
||||
this.noResults.set(false);
|
||||
return;
|
||||
Template.importMapMembersAddPopup.onRendered(function () {
|
||||
this.find('.js-search-member-input').focus();
|
||||
});
|
||||
|
||||
Template.importMapMembersAddPopup.onDestroyed(function () {
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
this.searching.set(false);
|
||||
});
|
||||
|
||||
function importPerformSearch(tpl, query) {
|
||||
if (!query || query.length < 2) {
|
||||
tpl.searchResults.set([]);
|
||||
tpl.noResults.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
tpl.searching.set(true);
|
||||
tpl.noResults.set(false);
|
||||
|
||||
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
|
||||
tpl.searchResults.set(results);
|
||||
tpl.searching.set(false);
|
||||
|
||||
if (results.length === 0) {
|
||||
tpl.noResults.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
Template.importMapMembersAddPopup.events({
|
||||
'click .js-select-import'(event, tpl) {
|
||||
if (_importMapMembersTpl) {
|
||||
_importMapMembersTpl.mapSelectedMember(Template.currentData().__originalId);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
'keyup .js-search-member-input'(event, tpl) {
|
||||
const query = event.target.value.trim();
|
||||
|
||||
if (tpl.searchTimeout) {
|
||||
clearTimeout(tpl.searchTimeout);
|
||||
}
|
||||
|
||||
this.searching.set(true);
|
||||
this.noResults.set(false);
|
||||
|
||||
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
|
||||
this.searchResults.set(results);
|
||||
this.searching.set(false);
|
||||
|
||||
if (results.length === 0) {
|
||||
this.noResults.set(true);
|
||||
}
|
||||
tpl.searchTimeout = setTimeout(() => {
|
||||
importPerformSearch(tpl, query);
|
||||
}, 300);
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'keyup .js-search-member-input'(event) {
|
||||
const query = event.target.value.trim();
|
||||
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.performSearch(query);
|
||||
}, 300);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('importMapMembersAddPopupSearch');
|
||||
});
|
||||
|
||||
Template.importMapMembersAddPopup.helpers({
|
||||
searchResults() {
|
||||
@@ -380,5 +378,5 @@ Template.importMapMembersAddPopup.helpers({
|
||||
},
|
||||
noResults() {
|
||||
return importMemberPopupState.noResults;
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
template(name="basicTabs")
|
||||
.basicTabs-container(class="{{name}}")
|
||||
ul.tabs-list
|
||||
each tabs
|
||||
li.tab-item(class="{{isActiveTab slug}} {{class}}" data-tab="{{slug}}") {{name}}
|
||||
.tabs-content-container
|
||||
+Template.contentBlock
|
||||
|
||||
template(name="tabContent")
|
||||
section.tabs-content(class="{{isActiveTab slug}}" data-tab="{{slug}}")
|
||||
+Template.contentBlock
|
||||
@@ -0,0 +1,56 @@
|
||||
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,
|
||||
// its parent basicTabs instance is on top of this stack.
|
||||
// This is necessary because Blaze content blocks render with parentView pointing
|
||||
// back to the calling context (e.g. membersWidget), not the basicTabs template —
|
||||
// so walking the view tree cannot find the basicTabs instance.
|
||||
const _creatingStack = [];
|
||||
|
||||
Template.basicTabs.onCreated(function () {
|
||||
const activeTab = this.data.activeTab
|
||||
? { slug: this.data.activeTab }
|
||||
: this.data.tabs[0];
|
||||
this._activeTab = new ReactiveVar(activeTab);
|
||||
|
||||
this.isActiveSlug = (slug) => {
|
||||
const current = this._activeTab.get();
|
||||
return current && current.slug === slug;
|
||||
};
|
||||
|
||||
_creatingStack.push(this);
|
||||
});
|
||||
|
||||
Template.basicTabs.onRendered(function () {
|
||||
const idx = _creatingStack.lastIndexOf(this);
|
||||
if (idx !== -1) _creatingStack.splice(idx, 1);
|
||||
});
|
||||
|
||||
Template.basicTabs.helpers({
|
||||
isActiveTab(slug) {
|
||||
if (Template.instance().isActiveSlug(slug)) {
|
||||
return 'active';
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.basicTabs.events({
|
||||
'click .tab-item'(e, t) {
|
||||
t._activeTab.set(this);
|
||||
},
|
||||
});
|
||||
|
||||
Template.tabContent.onCreated(function () {
|
||||
// Capture the parent basicTabs instance at creation time via the stack.
|
||||
// isActiveSlug reads a ReactiveVar on the basicTabs instance, so the
|
||||
// isActiveTab helper below will re-run reactively when the tab changes.
|
||||
this._basicTabsInst = _creatingStack[_creatingStack.length - 1] || null;
|
||||
});
|
||||
|
||||
Template.tabContent.helpers({
|
||||
isActiveTab(slug) {
|
||||
const inst = Template.instance()._basicTabsInst;
|
||||
if (inst && inst.isActiveSlug(slug)) return 'active';
|
||||
},
|
||||
});
|
||||
+522
-145
@@ -161,29 +161,35 @@ body.list-resizing-active * {
|
||||
/* Use original display for consistent button positioning */
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
/* Prevent vertical expansion but allow normal height */
|
||||
overflow: hidden !important;
|
||||
/* Allow overflow for text wrapping and forms */
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Clearfix for floated buttons */
|
||||
.list-header::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* Ensure title text doesn't cause height changes for all lists */
|
||||
.list-header .list-header-name {
|
||||
/* Prevent text wrapping to maintain consistent height */
|
||||
white-space: nowrap !important;
|
||||
/* Truncate text with ellipsis if too long */
|
||||
text-overflow: ellipsis !important;
|
||||
/* Allow text wrapping to flow below buttons */
|
||||
white-space: normal !important;
|
||||
/* Ensure proper line height */
|
||||
line-height: 1.2 !important;
|
||||
/* Ensure it doesn't overflow */
|
||||
overflow: hidden !important;
|
||||
/* Add margin to prevent overlap with buttons */
|
||||
margin-right: 120px !important;
|
||||
/* Ensure it doesn't overflow horizontally */
|
||||
overflow-wrap: break-word !important;
|
||||
word-wrap: break-word !important;
|
||||
/* Full width since buttons are now absolutely positioned above */
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Position elements from right to left: hamburger, add card, drag handle */
|
||||
/* Position elements at top aligned with collapse button */
|
||||
.list-header .js-open-list-menu {
|
||||
position: absolute !important;
|
||||
top: 2.5vh !important;
|
||||
right: 1.5vw !important;
|
||||
top: 5px !important;
|
||||
right: 10px !important;
|
||||
z-index: 15 !important;
|
||||
display: inline-block !important;
|
||||
padding: 4px !important;
|
||||
@@ -191,8 +197,8 @@ body.list-resizing-active * {
|
||||
|
||||
.list-header .list-header-plus-top {
|
||||
position: absolute !important;
|
||||
top: 2.5vh !important;
|
||||
right: 3.25vw !important;
|
||||
top: 5px !important;
|
||||
right: 30px !important;
|
||||
z-index: 15 !important;
|
||||
display: inline-block !important;
|
||||
padding: 4px !important;
|
||||
@@ -200,8 +206,8 @@ body.list-resizing-active * {
|
||||
|
||||
.list-header .list-header-handle-desktop {
|
||||
position: absolute !important;
|
||||
top: 2.5vh !important;
|
||||
right: 6.5vw !important;
|
||||
top: 5px !important;
|
||||
right: 80px !important;
|
||||
z-index: 15 !important;
|
||||
display: inline-block !important;
|
||||
cursor: move !important;
|
||||
@@ -245,73 +251,147 @@ body.list-resizing-active * {
|
||||
}
|
||||
.list.list-collapsed {
|
||||
flex: none;
|
||||
min-width: 60px;
|
||||
max-width: 80px;
|
||||
width: 60px;
|
||||
min-width: 30px;
|
||||
max-width: 30px;
|
||||
width: 30px;
|
||||
min-height: 60vh;
|
||||
height: 60vh;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
}
|
||||
.list.list-collapsed .list-header {
|
||||
padding: 1vh 1.5vw 0.5vh;
|
||||
min-height: 2.5vh !important;
|
||||
height: auto !important;
|
||||
padding: 5px 0;
|
||||
min-height: 100% !important;
|
||||
height: 100% !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
overflow: visible !important;
|
||||
width: 30px;
|
||||
max-width: 30px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* In collapsed lists, keep controls inside the 30px column.
|
||||
The inlined wrapper is normally display: contents; convert it to a real
|
||||
flex container so control ordering and centering apply reliably. */
|
||||
.list.list-collapsed .list-header > .js-inlined-form-wrapper {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
max-width: 60px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
/* Keep collapsed header controls in a deterministic vertical order:
|
||||
collapse toggle, drag handle, then rotated title. */
|
||||
.list.list-collapsed .list-header > .js-inlined-form-wrapper > .list-header-handle-desktop,
|
||||
.list.list-collapsed .list-header > .js-inlined-form-wrapper > .list-header-handle {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.list.list-collapsed .list-header > .js-inlined-form-wrapper > .js-collapse {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.list.list-collapsed .list-header > .js-inlined-form-wrapper > .list-rotated {
|
||||
order: 3;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
margin: 0 auto 0 auto;
|
||||
z-index: 10;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
position: relative !important;
|
||||
margin: 5px auto;
|
||||
z-index: 30;
|
||||
padding: 5px;
|
||||
font-size: 16px;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
width: fit-content;
|
||||
width: auto;
|
||||
left: auto !important;
|
||||
top: auto !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-header-handle {
|
||||
position: absolute !important;
|
||||
top: 30px !important;
|
||||
right: 1.5vw !important;
|
||||
z-index: 15 !important;
|
||||
position: relative !important;
|
||||
margin: 5px auto;
|
||||
z-index: 31;
|
||||
padding: 5px;
|
||||
display: block;
|
||||
width: auto;
|
||||
top: auto !important;
|
||||
right: auto !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.list.list-collapsed .list-header .list-header-handle-desktop {
|
||||
position: relative !important;
|
||||
margin: 5px auto;
|
||||
z-index: 31;
|
||||
padding: 5px;
|
||||
display: block;
|
||||
width: auto;
|
||||
top: auto !important;
|
||||
right: auto !important;
|
||||
left: auto !important;
|
||||
transform: none !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated {
|
||||
width: auto !important;
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
height: auto !important;
|
||||
margin: 20px 0 0 0 !important;
|
||||
margin: 10px 0 0 0 !important;
|
||||
position: relative !important;
|
||||
overflow: visible !important;
|
||||
overflow: hidden !important;
|
||||
transform: none;
|
||||
transform-origin: center center;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
text-align: left;
|
||||
overflow: visible;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
display: block !important;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
color: #333;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ddd;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
margin: 0;
|
||||
width: 100vh;
|
||||
height: 30px;
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
top: 50%;
|
||||
transform: translateY(calc(-50% + 20px)) rotate(0deg);
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
position: static;
|
||||
left: auto;
|
||||
top: auto;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: mixed;
|
||||
transform: rotate(180deg);
|
||||
z-index: 10;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
pointer-events: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.list.list-composer,
|
||||
.list-composer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show list-composer when inside an active inlined form */
|
||||
form.inlined-form .list-composer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.list.list-composer .open-list-composer,
|
||||
@@ -348,16 +428,17 @@ body.list-resizing-active * {
|
||||
display: none;
|
||||
}
|
||||
.list-header .list-header-name {
|
||||
display: inline;
|
||||
display: block;
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
min-height: 1.2vh;
|
||||
min-width: 4vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
}
|
||||
/* Sum badge shown before list title */
|
||||
.list-header .list-sum-badge {
|
||||
@@ -398,6 +479,8 @@ body.list-resizing-active * {
|
||||
.list-header .list-header-plus-top {
|
||||
color: #a6a6a6;
|
||||
margin-right: 15px;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.list-header .list-header-collapse-right {
|
||||
color: #a6a6a6;
|
||||
@@ -406,41 +489,50 @@ body.list-resizing-active * {
|
||||
color: #a6a6a6;
|
||||
margin-right: 15px;
|
||||
}
|
||||
/* List header collapse button styling */
|
||||
.list-header .list-header-collapse-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* List header collapse button styling - positioned at top left */
|
||||
.list-header .js-collapse {
|
||||
position: absolute !important;
|
||||
top: 5px !important;
|
||||
left: 10px !important;
|
||||
color: #a6a6a6;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
vertical-align: top;
|
||||
padding: 5px 8px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
line-height: 1.2;
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
text-decoration: none;
|
||||
margin: 0;
|
||||
z-index: 15;
|
||||
}
|
||||
.list-header .js-collapse:hover {
|
||||
background-color: transparent;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.list-header .list-header-collapse-container > div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
/* Title text container */
|
||||
.list-header > div {
|
||||
padding-top: 0;
|
||||
width: 100%;
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* Expanded desktop collapse button — static, participates in flex row 1. */
|
||||
.list:not(.list-collapsed):not(.mobile-view) .list-header .js-collapse {
|
||||
position: static !important;
|
||||
top: auto !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
transform: none !important;
|
||||
margin: 0 !important;
|
||||
min-width: 24px;
|
||||
padding: 2px 4px !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
display: inline-block !important;
|
||||
@@ -448,132 +540,164 @@ body.list-resizing-active * {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Hide menu button in collapsed state */
|
||||
.list.list-collapsed .list-header .js-open-list-menu,
|
||||
.list.list-collapsed .list-header .list-header-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for collapsed lists */
|
||||
@media (min-width: 768px) {
|
||||
.list.list-collapsed {
|
||||
min-width: 60px;
|
||||
max-width: 80px;
|
||||
width: 60px;
|
||||
min-width: 30px;
|
||||
max-width: 30px;
|
||||
width: 30px;
|
||||
min-height: 60vh;
|
||||
height: 60vh;
|
||||
}
|
||||
.list.list-collapsed .list-header {
|
||||
max-width: 60px;
|
||||
margin: 0 auto;
|
||||
min-height: 2.5vh !important;
|
||||
height: auto !important;
|
||||
width: 30px;
|
||||
max-width: 30px;
|
||||
margin: 0;
|
||||
min-height: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated {
|
||||
width: auto !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
margin: 20px 0 0 0 !important;
|
||||
margin: 10px 0 0 0 !important;
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
transform: none;
|
||||
flex: 1;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
width: 100vh;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
height: 100%;
|
||||
line-height: 1.2;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
top: 50%;
|
||||
transform: translateY(calc(-50% + 120px)) rotate(0deg);
|
||||
overflow: hidden;
|
||||
position: static;
|
||||
left: auto;
|
||||
top: auto;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: mixed;
|
||||
transform: rotate(180deg);
|
||||
text-align: center;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
display: block !important;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ddd;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: #333;
|
||||
z-index: 10;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
margin: 0 auto 20px auto;
|
||||
margin: 5px auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.list.list-collapsed {
|
||||
min-width: 30px;
|
||||
max-width: 30px;
|
||||
width: 30px;
|
||||
min-height: 60vh;
|
||||
height: 60vh;
|
||||
}
|
||||
.list.list-collapsed .list-header {
|
||||
min-height: 2.5vh !important;
|
||||
height: auto !important;
|
||||
width: 30px;
|
||||
max-width: 30px;
|
||||
min-height: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated {
|
||||
width: auto !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
margin: 20px 0 0 0 !important;
|
||||
margin: 10px 0 0 0 !important;
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
transform: none;
|
||||
flex: 1;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
width: 100vh;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
height: 100%;
|
||||
line-height: 1.2;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
top: 50%;
|
||||
transform: translateY(calc(-50% + 120px)) rotate(0deg);
|
||||
overflow: hidden;
|
||||
position: static;
|
||||
left: auto;
|
||||
top: auto;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: mixed;
|
||||
transform: rotate(180deg);
|
||||
text-align: center;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
display: block !important;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ddd;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: #333;
|
||||
z-index: 10;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
margin: 0 auto 20px auto;
|
||||
margin: 5px auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.list.list-collapsed {
|
||||
min-width: 30px;
|
||||
max-width: 30px;
|
||||
width: 30px;
|
||||
min-height: 60vh;
|
||||
height: 60vh;
|
||||
}
|
||||
.list.list-collapsed .list-header {
|
||||
min-height: 2.5vh !important;
|
||||
height: auto !important;
|
||||
width: 30px;
|
||||
max-width: 30px;
|
||||
min-height: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated {
|
||||
width: auto !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
margin: 20px 0 0 0 !important;
|
||||
margin: 10px 0 0 0 !important;
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
transform: none;
|
||||
flex: 1;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
width: 100vh;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
height: 100%;
|
||||
line-height: 1.2;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
top: 50%;
|
||||
transform: translateY(calc(-50% + 40px)) rotate(0deg);
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
position: static;
|
||||
left: auto;
|
||||
top: auto;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: mixed;
|
||||
transform: rotate(180deg);
|
||||
text-align: center;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
display: block !important;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ddd;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: #333;
|
||||
z-index: 10;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
margin: 0 auto 20px auto;
|
||||
margin: 5px auto;
|
||||
}
|
||||
}
|
||||
.list-header .list-header-collapse {
|
||||
@@ -596,6 +720,8 @@ body.list-resizing-active * {
|
||||
}
|
||||
.js-open-list-menu {
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.list-body {
|
||||
flex: 1 1 auto;
|
||||
@@ -1070,6 +1196,7 @@ body.list-resizing-active * {
|
||||
|
||||
/* 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;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
@@ -1078,9 +1205,9 @@ body.list-resizing-active * {
|
||||
white-space: normal !important;
|
||||
overflow: visible !important;
|
||||
text-overflow: clip !important;
|
||||
display: inline-block !important;
|
||||
/* Reserve space for right-side controls (menu, handle, count) */
|
||||
max-width: calc(100% - 120px) !important;
|
||||
display: block !important;
|
||||
/* Full width since buttons are absolutely positioned */
|
||||
width: 100% !important;
|
||||
/* Break long words to avoid overflow */
|
||||
word-break: break-word !important;
|
||||
}
|
||||
@@ -1099,77 +1226,327 @@ body.list-resizing-active * {
|
||||
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:
|
||||
keep drag handle visible in every collapsed list and preserve rotated title. */
|
||||
.list.list-collapsed:not(.mobile-view) {
|
||||
overflow: visible !important;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.list.list-collapsed:not(.mobile-view) .list-header {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
min-height: 100% !important;
|
||||
height: 100% !important;
|
||||
padding: 5px 0 !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.list.list-collapsed:not(.mobile-view) .list-header > .js-inlined-form-wrapper {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.list.list-collapsed:not(.mobile-view) .list-header > .js-inlined-form-wrapper > .list-header-handle-desktop,
|
||||
.list.list-collapsed:not(.mobile-view) .list-header > .js-inlined-form-wrapper > .list-header-handle {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.list.list-collapsed:not(.mobile-view) .list-header > .js-inlined-form-wrapper > .js-collapse {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.list.list-collapsed:not(.mobile-view) .list-header > .js-inlined-form-wrapper > .list-rotated {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.list.list-collapsed:not(.mobile-view) .list-header .list-header-handle-desktop,
|
||||
.list.list-collapsed:not(.mobile-view) .list-header .list-header-handle {
|
||||
position: relative !important;
|
||||
top: auto !important;
|
||||
right: auto !important;
|
||||
left: auto !important;
|
||||
transform: none !important;
|
||||
display: block !important;
|
||||
margin: 4px auto 2px auto !important;
|
||||
padding: 5px !important;
|
||||
z-index: 500 !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
pointer-events: auto !important;
|
||||
color: #4d4d4d !important;
|
||||
}
|
||||
|
||||
.list.list-collapsed:not(.mobile-view) .list-header .js-collapse {
|
||||
position: relative !important;
|
||||
top: auto !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
display: block !important;
|
||||
margin: 2px auto !important;
|
||||
z-index: 400 !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.list.list-collapsed:not(.mobile-view) .list-header .list-rotated {
|
||||
position: relative !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
margin: 4px 0 0 0 !important;
|
||||
transform: none !important;
|
||||
transform-origin: initial !important;
|
||||
overflow: hidden !important;
|
||||
z-index: 1 !important;
|
||||
pointer-events: none !important;
|
||||
display: block !important;
|
||||
flex: 0 0 auto !important;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.list.list-collapsed:not(.mobile-view) .list-header .list-rotated h2.list-header-name {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
margin: 0 !important;
|
||||
max-height: 100% !important;
|
||||
padding: 2px 0 !important;
|
||||
white-space: normal !important;
|
||||
overflow: hidden !important;
|
||||
transform: none !important;
|
||||
writing-mode: vertical-lr !important;
|
||||
text-orientation: mixed !important;
|
||||
text-align: left !important;
|
||||
visibility: visible !important;
|
||||
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 {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
.list.list-collapsed .collapsed-list-drag-area:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Final expanded desktop header layout:
|
||||
collapse, title, drag handle, add-to-top, menu. */
|
||||
/* Final expanded desktop header layout:
|
||||
Row 1: collapse | drag handle | + add card | menu
|
||||
Row 2: list title (full width) */
|
||||
.list:not(.list-collapsed):not(.mobile-view) .list-header {
|
||||
display: block !important;
|
||||
/* Match collapsed header top anchor for toolbar controls. */
|
||||
padding-top: 5px !important;
|
||||
}
|
||||
|
||||
.list:not(.list-collapsed):not(.mobile-view) .list-header > .js-inlined-form-wrapper {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Collapse button — first item in toolbar row */
|
||||
.list:not(.list-collapsed):not(.mobile-view) .list-header > .js-inlined-form-wrapper > .js-collapse {
|
||||
order: 1;
|
||||
flex: 0 0 auto;
|
||||
position: static !important;
|
||||
top: auto !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
transform: none !important;
|
||||
margin: 2px 2px 0 0 !important;
|
||||
padding: 2px 4px !important;
|
||||
min-width: 24px;
|
||||
width: auto;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 20 !important;
|
||||
}
|
||||
|
||||
/* Title div — second row, full width */
|
||||
.list:not(.list-collapsed):not(.mobile-view) .list-header > .js-inlined-form-wrapper > div:not(.list-header-menu) {
|
||||
order: 10;
|
||||
flex: 1 0 100%;
|
||||
min-width: 0;
|
||||
padding-top: 4px !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Hide standalone duplicate drag handle outside the menu group */
|
||||
.list:not(.list-collapsed):not(.mobile-view) .list-header > .js-inlined-form-wrapper > .list-header-handle-desktop {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Watch icon in toolbar row */
|
||||
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-watch-icon {
|
||||
order: 2;
|
||||
flex: 0 0 auto;
|
||||
padding-left: 0;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* Menu group — right side of toolbar row */
|
||||
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-menu {
|
||||
order: 5;
|
||||
float: none;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
margin-top: 2px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* Reset absolute positioning on all menu-group items */
|
||||
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-menu .list-header-handle-desktop,
|
||||
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-menu .list-header-plus-top,
|
||||
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-menu .js-open-list-menu {
|
||||
position: static !important;
|
||||
top: auto !important;
|
||||
right: auto !important;
|
||||
left: auto !important;
|
||||
transform: none !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 !important;
|
||||
padding: 2px 6px !important;
|
||||
}
|
||||
|
||||
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-menu .list-header-handle-desktop {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-menu .list-header-plus-top {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.list:not(.list-collapsed):not(.mobile-view) .list-header .list-header-menu .js-open-list-menu {
|
||||
order: 3;
|
||||
}
|
||||
.list.list-collapsed .list-header-name-collapsed {
|
||||
writing-mode: vertical-rl;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
position: relative !important;
|
||||
left: -10px !important;
|
||||
color: #333;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: block !important;
|
||||
align-items: initial;
|
||||
justify-content: initial;
|
||||
font-size: 16px !important;
|
||||
box-shadow: none;
|
||||
margin: 5px auto;
|
||||
z-index: 10;
|
||||
}
|
||||
+284
-242
@@ -1,204 +1,217 @@
|
||||
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;
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
// Proxy
|
||||
openForm(options) {
|
||||
this.childComponents('listBody')[0].openForm(options);
|
||||
},
|
||||
Template.list.onCreated(function () {
|
||||
this.newCardFormIsVisible = new ReactiveVar(true);
|
||||
|
||||
onCreated() {
|
||||
this.newCardFormIsVisible = new ReactiveVar(true);
|
||||
},
|
||||
// Proxy - find the listBody child template instance via the DOM
|
||||
this.openForm = (options) => {
|
||||
const listBodyEl = this.find('.list-body');
|
||||
const view = listBodyEl && Blaze.getView(listBodyEl, 'Template.listBody');
|
||||
const listBodyInstance = view?.templateInstance?.();
|
||||
if (listBodyInstance) listBodyInstance.openForm(options);
|
||||
};
|
||||
});
|
||||
|
||||
// The jquery UI sortable library is the best solution I've found so far. I
|
||||
// tried sortable and dragula but they were not powerful enough four our use
|
||||
// case. I also considered writing/forking a drag-and-drop + sortable library
|
||||
// but it's probably too much work.
|
||||
// By calling asking the sortable library to cancel its move on the `stop`
|
||||
// callback, we basically solve all issues related to reactive updates. A
|
||||
// comment below provides further details.
|
||||
onRendered() {
|
||||
const boardComponent = this.parentComponent().parentComponent();
|
||||
// The jquery UI sortable library is the best solution I've found so far. I
|
||||
// tried sortable and dragula but they were not powerful enough four our use
|
||||
// case. I also considered writing/forking a drag-and-drop + sortable library
|
||||
// but it's probably too much work.
|
||||
// By calling asking the sortable library to cancel its move on the `stop`
|
||||
// callback, we basically solve all issues related to reactive updates. A
|
||||
// comment below provides further details.
|
||||
Template.list.onRendered(function () {
|
||||
const boardBodyEl = this.firstNode?.parentElement?.closest?.('.board-body') ||
|
||||
document.querySelector('.board-body');
|
||||
const boardView = boardBodyEl && Blaze.getView(boardBodyEl, 'Template.boardBody');
|
||||
const boardComponent = boardView?.templateInstance?.();
|
||||
|
||||
// Initialize list resize functionality immediately
|
||||
this.initializeListResize();
|
||||
// Initialize list resize functionality immediately
|
||||
this.initializeListResize();
|
||||
|
||||
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
|
||||
const $cards = this.$('.js-minicards');
|
||||
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
|
||||
const $cards = this.$('.js-minicards');
|
||||
|
||||
$cards.sortable({
|
||||
connectWith: '.js-minicards:not(.js-list-full)',
|
||||
tolerance: 'pointer',
|
||||
appendTo: '.board-canvas',
|
||||
helper(evt, item) {
|
||||
const helper = item.clone();
|
||||
if (MultiSelection.isActive()) {
|
||||
const andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
|
||||
if (andNOthers > 0) {
|
||||
helper.append(
|
||||
$(
|
||||
Blaze.toHTML(
|
||||
HTML.DIV(
|
||||
{ class: 'and-n-other' },
|
||||
TAPi18n.__('and-n-other-card', { count: andNOthers }),
|
||||
),
|
||||
$cards.sortable({
|
||||
connectWith: '.js-minicards:not(.js-list-full)',
|
||||
tolerance: 'pointer',
|
||||
appendTo: '.board-canvas',
|
||||
helper(evt, item) {
|
||||
const helper = item.clone();
|
||||
if (MultiSelection.isActive()) {
|
||||
const andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
|
||||
if (andNOthers > 0) {
|
||||
helper.append(
|
||||
$(
|
||||
Blaze.toHTML(
|
||||
HTML.DIV(
|
||||
{ class: 'and-n-other' },
|
||||
TAPi18n.__('and-n-other-card', { count: andNOthers }),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
return helper;
|
||||
},
|
||||
distance: 7,
|
||||
items: itemsSelector,
|
||||
placeholder: 'minicard-wrapper placeholder',
|
||||
scrollSpeed: 10,
|
||||
start(evt, ui) {
|
||||
ui.helper.css('z-index', 1000);
|
||||
ui.placeholder.height(ui.helper.height());
|
||||
EscapeActions.executeUpTo('popup-close');
|
||||
boardComponent.setIsDragging(true);
|
||||
},
|
||||
stop(evt, ui) {
|
||||
// To attribute the new index number, we need to get the DOM element
|
||||
// of the previous and the following card -- if any.
|
||||
const prevCardDom = ui.item.prev('.js-minicard').get(0);
|
||||
const nextCardDom = ui.item.next('.js-minicard').get(0);
|
||||
const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
|
||||
const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
|
||||
const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const defaultSwimlaneId = currentBoard.getDefaultSwimline()._id;
|
||||
let targetSwimlaneId = null;
|
||||
}
|
||||
return helper;
|
||||
},
|
||||
distance: 7,
|
||||
items: itemsSelector,
|
||||
placeholder: 'minicard-wrapper placeholder',
|
||||
scrollSpeed: 10,
|
||||
start(evt, ui) {
|
||||
ui.helper.css('z-index', 1000);
|
||||
ui.placeholder.height(ui.helper.height());
|
||||
EscapeActions.executeUpTo('popup-close');
|
||||
if (boardComponent) boardComponent.setIsDragging(true);
|
||||
},
|
||||
stop(evt, ui) {
|
||||
// To attribute the new index number, we need to get the DOM element
|
||||
// of the previous and the following card -- if any.
|
||||
const prevCardDom = ui.item.prev('.js-minicard').get(0);
|
||||
const nextCardDom = ui.item.next('.js-minicard').get(0);
|
||||
const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
|
||||
const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
|
||||
const listData = Blaze.getData(ui.item.parents('.list').get(0));
|
||||
const listId = listData._id;
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const defaultSwimlaneId = currentBoard.getDefaultSwimline()._id;
|
||||
let targetSwimlaneId = null;
|
||||
|
||||
// only set a new swimelane ID if the swimlanes view is active
|
||||
if (
|
||||
Utils.boardView() === 'board-view-swimlanes' ||
|
||||
currentBoard.isTemplatesBoard()
|
||||
)
|
||||
targetSwimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))
|
||||
._id;
|
||||
// only set a new swimelane ID if the swimlanes view is active
|
||||
if (
|
||||
Utils.boardView() === 'board-view-swimlanes' ||
|
||||
currentBoard.isTemplatesBoard()
|
||||
) {
|
||||
targetSwimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))
|
||||
._id;
|
||||
} else if (listData.swimlaneId) {
|
||||
targetSwimlaneId = listData.swimlaneId;
|
||||
}
|
||||
|
||||
// Normally the jquery-ui sortable library moves the dragged DOM element
|
||||
// to its new position, which disrupts Blaze reactive updates mechanism
|
||||
// (especially when we move the last card of a list, or when multiple
|
||||
// users move some cards at the same time). To prevent these UX glitches
|
||||
// we ask sortable to gracefully cancel the move, and to put back the
|
||||
// DOM in its initial state. The card move is then handled reactively by
|
||||
// Blaze with the below query.
|
||||
$cards.sortable('cancel');
|
||||
// Normally the jquery-ui sortable library moves the dragged DOM element
|
||||
// to its new position, which disrupts Blaze reactive updates mechanism
|
||||
// (especially when we move the last card of a list, or when multiple
|
||||
// users move some cards at the same time). To prevent these UX glitches
|
||||
// we ask sortable to gracefully cancel the move, and to put back the
|
||||
// DOM in its initial state. The card move is then handled reactively by
|
||||
// Blaze with the below query.
|
||||
$cards.sortable('cancel');
|
||||
|
||||
if (MultiSelection.isActive()) {
|
||||
ReactiveCache.getCards(MultiSelection.getMongoSelector(), { sort: ['sort'] }).forEach((card, i) => {
|
||||
const newSwimlaneId = targetSwimlaneId
|
||||
? targetSwimlaneId
|
||||
: card.swimlaneId || defaultSwimlaneId;
|
||||
card.move(
|
||||
currentBoard._id,
|
||||
newSwimlaneId,
|
||||
listId,
|
||||
sortIndex.base + i * sortIndex.increment,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const cardDomElement = ui.item.get(0);
|
||||
const card = Blaze.getData(cardDomElement);
|
||||
if (MultiSelection.isActive()) {
|
||||
ReactiveCache.getCards(MultiSelection.getMongoSelector(), { sort: ['sort'] }).forEach((card, i) => {
|
||||
const newSwimlaneId = targetSwimlaneId
|
||||
? targetSwimlaneId
|
||||
: card.swimlaneId || defaultSwimlaneId;
|
||||
card.move(currentBoard._id, newSwimlaneId, listId, sortIndex.base);
|
||||
}
|
||||
boardComponent.setIsDragging(false);
|
||||
},
|
||||
sort(event, ui) {
|
||||
const $boardCanvas = $('.board-canvas');
|
||||
const boardCanvas = $boardCanvas[0];
|
||||
|
||||
if (event.pageX < 10) { // scroll to the left
|
||||
boardCanvas.scrollLeft -= 15;
|
||||
ui.helper[0].offsetLeft -= 15;
|
||||
}
|
||||
if (
|
||||
event.pageX > boardCanvas.offsetWidth - 10 &&
|
||||
boardCanvas.scrollLeft < $boardCanvas.data('scrollLeftMax') // don't scroll more than possible
|
||||
) { // scroll to the right
|
||||
boardCanvas.scrollLeft += 15;
|
||||
}
|
||||
if (
|
||||
event.pageY > boardCanvas.offsetHeight - 10 &&
|
||||
event.pageY + boardCanvas.scrollTop < $boardCanvas.data('scrollTopMax') // don't scroll more than possible
|
||||
) { // scroll to the bottom
|
||||
boardCanvas.scrollTop += 15;
|
||||
}
|
||||
if (event.pageY < 10) { // scroll to the top
|
||||
boardCanvas.scrollTop -= 15;
|
||||
}
|
||||
},
|
||||
activate(event, ui) {
|
||||
const $boardCanvas = $('.board-canvas');
|
||||
const boardCanvas = $boardCanvas[0];
|
||||
// scrollTopMax and scrollLeftMax only available at Firefox (https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTopMax)
|
||||
// https://www.it-swarm.com.de/de/javascript/so-erhalten-sie-den-maximalen-dokument-scrolltop-wert/1069126844/
|
||||
$boardCanvas.data('scrollTopMax', boardCanvas.scrollHeight - boardCanvas.clientTop);
|
||||
// https://stackoverflow.com/questions/5138373/how-do-i-get-the-max-value-of-scrollleft/5704386#5704386
|
||||
$boardCanvas.data('scrollLeftMax', boardCanvas.scrollWidth - boardCanvas.clientWidth);
|
||||
},
|
||||
});
|
||||
|
||||
this.autorun(() => {
|
||||
if ($cards.data('uiSortable') || $cards.data('sortable')) {
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$cards.sortable('option', 'handle', '.handle');
|
||||
} else {
|
||||
$cards.sortable('option', 'handle', '.minicard');
|
||||
}
|
||||
|
||||
$cards.sortable(
|
||||
'option',
|
||||
'disabled',
|
||||
// Disable drag-dropping when user is not member
|
||||
!Utils.canModifyBoard(),
|
||||
// Not disable drag-dropping while in multi-selection mode
|
||||
// MultiSelection.isActive() || !Utils.canModifyBoard(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// We want to re-run this function any time a card is added.
|
||||
this.autorun(() => {
|
||||
const currentBoardId = Tracker.nonreactive(() => {
|
||||
return Session.get('currentBoard');
|
||||
});
|
||||
Tracker.afterFlush(() => {
|
||||
$cards.find(itemsSelector).droppable({
|
||||
hoverClass: 'draggable-hover-card',
|
||||
accept: '.js-member,.js-label',
|
||||
drop(event, ui) {
|
||||
const cardId = Blaze.getData(this)._id;
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
|
||||
if (ui.draggable.hasClass('js-member')) {
|
||||
const memberId = Blaze.getData(ui.draggable.get(0)).userId;
|
||||
card.assignMember(memberId);
|
||||
} else {
|
||||
const labelId = Blaze.getData(ui.draggable.get(0))._id;
|
||||
card.addLabel(labelId);
|
||||
}
|
||||
},
|
||||
card.move(
|
||||
currentBoard._id,
|
||||
newSwimlaneId,
|
||||
listId,
|
||||
sortIndex.base + i * sortIndex.increment,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const cardDomElement = ui.item.get(0);
|
||||
const card = Blaze.getData(cardDomElement);
|
||||
const newSwimlaneId = targetSwimlaneId
|
||||
? targetSwimlaneId
|
||||
: card.swimlaneId || defaultSwimlaneId;
|
||||
card.move(currentBoard._id, newSwimlaneId, listId, sortIndex.base);
|
||||
}
|
||||
if (boardComponent) boardComponent.setIsDragging(false);
|
||||
},
|
||||
sort(event, ui) {
|
||||
const $boardCanvas = $('.board-canvas');
|
||||
const boardCanvas = $boardCanvas[0];
|
||||
|
||||
if (event.pageX < 10) { // scroll to the left
|
||||
boardCanvas.scrollLeft -= 15;
|
||||
ui.helper[0].offsetLeft -= 15;
|
||||
}
|
||||
if (
|
||||
event.pageX > boardCanvas.offsetWidth - 10 &&
|
||||
boardCanvas.scrollLeft < $boardCanvas.data('scrollLeftMax') // don't scroll more than possible
|
||||
) { // scroll to the right
|
||||
boardCanvas.scrollLeft += 15;
|
||||
}
|
||||
if (
|
||||
event.pageY > boardCanvas.offsetHeight - 10 &&
|
||||
event.pageY + boardCanvas.scrollTop < $boardCanvas.data('scrollTopMax') // don't scroll more than possible
|
||||
) { // scroll to the bottom
|
||||
boardCanvas.scrollTop += 15;
|
||||
}
|
||||
if (event.pageY < 10) { // scroll to the top
|
||||
boardCanvas.scrollTop -= 15;
|
||||
}
|
||||
},
|
||||
activate(event, ui) {
|
||||
const $boardCanvas = $('.board-canvas');
|
||||
const boardCanvas = $boardCanvas[0];
|
||||
// scrollTopMax and scrollLeftMax only available at Firefox (https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTopMax)
|
||||
// https://www.it-swarm.com.de/de/javascript/so-erhalten-sie-den-maximalen-dokument-scrolltop-wert/1069126844/
|
||||
$boardCanvas.data('scrollTopMax', boardCanvas.scrollHeight - boardCanvas.clientTop);
|
||||
// https://stackoverflow.com/questions/5138373/how-do-i-get-the-max-value-of-scrollleft/5704386#5704386
|
||||
$boardCanvas.data('scrollLeftMax', boardCanvas.scrollWidth - boardCanvas.clientWidth);
|
||||
},
|
||||
});
|
||||
|
||||
this.autorun(() => {
|
||||
if ($cards.data('uiSortable') || $cards.data('sortable')) {
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$cards.sortable('option', 'handle', '.handle');
|
||||
} else {
|
||||
$cards.sortable('option', 'handle', '.minicard');
|
||||
}
|
||||
|
||||
$cards.sortable(
|
||||
'option',
|
||||
'disabled',
|
||||
// Disable drag-dropping when user is not member
|
||||
!Utils.canModifyBoard(),
|
||||
// Not disable drag-dropping while in multi-selection mode
|
||||
// MultiSelection.isActive() || !Utils.canModifyBoard(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// We want to re-run this function any time a card is added.
|
||||
this.autorun(() => {
|
||||
const currentBoardId = Tracker.nonreactive(() => {
|
||||
return Session.get('currentBoard');
|
||||
});
|
||||
Tracker.afterFlush(() => {
|
||||
$cards.find(itemsSelector).droppable({
|
||||
hoverClass: 'draggable-hover-card',
|
||||
accept: '.js-member,.js-label',
|
||||
drop(event, ui) {
|
||||
const cardId = Blaze.getData(this)._id;
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
|
||||
if (ui.draggable.hasClass('js-member')) {
|
||||
const memberId = Blaze.getData(ui.draggable.get(0)).userId;
|
||||
card.assignMember(memberId);
|
||||
} else {
|
||||
const labelId = Blaze.getData(ui.draggable.get(0))._id;
|
||||
card.addLabel(labelId);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Template.list.helpers({
|
||||
listWidth() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const list = Template.currentData();
|
||||
if (!list) return 270; // Return default width if list is not available
|
||||
|
||||
|
||||
if (user) {
|
||||
// For logged-in users, get from user profile
|
||||
return user.getListWidthFromStorage(list.boardId, list._id);
|
||||
@@ -223,7 +236,7 @@ BlazeComponent.extendComponent({
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const list = Template.currentData();
|
||||
if (!list) return 550; // Return default constraint if list is not available
|
||||
|
||||
|
||||
if (user) {
|
||||
// For logged-in users, get from user profile
|
||||
return user.getListConstraintFromStorage(list.boardId, list._id);
|
||||
@@ -254,59 +267,96 @@ BlazeComponent.extendComponent({
|
||||
return user.isAutoWidth(list.boardId);
|
||||
},
|
||||
|
||||
initializeListResize() {
|
||||
// Check if we're still in a valid template context
|
||||
if (!Template.currentData()) {
|
||||
console.warn('No current template data available for list resize initialization');
|
||||
collapsed() {
|
||||
return Utils.getListCollapseState(this);
|
||||
},
|
||||
});
|
||||
|
||||
// initializeListResize as a method on the template instance
|
||||
Template.list.onCreated(function () {
|
||||
const tpl = this;
|
||||
|
||||
tpl.initializeListResize = function () {
|
||||
// Resolve list data from the template instance to avoid relying on
|
||||
// Template.currentData() from async callbacks where no current view exists.
|
||||
const list = tpl.data || Blaze.getData(tpl.firstNode);
|
||||
if (!list) {
|
||||
console.warn('No list data available for list resize initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
const list = Template.currentData();
|
||||
const $list = this.$('.js-list');
|
||||
const $resizeHandle = this.$('.js-list-resize-handle');
|
||||
|
||||
const $list = tpl.$('.js-list');
|
||||
const $resizeHandle = tpl.$('.js-list-resize-handle');
|
||||
|
||||
const isCollapsed = Utils.getListCollapseState(list);
|
||||
if (isCollapsed) {
|
||||
// Collapsed lists do not render a resize handle by design.
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if elements exist
|
||||
if (!$list.length || !$resizeHandle.length) {
|
||||
console.warn('List or resize handle not found, retrying in 100ms');
|
||||
Meteor.setTimeout(() => {
|
||||
if (!this.isDestroyed) {
|
||||
this.initializeListResize();
|
||||
if (!tpl.view.isDestroyed) {
|
||||
tpl.initializeListResize();
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Only enable resize for non-collapsed, non-auto-width lists
|
||||
const isAutoWidth = this.autoWidth();
|
||||
const isCollapsed = Utils.getListCollapseState(list);
|
||||
if (isCollapsed || isAutoWidth) {
|
||||
$resizeHandle.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper to get autoWidth state
|
||||
const getAutoWidth = () => {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (!user) return false;
|
||||
return user.isAutoWidth(list.boardId);
|
||||
};
|
||||
|
||||
// Reactively show/hide resize handle based on collapse and auto-width state
|
||||
tpl.autorun(() => {
|
||||
const isAutoWidth = getAutoWidth();
|
||||
const isCollapsed = Utils.getListCollapseState(list);
|
||||
if (isCollapsed || isAutoWidth) {
|
||||
$resizeHandle.hide();
|
||||
} else {
|
||||
$resizeHandle.show();
|
||||
}
|
||||
});
|
||||
|
||||
let isResizing = false;
|
||||
let startX = 0;
|
||||
let startWidth = 0;
|
||||
let minWidth = 100; // Minimum width as defined in the existing code
|
||||
let maxWidth = this.listConstraint() || 1000; // Use constraint as max width
|
||||
let listConstraint = this.listConstraint(); // Store constraint value for use in event handlers
|
||||
const component = this; // Store reference to component for use in event handlers
|
||||
let minWidth = 270; // Minimum width matching system default
|
||||
|
||||
// Get listConstraint value
|
||||
const getListConstraint = () => {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user) {
|
||||
return user.getListConstraintFromStorage(list.boardId, list._id);
|
||||
}
|
||||
try {
|
||||
const stored = localStorage.getItem('wekan-list-constraints');
|
||||
if (stored) {
|
||||
const constraints = JSON.parse(stored);
|
||||
if (constraints[list.boardId] && constraints[list.boardId][list._id]) {
|
||||
return constraints[list.boardId][list._id];
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return 550;
|
||||
};
|
||||
|
||||
const startResize = (e) => {
|
||||
isResizing = true;
|
||||
startX = e.pageX || e.originalEvent.touches[0].pageX;
|
||||
startWidth = $list.outerWidth();
|
||||
|
||||
|
||||
|
||||
// Add visual feedback
|
||||
$list.addClass('list-resizing');
|
||||
$('body').addClass('list-resizing-active');
|
||||
|
||||
|
||||
|
||||
// Prevent text selection during resize
|
||||
$('body').css('user-select', 'none');
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
@@ -315,11 +365,11 @@ BlazeComponent.extendComponent({
|
||||
if (!isResizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
|
||||
const deltaX = currentX - startX;
|
||||
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
|
||||
|
||||
const newWidth = Math.max(minWidth, startWidth + deltaX);
|
||||
|
||||
// Apply the new width immediately for real-time feedback
|
||||
$list[0].style.setProperty('--list-width', `${newWidth}px`);
|
||||
$list[0].style.setProperty('width', `${newWidth}px`);
|
||||
@@ -329,22 +379,22 @@ BlazeComponent.extendComponent({
|
||||
$list[0].style.setProperty('flex-basis', 'auto');
|
||||
$list[0].style.setProperty('flex-grow', '0');
|
||||
$list[0].style.setProperty('flex-shrink', '0');
|
||||
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const stopResize = (e) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
|
||||
isResizing = false;
|
||||
|
||||
|
||||
// Calculate final width
|
||||
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
|
||||
const deltaX = currentX - startX;
|
||||
const finalWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
|
||||
|
||||
const finalWidth = Math.max(minWidth, startWidth + deltaX);
|
||||
const listConstraint = getListConstraint();
|
||||
|
||||
// Ensure the final width is applied
|
||||
$list[0].style.setProperty('--list-width', `${finalWidth}px`);
|
||||
$list[0].style.setProperty('width', `${finalWidth}px`);
|
||||
@@ -354,23 +404,19 @@ BlazeComponent.extendComponent({
|
||||
$list[0].style.setProperty('flex-basis', 'auto');
|
||||
$list[0].style.setProperty('flex-grow', '0');
|
||||
$list[0].style.setProperty('flex-shrink', '0');
|
||||
|
||||
|
||||
// Remove visual feedback but keep the width
|
||||
$list.removeClass('list-resizing');
|
||||
$('body').removeClass('list-resizing-active');
|
||||
$('body').css('user-select', '');
|
||||
|
||||
// Keep the CSS custom property for persistent width
|
||||
// The CSS custom property will remain on the element to maintain the width
|
||||
|
||||
|
||||
// Save the new width using the existing system
|
||||
const boardId = list.boardId;
|
||||
const listId = list._id;
|
||||
|
||||
// Use the new storage method that handles both logged-in and non-logged-in users
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
}
|
||||
|
||||
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (currentUser) {
|
||||
// For logged-in users, use server method
|
||||
@@ -388,32 +434,32 @@ BlazeComponent.extendComponent({
|
||||
// Save list width
|
||||
const storedWidths = localStorage.getItem('wekan-list-widths');
|
||||
let widths = storedWidths ? JSON.parse(storedWidths) : {};
|
||||
|
||||
|
||||
if (!widths[boardId]) {
|
||||
widths[boardId] = {};
|
||||
}
|
||||
widths[boardId][listId] = finalWidth;
|
||||
|
||||
|
||||
localStorage.setItem('wekan-list-widths', JSON.stringify(widths));
|
||||
|
||||
|
||||
// Save list constraint
|
||||
const storedConstraints = localStorage.getItem('wekan-list-constraints');
|
||||
let constraints = storedConstraints ? JSON.parse(storedConstraints) : {};
|
||||
|
||||
|
||||
if (!constraints[boardId]) {
|
||||
constraints[boardId] = {};
|
||||
}
|
||||
constraints[boardId][listId] = listConstraint;
|
||||
|
||||
|
||||
localStorage.setItem('wekan-list-constraints', JSON.stringify(constraints));
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error saving list width/constraint to localStorage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
@@ -421,23 +467,21 @@ BlazeComponent.extendComponent({
|
||||
$resizeHandle.on('mousedown', startResize);
|
||||
$(document).on('mousemove', doResize);
|
||||
$(document).on('mouseup', stopResize);
|
||||
|
||||
|
||||
// Touch events for mobile
|
||||
$resizeHandle.on('touchstart', startResize, { passive: false });
|
||||
$(document).on('touchmove', doResize, { passive: false });
|
||||
$(document).on('touchend', stopResize, { passive: false });
|
||||
|
||||
|
||||
|
||||
// Prevent dragscroll interference
|
||||
$resizeHandle.on('mousedown', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Reactively update resize handle visibility when auto-width or collapse changes
|
||||
component.autorun(() => {
|
||||
tpl.autorun(() => {
|
||||
const collapsed = Utils.getListCollapseState(list);
|
||||
if (component.autoWidth() || collapsed) {
|
||||
if (getAutoWidth() || collapsed) {
|
||||
$resizeHandle.hide();
|
||||
} else {
|
||||
$resizeHandle.show();
|
||||
@@ -445,19 +489,13 @@ BlazeComponent.extendComponent({
|
||||
});
|
||||
|
||||
// Clean up on component destruction
|
||||
component.onDestroyed(() => {
|
||||
tpl.view.onViewDestroyed(() => {
|
||||
$(document).off('mousemove', doResize);
|
||||
$(document).off('mouseup', stopResize);
|
||||
$(document).off('touchmove', doResize);
|
||||
$(document).off('touchend', stopResize);
|
||||
});
|
||||
},
|
||||
}).register('list');
|
||||
|
||||
Template.list.helpers({
|
||||
collapsed() {
|
||||
return Utils.getListCollapseState(this);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Template.miniList.events({
|
||||
@@ -466,3 +504,7 @@ Template.miniList.events({
|
||||
Session.set('currentList', listId);
|
||||
},
|
||||
});
|
||||
|
||||
// NOTE: Collapsed list drag-reorder was previously here but referenced
|
||||
// boardComponent from an outer scope. If needed, this should be moved
|
||||
// into Template.list.onRendered where boardComponent is available.
|
||||
@@ -2,9 +2,8 @@ template(name="listBody")
|
||||
unless collapsed
|
||||
.list-body(class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}")
|
||||
.minicards.clearfix.js-minicards(class="{{#if reachedWipLimit}}js-list-full{{/if}}")
|
||||
if cards.length
|
||||
+inlinedForm(autoclose=false position="top")
|
||||
+addCardForm(listId=_id position="top")
|
||||
+inlinedForm(autoclose=false position="top")
|
||||
+addCardForm(listId=_id position="top")
|
||||
ul.sidebar-list
|
||||
each customFieldsSum
|
||||
li
|
||||
@@ -26,13 +25,15 @@ template(name="listBody")
|
||||
+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(
|
||||
@@ -54,7 +55,8 @@ template(name="addCardForm")
|
||||
|
||||
.add-controls.clearfix
|
||||
button.primary.confirm(type="submit") {{_ 'add'}}
|
||||
a.js-close-inlined-form | ❌
|
||||
a.js-close-inlined-form
|
||||
i.fa.fa-times-thin
|
||||
.add-controls.clearfix
|
||||
unless currentBoard.isTemplatesBoard
|
||||
unless currentBoard.isTemplateBoard
|
||||
|
||||
+756
-672
File diff suppressed because it is too large.
Load diff
@@ -8,7 +8,7 @@ template(name="listHeader")
|
||||
if isMiniScreen
|
||||
if currentList
|
||||
a.list-header-left-icon.js-unselect-list
|
||||
| ◀️
|
||||
i.fa.fa-caret-left
|
||||
else
|
||||
if collapsed
|
||||
if showCardsCountForList cards.length
|
||||
@@ -16,7 +16,7 @@ template(name="listHeader")
|
||||
span.cardCount {{cardsCount}}
|
||||
if isMiniScreen
|
||||
h2.list-header-name(
|
||||
title="{{ moment modifiedAt 'LLL' }}"
|
||||
title="{{ displayDate modifiedAt 'LLL' }}"
|
||||
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
|
||||
+viewer
|
||||
= title
|
||||
@@ -30,22 +30,21 @@ template(name="listHeader")
|
||||
|
|
||||
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
|
||||
else
|
||||
div.list-header-collapse-container
|
||||
a.list-collapse-indicator.js-collapse(title="{{_ 'collapse'}}")
|
||||
if collapsed
|
||||
| ▶
|
||||
else
|
||||
| 🔽
|
||||
div(class="{{#if collapsed}}list-rotated{{/if}}")
|
||||
h2.list-header-name(
|
||||
title="{{ moment modifiedAt 'LLL' }}"
|
||||
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
|
||||
+viewer
|
||||
= title
|
||||
if wipLimit.enabled
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
|/#{wipLimit.value})
|
||||
a.list-collapse-indicator.js-collapse(title="{{#if collapsed}}{{_ 'uncollapse'}}{{else}}{{_ 'collapse'}}{{/if}}" aria-label="{{#if collapsed}}{{_ 'uncollapse'}}{{else}}{{_ 'collapse'}}{{/if}}")
|
||||
if collapsed
|
||||
i.fa.fa-caret-right
|
||||
else
|
||||
i.fa.fa-caret-down
|
||||
div(class="{{#if collapsed}}list-rotated{{/if}}")
|
||||
h2.list-header-name(
|
||||
title="{{ displayDate modifiedAt 'LLL' }}"
|
||||
class="{{#unless collapsed}}{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}{{/unless}}")
|
||||
+viewer
|
||||
= title
|
||||
if wipLimit.enabled
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
|/#{wipLimit.value})
|
||||
unless collapsed
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
|
||||
@@ -55,34 +54,48 @@ template(name="listHeader")
|
||||
if isMiniScreen
|
||||
if currentList
|
||||
if isWatching
|
||||
i.list-header-watch-icon | 👁️
|
||||
i.list-header-watch-icon i.fa.fa-eye
|
||||
div.list-header-menu
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
if canSeeAddCard
|
||||
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") ➕
|
||||
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
|
||||
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
|
||||
i.fa.fa-plus
|
||||
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
else
|
||||
a.list-header-menu-icon.js-select-list ▶️
|
||||
a.list-header-menu-icon.js-select-list
|
||||
i.fa.fa-caret-right
|
||||
unless currentUser.isWorker
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
a.list-header-handle.handle.js-list-handle ↕️
|
||||
a.list-header-handle.handle.js-list-handle
|
||||
i.fa.fa-arrows
|
||||
else if currentUser.isBoardMember
|
||||
if isWatching
|
||||
i.list-header-watch-icon | 👁️
|
||||
i.list-header-watch-icon i.fa.fa-eye
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
a.list-header-handle-desktop.handle.js-list-handle(title="{{_ 'drag-list'}}")
|
||||
i.fa.fa-arrows
|
||||
unless collapsed
|
||||
div.list-header-menu
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
//if isBoardAdmin
|
||||
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
|
||||
//
|
||||
a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
a.list-header-handle-desktop.handle.js-list-handle(title="{{_ 'drag-list'}}") ↕️
|
||||
a.list-header-handle-desktop.handle.js-list-handle(title="{{_ 'drag-list'}}")
|
||||
i.fa.fa-arrows
|
||||
if canSeeAddCard
|
||||
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") ➕
|
||||
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
|
||||
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
|
||||
i.fa.fa-plus
|
||||
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
|
||||
template(name="editListTitleForm")
|
||||
.list-composer
|
||||
@@ -90,31 +103,42 @@ template(name="editListTitleForm")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm(type="submit") {{_ 'save'}}
|
||||
a.js-close-inlined-form
|
||||
| ❌
|
||||
i.fa.fa-times-thin
|
||||
|
||||
template(name="listActionPopup")
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-add-card.list-header-plus-top
|
||||
i.fa.fa-plus
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'add-card-to-top-of-list'}}
|
||||
li
|
||||
a.js-add-card.list-header-plus-bottom
|
||||
| ➕
|
||||
| ⬇️
|
||||
i.fa.fa-plus
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'add-card-to-bottom-of-list'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-add-list
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-list'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-set-list-width
|
||||
| ↔️
|
||||
i.fa.fa-arrows-h
|
||||
| {{_ 'set-list-width'}}
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-toggle-watch-list
|
||||
if isWatching
|
||||
| 👁️
|
||||
i.fa.fa-eye
|
||||
| {{_ 'unwatch'}}
|
||||
else
|
||||
| 🙈
|
||||
i.fa.fa-eye-slash
|
||||
| {{_ 'watch'}}
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isReadOnly
|
||||
@@ -123,33 +147,45 @@ template(name="listActionPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-set-color-list
|
||||
| 🎨
|
||||
i.fa.fa-paint-brush
|
||||
| {{_ 'set-color-list'}}
|
||||
ul.pop-over-list
|
||||
if cards.length
|
||||
li
|
||||
a.js-select-cards
|
||||
| ☑️
|
||||
i.fa.fa-select-square
|
||||
| {{_ 'list-select-cards'}}
|
||||
if currentUser.isBoardAdmin
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-set-wip-limit
|
||||
| 🚫
|
||||
i.fa.fa-ban
|
||||
| {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
unless currentUser.isWorker
|
||||
li
|
||||
a.js-copy-list
|
||||
i.fa.fa-clipboard
|
||||
| {{_ 'copyListPopup-title'}}
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-move-list
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'moveListPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-close-list
|
||||
| ➡️
|
||||
| 📦
|
||||
i.fa.fa-arrow-right
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archive-list'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
| 🔗
|
||||
i.fa.fa-link
|
||||
| {{_ 'listMorePopup-title'}}
|
||||
|
||||
template(name="boardLists")
|
||||
@@ -166,13 +202,20 @@ template(name="listMorePopup")
|
||||
span.clearfix
|
||||
span {{_ 'link-list'}}
|
||||
= ' '
|
||||
| {{#if board.isPublic}}🌐{{else}}🔒{{/if}}
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
input.inline-input(type="text" readonly value="{{ rootUrl }}")
|
||||
| {{_ 'added'}}
|
||||
span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
|
||||
span.date(title=list.createdAt) {{ displayDate createdAt 'LLL' }}
|
||||
//unless currentUser.isWorker
|
||||
// if currentUser.isBoardAdmin
|
||||
// a.js-delete {{_ 'delete'}}
|
||||
//
|
||||
if currentUser.isBoardAdmin
|
||||
//
|
||||
a.js-delete {{_ 'delete'}}
|
||||
|
||||
template(name="listArchivePopup")
|
||||
p {{_ "list-archive-pop"}}
|
||||
p {{_ "list-archive-suggest"}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
|
||||
|
||||
template(name="listDeletePopup")
|
||||
p {{_ "list-delete-pop"}}
|
||||
@@ -186,7 +229,7 @@ template(name="setWipLimitPopup")
|
||||
ul.pop-over-list
|
||||
li: a.js-enable-wip-limit {{_ 'enable-wip-limit'}}
|
||||
if isWipLimitEnabled
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
if isWipLimitEnabled
|
||||
p
|
||||
input.wip-limit-value(type="number" value="{{ wipLimitValue }}" min="1" max="99")
|
||||
@@ -207,8 +250,8 @@ template(name="setListWidthPopup")
|
||||
#js-list-width-edit
|
||||
label {{_ 'set-list-width-value'}}
|
||||
p
|
||||
input.list-width-value(type="number" value="{{ listWidthValue }}" min="100")
|
||||
input.list-constraint-value(type="number" value="{{ listConstraintValue }}" min="100")
|
||||
input.list-width-value(type="number" value="{{ listWidthValue }}" min="270")
|
||||
input.list-constraint-value(type="number" value="{{ listConstraintValue }}" min="270")
|
||||
input.list-width-apply(type="submit" value="{{_ 'apply'}}")
|
||||
input.list-width-error
|
||||
br
|
||||
@@ -219,7 +262,7 @@ template(name="setListWidthPopup")
|
||||
|
||||
template(name="listWidthErrorPopup")
|
||||
.list-width-invalid
|
||||
p {{_ 'list-width-error-message'}} '>=100'
|
||||
p {{_ 'list-width-error-message'}} '>=270'
|
||||
button.full.js-back-view(type="submit") {{_ 'cancel'}}
|
||||
|
||||
template(name="setListColorPopup")
|
||||
@@ -228,6 +271,77 @@ template(name="setListColorPopup")
|
||||
// note: we use the swimlane palette to have more than just the border
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
|
||||
template(name="copyListPopup")
|
||||
label(for='copy-list-title') {{_ 'title'}}:
|
||||
input#copy-list-title.full-line(type="text" value="{{title}}" autofocus)
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
div
|
||||
input(type="radio" name="list-position" value="left" checked id="list-position-left" style="display: inline")
|
||||
label(for="list-position-left") {{_ 'left-of-list'}}
|
||||
div
|
||||
input(type="radio" name="list-position" value="right" id="list-position-right" style="display: inline")
|
||||
label(for="list-position-right") {{_ 'right-of-list'}}
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
||||
template(name="moveListPopup")
|
||||
label(for='move-list-title') {{_ 'title'}}:
|
||||
input#move-list-title.full-line(type="text" value="{{title}}" autofocus)
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
div
|
||||
input(type="radio" name="list-position" value="left" checked id="list-position-left" style="display: inline")
|
||||
label(for="list-position-left") {{_ 'left-of-list'}}
|
||||
div
|
||||
input(type="radio" name="list-position" value="right" id="list-position-right" style="display: inline")
|
||||
label(for="list-position-right") {{_ 'right-of-list'}}
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
||||
template(name="addListPopup")
|
||||
form.js-add-list-form
|
||||
input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}" autocomplete="off" autofocus)
|
||||
if currentSwimlaneData
|
||||
if swimlaneLists.length
|
||||
label {{_ 'add-after-list'}}
|
||||
select.list-position-input.full-line
|
||||
each swimlaneLists
|
||||
option(value="{{_id}}" selected="{{$eq _id currentListIdValue}}") {{increment @index}} {{title}}
|
||||
else
|
||||
if currentBoard.lists.length
|
||||
label {{_ 'add-after-list'}}
|
||||
select.list-position-input.full-line
|
||||
each currentBoard.lists
|
||||
option(value="{{_id}}" selected="{{$eq _id currentListIdValue}}") {{increment @index}} {{title}}
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-add-list(type="submit") {{_ 'save'}}
|
||||
unless currentBoard.isTemplatesBoard
|
||||
unless currentBoard.isTemplateBoard
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-list-template {{_ 'template'}}
|
||||
@@ -2,19 +2,24 @@ import { ReactiveCache } from '/imports/reactiveCache';
|
||||
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;
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.listHeader.helpers({
|
||||
canSeeAddCard() {
|
||||
const list = Template.currentData();
|
||||
return (
|
||||
(!list.getWipLimit('enabled') ||
|
||||
list.getWipLimit('soft') ||
|
||||
!this.reachedWipLimit()) &&
|
||||
!Template.instance().reachedWipLimit()) &&
|
||||
!ReactiveCache.getCurrentUser().isWorker()
|
||||
);
|
||||
},
|
||||
@@ -22,42 +27,19 @@ BlazeComponent.extendComponent({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
starred(check = undefined) {
|
||||
|
||||
starred() {
|
||||
const list = Template.currentData();
|
||||
const status = list.isStarred();
|
||||
if (check === undefined) {
|
||||
// just check
|
||||
return status;
|
||||
} else {
|
||||
list.star(!status);
|
||||
return !status;
|
||||
}
|
||||
return list.isStarred();
|
||||
},
|
||||
collapsed(check = undefined) {
|
||||
|
||||
collapsed() {
|
||||
const list = Template.currentData();
|
||||
const status = Utils.getListCollapseState(list);
|
||||
if (check === undefined) {
|
||||
// just check
|
||||
return status;
|
||||
} else {
|
||||
const next = typeof check === 'boolean' ? check : !status;
|
||||
Utils.setListCollapseState(list, next);
|
||||
return next;
|
||||
}
|
||||
},
|
||||
editTitle(event) {
|
||||
event.preventDefault();
|
||||
const newTitle = this.childComponents('inlinedForm')[0]
|
||||
.getValue()
|
||||
.trim();
|
||||
const list = this.currentData();
|
||||
if (newTitle) {
|
||||
list.rename(newTitle.trim());
|
||||
}
|
||||
return Utils.getListCollapseState(list);
|
||||
},
|
||||
|
||||
isWatching() {
|
||||
const list = this.currentData();
|
||||
const list = Template.currentData();
|
||||
return list.findWatcher(Meteor.userId());
|
||||
},
|
||||
|
||||
@@ -73,10 +55,9 @@ BlazeComponent.extendComponent({
|
||||
cardsCount() {
|
||||
const list = Template.currentData();
|
||||
let swimlaneId = '';
|
||||
if (Utils.boardView() === 'board-view-swimlanes')
|
||||
swimlaneId = this.parentComponent()
|
||||
.parentComponent()
|
||||
.data()._id;
|
||||
if (Utils.boardView() === 'board-view-swimlanes') {
|
||||
swimlaneId = list.swimlaneId || '';
|
||||
}
|
||||
|
||||
const ret = list.cards(swimlaneId).length;
|
||||
return ret;
|
||||
@@ -99,7 +80,8 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
showCardsCountForList(count) {
|
||||
const limit = this.limitToShowCardsCount();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const limit = currentUser ? currentUser.getLimitToShowCardsCount() : false;
|
||||
return limit >= 0 && count >= limit;
|
||||
},
|
||||
|
||||
@@ -111,40 +93,6 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-list-star'(event) {
|
||||
event.preventDefault();
|
||||
this.starred(!this.starred());
|
||||
},
|
||||
'click .js-collapse'(event) {
|
||||
event.preventDefault();
|
||||
this.collapsed(!this.collapsed());
|
||||
},
|
||||
'click .js-open-list-menu': Popup.open('listAction'),
|
||||
'click .js-add-card.list-header-plus-top'(event) {
|
||||
const listDom = $(event.target).parents(
|
||||
`#js-list-${this.currentData()._id}`,
|
||||
)[0];
|
||||
const listComponent = BlazeComponent.getComponentForElement(listDom);
|
||||
listComponent.openForm({
|
||||
position: 'top',
|
||||
});
|
||||
},
|
||||
'click .js-unselect-list'() {
|
||||
Session.set('currentList', null);
|
||||
},
|
||||
submit: this.editTitle,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('listHeader');
|
||||
|
||||
Template.listHeader.helpers({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
numberFieldsSum() {
|
||||
const list = Template.currentData();
|
||||
if (!list) return 0;
|
||||
@@ -177,6 +125,7 @@ Template.listHeader.helpers({
|
||||
}
|
||||
return total;
|
||||
},
|
||||
|
||||
hasNumberFieldsSum() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const fields = ReactiveCache.getCustomFields({
|
||||
@@ -188,6 +137,56 @@ Template.listHeader.helpers({
|
||||
},
|
||||
});
|
||||
|
||||
// Helper function on template instance for reachedWipLimit check
|
||||
Template.listHeader.onCreated(function () {
|
||||
this.reachedWipLimit = function () {
|
||||
const list = Template.currentData();
|
||||
return (
|
||||
list.getWipLimit('enabled') &&
|
||||
list.getWipLimit('value') <= list.cards().length
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
Template.listHeader.events({
|
||||
async 'click .js-list-star'(event) {
|
||||
event.preventDefault();
|
||||
const list = Template.currentData();
|
||||
const status = list.isStarred();
|
||||
await list.star(!status);
|
||||
},
|
||||
'click .js-collapse'(event) {
|
||||
event.preventDefault();
|
||||
const list = Template.currentData();
|
||||
const status = Utils.getListCollapseState(list);
|
||||
Utils.setListCollapseState(list, !status);
|
||||
},
|
||||
'click .js-open-list-menu': Popup.open('listAction'),
|
||||
'click .js-add-card.list-header-plus-top'(event) {
|
||||
const listDom = $(event.target).parents(
|
||||
`#js-list-${Template.currentData()._id}`,
|
||||
)[0];
|
||||
const view = Blaze.getView(listDom, 'Template.list');
|
||||
const listComponent = view?.templateInstance?.();
|
||||
if (listComponent) {
|
||||
listComponent.openForm({
|
||||
position: 'top',
|
||||
});
|
||||
}
|
||||
},
|
||||
'click .js-unselect-list'() {
|
||||
Session.set('currentList', null);
|
||||
},
|
||||
async 'submit'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const newTitle = tpl.$('textarea,input[type=text]').val()?.trim();
|
||||
const list = Template.currentData();
|
||||
if (newTitle) {
|
||||
await list.rename(newTitle.trim());
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.listActionPopup.helpers({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
@@ -204,14 +203,29 @@ Template.listActionPopup.helpers({
|
||||
|
||||
Template.listActionPopup.events({
|
||||
'click .js-list-subscribe'() {},
|
||||
'click .js-add-card.list-header-plus-bottom'(event) {
|
||||
'click .js-add-card.list-header-plus-top'(event) {
|
||||
const listDom = $(`#js-list-${this._id}`)[0];
|
||||
const listComponent = BlazeComponent.getComponentForElement(listDom);
|
||||
listComponent.openForm({
|
||||
position: 'bottom',
|
||||
});
|
||||
const view = Blaze.getView(listDom, 'Template.list');
|
||||
const listComponent = view?.templateInstance?.();
|
||||
if (listComponent) {
|
||||
listComponent.openForm({
|
||||
position: 'top',
|
||||
});
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-add-card.list-header-plus-bottom'(event) {
|
||||
const listDom = $(`#js-list-${this._id}`)[0];
|
||||
const view = Blaze.getView(listDom, 'Template.list');
|
||||
const listComponent = view?.templateInstance?.();
|
||||
if (listComponent) {
|
||||
listComponent.openForm({
|
||||
position: 'bottom',
|
||||
});
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-add-list': Popup.open('addList'),
|
||||
'click .js-set-list-width': Popup.open('setListWidth'),
|
||||
'click .js-set-color-list': Popup.open('setListColor'),
|
||||
'click .js-select-cards'() {
|
||||
@@ -226,59 +240,17 @@ Template.listActionPopup.events({
|
||||
if (!err && ret) Popup.back();
|
||||
});
|
||||
},
|
||||
'click .js-close-list'(event) {
|
||||
event.preventDefault();
|
||||
this.archive();
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-close-list': Popup.afterConfirm('listArchive', async function() {
|
||||
await this.archive();
|
||||
Popup.close();
|
||||
}),
|
||||
'click .js-set-wip-limit': Popup.open('setWipLimit'),
|
||||
'click .js-copy-list': Popup.open('copyList'),
|
||||
'click .js-move-list': Popup.open('moveList'),
|
||||
'click .js-more': Popup.open('listMore'),
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
applyWipLimit() {
|
||||
const list = Template.currentData();
|
||||
const limit = parseInt(
|
||||
Template.instance()
|
||||
.$('.wip-limit-value')
|
||||
.val(),
|
||||
10,
|
||||
);
|
||||
|
||||
if (limit < list.cards().length && !list.getWipLimit('soft')) {
|
||||
Template.instance()
|
||||
.$('.wip-limit-error')
|
||||
.click();
|
||||
} else {
|
||||
Meteor.call('applyWipLimit', list._id, limit);
|
||||
Popup.back();
|
||||
}
|
||||
},
|
||||
|
||||
enableSoftLimit() {
|
||||
const list = Template.currentData();
|
||||
|
||||
if (
|
||||
list.getWipLimit('soft') &&
|
||||
list.getWipLimit('value') < list.cards().length
|
||||
) {
|
||||
list.setWipLimit(list.cards().length);
|
||||
}
|
||||
Meteor.call('enableSoftLimit', Template.currentData()._id);
|
||||
},
|
||||
|
||||
enableWipLimit() {
|
||||
const list = Template.currentData();
|
||||
// Prevent user from using previously stored wipLimit.value if it is less than the current number of cards in the list
|
||||
if (
|
||||
!list.getWipLimit('enabled') &&
|
||||
list.getWipLimit('value') < list.cards().length
|
||||
) {
|
||||
list.setWipLimit(list.cards().length);
|
||||
}
|
||||
Meteor.call('enableWipLimit', list._id);
|
||||
},
|
||||
|
||||
Template.setWipLimitPopup.helpers({
|
||||
isWipLimitSoft() {
|
||||
return Template.currentData().getWipLimit('soft');
|
||||
},
|
||||
@@ -290,125 +262,225 @@ BlazeComponent.extendComponent({
|
||||
wipLimitValue() {
|
||||
return Template.currentData().getWipLimit('value');
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-enable-wip-limit': this.enableWipLimit,
|
||||
'click .wip-limit-apply': this.applyWipLimit,
|
||||
'click .wip-limit-error': Popup.open('wipLimitError'),
|
||||
'click .materialCheckBox': this.enableSoftLimit,
|
||||
},
|
||||
];
|
||||
Template.setWipLimitPopup.events({
|
||||
async 'click .js-enable-wip-limit'() {
|
||||
const list = Template.currentData();
|
||||
// Prevent user from using previously stored wipLimit.value if it is less than the current number of cards in the list
|
||||
if (
|
||||
!list.getWipLimit('enabled') &&
|
||||
list.getWipLimit('value') < list.cards().length
|
||||
) {
|
||||
await list.setWipLimit(list.cards().length);
|
||||
}
|
||||
Meteor.call('enableWipLimit', list._id);
|
||||
},
|
||||
}).register('setWipLimitPopup');
|
||||
'click .wip-limit-apply'(event, tpl) {
|
||||
const list = Template.currentData();
|
||||
const limit = parseInt(
|
||||
tpl.$('.wip-limit-value').val(),
|
||||
10,
|
||||
);
|
||||
|
||||
if (limit < list.cards().length && !list.getWipLimit('soft')) {
|
||||
tpl.$('.wip-limit-error').click();
|
||||
} else {
|
||||
Meteor.call('applyWipLimit', list._id, limit);
|
||||
Popup.back();
|
||||
}
|
||||
},
|
||||
'click .wip-limit-error': Popup.open('wipLimitError'),
|
||||
async 'click .materialCheckBox'() {
|
||||
const list = Template.currentData();
|
||||
|
||||
if (
|
||||
list.getWipLimit('soft') &&
|
||||
list.getWipLimit('value') < list.cards().length
|
||||
) {
|
||||
await list.setWipLimit(list.cards().length);
|
||||
}
|
||||
Meteor.call('enableSoftLimit', Template.currentData()._id);
|
||||
},
|
||||
});
|
||||
|
||||
Template.listMorePopup.events({
|
||||
'click .js-delete': Popup.afterConfirm('listDelete', function() {
|
||||
Popup.back();
|
||||
const allCards = this.allCards();
|
||||
const allCardIds = _.pluck(allCards, '_id');
|
||||
const list = Lists.findOne(this._id);
|
||||
if (!list) return;
|
||||
const allCards = list.allCards();
|
||||
const allCardIds = allCards.map(c => c._id);
|
||||
// it's okay if the linked cards are on the same list
|
||||
if (
|
||||
ReactiveCache.getCards({
|
||||
$and: [
|
||||
{ listId: { $ne: this._id } },
|
||||
{ listId: { $ne: list._id } },
|
||||
{ linkedId: { $in: allCardIds } },
|
||||
],
|
||||
}).length === 0
|
||||
) {
|
||||
allCardIds.map(_id => Cards.remove(_id));
|
||||
Lists.remove(this._id);
|
||||
Lists.remove(list._id);
|
||||
} else {
|
||||
// TODO: Figure out more informative message.
|
||||
// Popup with a hint that the list cannot be deleted as there are
|
||||
// linked cards. We can adapt the query above so we can list the linked
|
||||
// cards.
|
||||
// Related:
|
||||
// client/components/cards/cardDetails.js about line 969
|
||||
// https://github.com/wekan/wekan/issues/2785
|
||||
const message = `${TAPi18n.__(
|
||||
'delete-linked-cards-before-this-list',
|
||||
)} linkedId: ${
|
||||
this._id
|
||||
list._id
|
||||
} at client/components/lists/listHeader.js and https://github.com/wekan/wekan/issues/2785`;
|
||||
alert(message);
|
||||
}
|
||||
Utils.goBoardId(this.boardId);
|
||||
Utils.goBoardId(list.boardId);
|
||||
}),
|
||||
});
|
||||
|
||||
Template.listHeader.helpers({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
function registerListDialogTemplate(templateName) {
|
||||
Template[templateName].helpers({
|
||||
boards() {
|
||||
return Template.instance().dialog.boards();
|
||||
},
|
||||
swimlanes() {
|
||||
return Template.instance().dialog.swimlanes();
|
||||
},
|
||||
lists() {
|
||||
return Template.instance().dialog.lists();
|
||||
},
|
||||
isDialogOptionBoardId(boardId) {
|
||||
return Template.instance().dialog.isDialogOptionBoardId(boardId);
|
||||
},
|
||||
isDialogOptionSwimlaneId(swimlaneId) {
|
||||
return Template.instance().dialog.isDialogOptionSwimlaneId(swimlaneId);
|
||||
},
|
||||
isDialogOptionListId(listId) {
|
||||
return Template.instance().dialog.isDialogOptionListId(listId);
|
||||
},
|
||||
isTitleDefault(title) {
|
||||
return Template.instance().dialog.isTitleDefault(title);
|
||||
},
|
||||
});
|
||||
|
||||
Template[templateName].events({
|
||||
async 'click .js-done'(event, tpl) {
|
||||
const dialog = tpl.dialog;
|
||||
const boardSelect = tpl.$('.js-select-boards')[0];
|
||||
const boardId = boardSelect?.options[boardSelect?.selectedIndex]?.value;
|
||||
const swimlaneSelect = tpl.$('.js-select-swimlanes')[0];
|
||||
const swimlaneId = swimlaneSelect?.options[swimlaneSelect?.selectedIndex]?.value;
|
||||
const listSelect = tpl.$('.js-select-lists')[0];
|
||||
const listId = listSelect?.options[listSelect?.selectedIndex]?.value || null;
|
||||
const position = tpl.$('input[name="list-position"]:checked').val() || 'right';
|
||||
try {
|
||||
await dialog.setDone({ boardId, swimlaneId, listId, position });
|
||||
} catch (e) {
|
||||
console.error('Error in list dialog operation:', e);
|
||||
}
|
||||
Popup.back(2);
|
||||
},
|
||||
'change .js-select-boards'(event, tpl) {
|
||||
tpl.dialog.getBoardData($(event.currentTarget).val());
|
||||
},
|
||||
'change .js-select-swimlanes'(event, tpl) {
|
||||
tpl.dialog.selectedSwimlaneId.set($(event.currentTarget).val());
|
||||
tpl.dialog.setFirstListId();
|
||||
},
|
||||
'change .js-select-lists'(event, tpl) {
|
||||
tpl.dialog.selectedListId.set($(event.currentTarget).val());
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Template.copyListPopup.onCreated(function () {
|
||||
this.dialog = new BoardSwimlaneListDialog(this, {
|
||||
getDialogOptions() {
|
||||
return null;
|
||||
},
|
||||
async setDone(options) {
|
||||
const tpl = Template.instance();
|
||||
const title = tpl.$('#copy-list-title').val().trim();
|
||||
if (!title) return;
|
||||
const list = Template.currentData();
|
||||
await Meteor.callAsync('copyList', list._id, options.boardId, options.swimlaneId, title, options.listId, options.position);
|
||||
},
|
||||
});
|
||||
});
|
||||
registerListDialogTemplate('copyListPopup');
|
||||
|
||||
Template.moveListPopup.onCreated(function () {
|
||||
this.dialog = new BoardSwimlaneListDialog(this, {
|
||||
getDialogOptions() {
|
||||
return null;
|
||||
},
|
||||
async setDone(options) {
|
||||
const tpl = Template.instance();
|
||||
const title = tpl.$('#move-list-title').val().trim();
|
||||
const list = Template.currentData();
|
||||
await Meteor.callAsync('moveList', list._id, options.boardId, options.swimlaneId, options.listId, options.position, title);
|
||||
},
|
||||
});
|
||||
});
|
||||
registerListDialogTemplate('moveListPopup');
|
||||
|
||||
Template.setListColorPopup.onCreated(function () {
|
||||
const data = Template.currentData();
|
||||
this.currentList = Lists.findOne(data._id) || data;
|
||||
this.currentColor = new ReactiveVar(this.currentList.color);
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentList = this.currentData();
|
||||
this.currentColor = new ReactiveVar(this.currentList.color);
|
||||
},
|
||||
|
||||
Template.setListColorPopup.helpers({
|
||||
colors() {
|
||||
return listsColors.map(color => ({ color, name: '' }));
|
||||
},
|
||||
|
||||
isSelected(color) {
|
||||
if (this.currentColor.get() === null) {
|
||||
const tpl = Template.instance();
|
||||
if (tpl.currentColor.get() === null) {
|
||||
return color === 'white';
|
||||
} else {
|
||||
return this.currentColor.get() === color;
|
||||
return tpl.currentColor.get() === color;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-palette-color'() {
|
||||
this.currentColor.set(this.currentData().color);
|
||||
},
|
||||
'click .js-submit'() {
|
||||
this.currentList.setColor(this.currentColor.get());
|
||||
Popup.close();
|
||||
},
|
||||
'click .js-remove-color'() {
|
||||
this.currentList.setColor(null);
|
||||
Popup.close();
|
||||
},
|
||||
},
|
||||
];
|
||||
Template.setListColorPopup.events({
|
||||
'click .js-palette-color'(event, tpl) {
|
||||
const paletteData = Blaze.getData(event.currentTarget);
|
||||
tpl.currentColor.set(paletteData?.color);
|
||||
},
|
||||
}).register('setListColorPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
applyListWidth() {
|
||||
const list = Template.currentData();
|
||||
const board = list.boardId;
|
||||
const width = parseInt(
|
||||
Template.instance()
|
||||
.$('.list-width-value')
|
||||
.val(),
|
||||
10,
|
||||
);
|
||||
const constraint = parseInt(
|
||||
Template.instance()
|
||||
.$('.list-constraint-value')
|
||||
.val(),
|
||||
10,
|
||||
);
|
||||
|
||||
// FIXME(mark-i-m): where do we put constants?
|
||||
if (width < 100 || !width || constraint < 100 || !constraint) {
|
||||
Template.instance()
|
||||
.$('.list-width-error')
|
||||
.click();
|
||||
} else {
|
||||
Meteor.call('applyListWidth', board, list._id, width, constraint);
|
||||
Popup.back();
|
||||
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) {
|
||||
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) {
|
||||
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();
|
||||
},
|
||||
});
|
||||
|
||||
Template.setListWidthPopup.helpers({
|
||||
listWidthValue() {
|
||||
const list = Template.currentData();
|
||||
const board = list.boardId;
|
||||
@@ -426,17 +498,127 @@ BlazeComponent.extendComponent({
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
return user && user.isAutoWidth(boardId);
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-auto-width-board'() {
|
||||
dragscroll.reset();
|
||||
ReactiveCache.getCurrentUser().toggleAutoWidth(Utils.getCurrentBoardId());
|
||||
},
|
||||
'click .list-width-apply': this.applyListWidth,
|
||||
'click .list-width-error': Popup.open('listWidthError'),
|
||||
},
|
||||
];
|
||||
Template.setListWidthPopup.events({
|
||||
'click .js-auto-width-board'() {
|
||||
dragscroll.reset();
|
||||
ReactiveCache.getCurrentUser().toggleAutoWidth(Utils.getCurrentBoardId());
|
||||
},
|
||||
}).register('setListWidthPopup');
|
||||
'click .list-width-apply'(event, tpl) {
|
||||
const list = Template.currentData();
|
||||
const board = list.boardId;
|
||||
const width = parseInt(
|
||||
tpl.$('.list-width-value').val(),
|
||||
10,
|
||||
);
|
||||
const constraint = parseInt(
|
||||
tpl.$('.list-constraint-value').val(),
|
||||
10,
|
||||
);
|
||||
|
||||
// FIXME(mark-i-m): where do we put constants?
|
||||
if (width < 270 || !width || constraint < 270 || !constraint) {
|
||||
tpl.$('.list-width-error').click();
|
||||
} else {
|
||||
Meteor.call('applyListWidth', board, list._id, width, constraint);
|
||||
Popup.back();
|
||||
}
|
||||
},
|
||||
'click .list-width-error': Popup.open('listWidthError'),
|
||||
});
|
||||
|
||||
Template.addListPopup.onCreated(function () {
|
||||
this.currentBoard = Utils.getCurrentBoard();
|
||||
this.currentSwimlaneId = new ReactiveVar(null);
|
||||
this.currentListId = new ReactiveVar(null);
|
||||
|
||||
// Get the swimlane context from opener
|
||||
const openerComponent = Popup.getOpenerComponent();
|
||||
const openerData = openerComponent?.data || Popup._getTopStack()?.dataContext;
|
||||
|
||||
// If opened from swimlane menu, openerData is the swimlane
|
||||
if (openerData?.type === 'swimlane' || openerData?.type === 'template-swimlane') {
|
||||
this.currentSwimlane = openerData;
|
||||
this.currentSwimlaneId.set(openerData._id);
|
||||
} else if (openerData?._id) {
|
||||
// If opened from list menu, get swimlane from the list
|
||||
const list = ReactiveCache.getList({ _id: openerData._id });
|
||||
if (list) {
|
||||
this.currentSwimlane = list.swimlaneId
|
||||
? ReactiveCache.getSwimlane({ _id: list.swimlaneId })
|
||||
: null;
|
||||
this.currentSwimlaneId.set(this.currentSwimlane?._id || null);
|
||||
this.currentListId.set(openerData._id);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.currentSwimlaneId.get()) {
|
||||
const defaultSwimlane = this.currentBoard.getDefaultSwimline?.();
|
||||
if (defaultSwimlane?._id) {
|
||||
this.currentSwimlane = defaultSwimlane;
|
||||
this.currentSwimlaneId.set(defaultSwimlane._id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Template.addListPopup.helpers({
|
||||
currentSwimlaneData() {
|
||||
const tpl = Template.instance();
|
||||
const swimlaneId = tpl.currentSwimlaneId.get();
|
||||
return swimlaneId ? ReactiveCache.getSwimlane({ _id: swimlaneId }) : null;
|
||||
},
|
||||
|
||||
currentListIdValue() {
|
||||
return Template.instance().currentListId.get();
|
||||
},
|
||||
|
||||
swimlaneLists() {
|
||||
const tpl = Template.instance();
|
||||
const swimlaneId = tpl.currentSwimlaneId.get();
|
||||
if (swimlaneId) {
|
||||
return ReactiveCache.getLists({ swimlaneId, archived: false }).sort((a, b) => a.sort - b.sort);
|
||||
}
|
||||
return tpl.currentBoard.lists;
|
||||
},
|
||||
});
|
||||
|
||||
Template.addListPopup.events({
|
||||
async 'submit .js-add-list-form'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
|
||||
const titleInput = tpl.find('.list-name-input');
|
||||
const title = titleInput?.value.trim();
|
||||
|
||||
if (!title) return;
|
||||
|
||||
const positionInput = tpl.find('.list-position-input');
|
||||
const afterListId =
|
||||
positionInput && positionInput.value ? positionInput.value.trim() : null;
|
||||
const nextListId =
|
||||
positionInput &&
|
||||
positionInput.selectedIndex >= 0 &&
|
||||
positionInput.options[positionInput.selectedIndex + 1]
|
||||
? positionInput.options[positionInput.selectedIndex + 1].value
|
||||
: null;
|
||||
const targetSwimlaneId =
|
||||
(tpl.currentSwimlaneId && tpl.currentSwimlaneId.get && tpl.currentSwimlaneId.get()) ||
|
||||
(tpl.currentSwimlane && tpl.currentSwimlane._id) ||
|
||||
null;
|
||||
|
||||
try {
|
||||
await Meteor.callAsync('createListAfter', {
|
||||
title,
|
||||
boardId: Session.get('currentBoard'),
|
||||
swimlaneId: targetSwimlaneId,
|
||||
afterListId,
|
||||
nextListId,
|
||||
type: 'list',
|
||||
});
|
||||
Popup.back();
|
||||
} catch (error) {
|
||||
console.error('Failed to create list after selected list:', error);
|
||||
}
|
||||
},
|
||||
'click .js-list-template': Popup.open('searchElement'),
|
||||
});
|
||||
@@ -3,6 +3,7 @@ template(name="minilist")
|
||||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
span.drag-handle(title="{{_ 'dragList'}}") ↕️
|
||||
span.drag-handle(title="{{_ 'dragList'}}")
|
||||
i.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
||||
@@ -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 = {
|
||||
@@ -18,21 +19,19 @@ const accessibilityHelpers = {
|
||||
};
|
||||
|
||||
// Main accessibility page component
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.error = new ReactiveVar('');
|
||||
this.loading = new ReactiveVar(false);
|
||||
Template.accessibility.onCreated(function () {
|
||||
this.error = new ReactiveVar('');
|
||||
this.loading = new ReactiveVar(false);
|
||||
|
||||
Meteor.subscribe('setting');
|
||||
Meteor.subscribe('accessibilitySettings');
|
||||
},
|
||||
...accessibilityHelpers
|
||||
}).register('accessibility');
|
||||
Meteor.subscribe('setting');
|
||||
Meteor.subscribe('accessibilitySettings');
|
||||
});
|
||||
|
||||
Template.accessibility.helpers(accessibilityHelpers);
|
||||
|
||||
// Header bar component
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
Meteor.subscribe('accessibilitySettings');
|
||||
},
|
||||
...accessibilityHelpers
|
||||
}).register('accessibilityHeaderBar');
|
||||
Template.accessibilityHeaderBar.onCreated(function () {
|
||||
Meteor.subscribe('accessibilitySettings');
|
||||
});
|
||||
|
||||
Template.accessibilityHeaderBar.helpers(accessibilityHelpers);
|
||||
@@ -1,3 +1,5 @@
|
||||
import Boards from '/models/boards';
|
||||
|
||||
Template.bookmarks.helpers({
|
||||
hasStarredBoards() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
@@ -15,12 +17,12 @@ Template.bookmarks.helpers({
|
||||
});
|
||||
|
||||
Template.bookmarks.events({
|
||||
'click .js-toggle-star'(e) {
|
||||
async 'click .js-toggle-star'(e) {
|
||||
e.preventDefault();
|
||||
const boardId = this._id;
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && boardId) {
|
||||
user.toggleBoardStar(boardId);
|
||||
await user.toggleBoardStar(boardId);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -42,12 +44,12 @@ Template.bookmarksPopup.helpers({
|
||||
});
|
||||
|
||||
Template.bookmarksPopup.events({
|
||||
'click .js-toggle-star'(e) {
|
||||
async 'click .js-toggle-star'(e) {
|
||||
e.preventDefault();
|
||||
const boardId = this._id;
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && boardId) {
|
||||
user.toggleBoardStar(boardId);
|
||||
await user.toggleBoardStar(boardId);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,18 +1,57 @@
|
||||
import { CardSearchPagedComponent } from '../../lib/cardSearch';
|
||||
import { CardSearchPaged } from '../../lib/cardSearch';
|
||||
|
||||
BlazeComponent.extendComponent({}).register('brokenCardsHeaderBar');
|
||||
Template.brokenCards.onCreated(function () {
|
||||
const search = new CardSearchPaged(this);
|
||||
this.search = search;
|
||||
|
||||
Meteor.subscribe('brokenCards', search.sessionId);
|
||||
});
|
||||
|
||||
Template.brokenCards.helpers({
|
||||
userId() {
|
||||
return Meteor.userId();
|
||||
},
|
||||
|
||||
// Return ReactiveVars so jade can use .get pattern
|
||||
searching() {
|
||||
return Template.instance().search.searching;
|
||||
},
|
||||
hasResults() {
|
||||
return Template.instance().search.hasResults;
|
||||
},
|
||||
hasQueryErrors() {
|
||||
return Template.instance().search.hasQueryErrors;
|
||||
},
|
||||
errorMessages() {
|
||||
return Template.instance().search.queryErrorMessages();
|
||||
},
|
||||
resultsCount() {
|
||||
return Template.instance().search.resultsCount;
|
||||
},
|
||||
resultsHeading() {
|
||||
return Template.instance().search.resultsHeading;
|
||||
},
|
||||
results() {
|
||||
return Template.instance().search.results;
|
||||
},
|
||||
getSearchHref() {
|
||||
return Template.instance().search.getSearchHref();
|
||||
},
|
||||
hasPreviousPage() {
|
||||
return Template.instance().search.hasPreviousPage;
|
||||
},
|
||||
hasNextPage() {
|
||||
return Template.instance().search.hasNextPage;
|
||||
},
|
||||
});
|
||||
|
||||
class BrokenCardsComponent extends CardSearchPagedComponent {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
|
||||
Meteor.subscribe('brokenCards', this.sessionId);
|
||||
}
|
||||
}
|
||||
BrokenCardsComponent.register('brokenCards');
|
||||
Template.brokenCards.events({
|
||||
'click .js-next-page'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.search.nextPage();
|
||||
},
|
||||
'click .js-previous-page'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
tpl.search.previousPage();
|
||||
},
|
||||
});
|
||||
@@ -1,23 +1,23 @@
|
||||
template(name="dueCardsHeaderBar")
|
||||
if currentUser
|
||||
h1
|
||||
| 📅
|
||||
i.fa.fa-calendar
|
||||
| {{_ 'dueCards-title'}}
|
||||
|
||||
.board-header-btns.left
|
||||
a.board-header-btn.js-due-cards-view-change(title="{{_ 'dueCardsViewChange-title'}}")
|
||||
| ▼
|
||||
i.fa.fa-caret-down
|
||||
if $eq dueCardsView 'me'
|
||||
| 👤
|
||||
i.fa.fa-user
|
||||
| {{_ 'dueCardsViewChange-choice-me'}}
|
||||
if $eq dueCardsView 'all'
|
||||
| 👥
|
||||
i.fa.fa-users
|
||||
| {{_ 'dueCardsViewChange-choice-all'}}
|
||||
|
||||
template(name="dueCardsModalTitle")
|
||||
if currentUser
|
||||
h2
|
||||
| ⌨️
|
||||
i.fa.fa-keyboard-o
|
||||
| {{_ 'dueCards-title'}}
|
||||
|
||||
template(name="dueCards")
|
||||
@@ -49,18 +49,18 @@ template(name="dueCardsViewChangePopup")
|
||||
li
|
||||
with "dueCardsViewChange-choice-me"
|
||||
a.js-due-cards-view-me
|
||||
| 👤
|
||||
i.fa.fa-user
|
||||
| {{_ 'dueCardsViewChange-choice-me'}}
|
||||
if $eq Utils.dueCardsView "me"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
hr
|
||||
li
|
||||
with "dueCardsViewChange-choice-all"
|
||||
a.js-due-cards-view-all
|
||||
| 👥
|
||||
i.fa.fa-users
|
||||
| {{_ 'dueCardsViewChange-choice-all'}}
|
||||
span.sub-name
|
||||
+viewer
|
||||
| {{_ 'dueCardsViewChange-choice-all-description' }}
|
||||
if $eq Utils.dueCardsView "all"
|
||||
| ✅
|
||||
i.fa.fa-check
|
||||
+123
-172
@@ -1,178 +1,38 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
// const subManager = new SubsManager();
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.dueCardsHeaderBar.helpers({
|
||||
dueCardsView() {
|
||||
// eslint-disable-next-line no-console
|
||||
// console.log('sort:', Utils.dueCardsView());
|
||||
return Utils && Utils.dueCardsView ? Utils.dueCardsView() : 'me';
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-due-cards-view-change': Popup.open('dueCardsViewChange'),
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('dueCardsHeaderBar');
|
||||
|
||||
Template.dueCards.helpers({
|
||||
userId() {
|
||||
return Meteor.userId();
|
||||
},
|
||||
dueCardsList() {
|
||||
const component = BlazeComponent.getComponentForElement(this.firstNode);
|
||||
if (component && component.dueCardsList) {
|
||||
return component.dueCardsList();
|
||||
}
|
||||
return [];
|
||||
},
|
||||
hasResults() {
|
||||
const component = BlazeComponent.getComponentForElement(this.firstNode);
|
||||
if (component && component.hasResults) {
|
||||
return component.hasResults.get();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
searching() {
|
||||
const component = BlazeComponent.getComponentForElement(this.firstNode);
|
||||
if (component && component.isLoading) {
|
||||
return component.isLoading.get();
|
||||
}
|
||||
return true; // Show loading by default
|
||||
},
|
||||
hasQueryErrors() {
|
||||
return false; // No longer using search, so always false
|
||||
},
|
||||
errorMessages() {
|
||||
return []; // No longer using search, so always empty
|
||||
},
|
||||
cardsCount() {
|
||||
const component = BlazeComponent.getComponentForElement(this.firstNode);
|
||||
if (component && component.cardsCount) {
|
||||
return component.cardsCount();
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
resultsText() {
|
||||
const component = BlazeComponent.getComponentForElement(this.firstNode);
|
||||
if (component && component.resultsText) {
|
||||
return component.resultsText();
|
||||
}
|
||||
return '';
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-due-cards-view-me'() {
|
||||
if (Utils && Utils.setDueCardsView) {
|
||||
Utils.setDueCardsView('me');
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
Template.dueCardsHeaderBar.events({
|
||||
'click .js-due-cards-view-change': Popup.open('dueCardsViewChange'),
|
||||
});
|
||||
|
||||
'click .js-due-cards-view-all'() {
|
||||
if (Utils && Utils.setDueCardsView) {
|
||||
Utils.setDueCardsView('all');
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('dueCardsViewChangePopup');
|
||||
Template.dueCards.onCreated(function () {
|
||||
this._cachedCards = null;
|
||||
this._cachedTimestamp = null;
|
||||
this.subscriptionHandle = null;
|
||||
this.isLoading = new ReactiveVar(true);
|
||||
this.hasResults = new ReactiveVar(false);
|
||||
this.searching = new ReactiveVar(false);
|
||||
|
||||
class DueCardsComponent extends BlazeComponent {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
|
||||
this._cachedCards = null;
|
||||
this._cachedTimestamp = null;
|
||||
this.subscriptionHandle = null;
|
||||
this.isLoading = new ReactiveVar(true);
|
||||
this.hasResults = new ReactiveVar(false);
|
||||
this.searching = new ReactiveVar(false);
|
||||
|
||||
// Subscribe to the optimized due cards publication
|
||||
this.autorun(() => {
|
||||
const allUsers = this.dueCardsView() === 'all';
|
||||
if (this.subscriptionHandle) {
|
||||
this.subscriptionHandle.stop();
|
||||
}
|
||||
this.subscriptionHandle = Meteor.subscribe('dueCards', allUsers);
|
||||
|
||||
// Update loading state based on subscription
|
||||
this.autorun(() => {
|
||||
if (this.subscriptionHandle && this.subscriptionHandle.ready()) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards: subscription ready, loading data...');
|
||||
}
|
||||
this.isLoading.set(false);
|
||||
const cards = this.dueCardsList();
|
||||
this.hasResults.set(cards && cards.length > 0);
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards: subscription not ready, showing loading...');
|
||||
}
|
||||
this.isLoading.set(true);
|
||||
this.hasResults.set(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
const tpl = this;
|
||||
|
||||
onDestroyed() {
|
||||
super.onDestroyed();
|
||||
if (this.subscriptionHandle) {
|
||||
this.subscriptionHandle.stop();
|
||||
}
|
||||
}
|
||||
|
||||
dueCardsView() {
|
||||
// eslint-disable-next-line no-console
|
||||
//console.log('sort:', Utils.dueCardsView());
|
||||
function dueCardsView() {
|
||||
return Utils && Utils.dueCardsView ? Utils.dueCardsView() : 'me';
|
||||
}
|
||||
|
||||
sortByBoard() {
|
||||
return this.dueCardsView() === 'board';
|
||||
}
|
||||
|
||||
hasResults() {
|
||||
return this.hasResults.get();
|
||||
}
|
||||
|
||||
cardsCount() {
|
||||
const cards = this.dueCardsList();
|
||||
return cards ? cards.length : 0;
|
||||
}
|
||||
|
||||
resultsText() {
|
||||
const count = this.cardsCount();
|
||||
if (count === 1) {
|
||||
return TAPi18n.__('one-card-found');
|
||||
} else {
|
||||
// Get the translated text and manually replace %s with the count
|
||||
const baseText = TAPi18n.__('n-cards-found');
|
||||
const result = baseText.replace('%s', count);
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards: base text:', baseText, 'count:', count, 'result:', result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
dueCardsList() {
|
||||
function dueCardsList() {
|
||||
// Check if subscription is ready
|
||||
if (!this.subscriptionHandle || !this.subscriptionHandle.ready()) {
|
||||
if (!tpl.subscriptionHandle || !tpl.subscriptionHandle.ready()) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards client: subscription not ready');
|
||||
}
|
||||
@@ -180,11 +40,11 @@ class DueCardsComponent extends BlazeComponent {
|
||||
}
|
||||
|
||||
// Use cached results if available to avoid expensive re-sorting
|
||||
if (this._cachedCards && this._cachedTimestamp && (Date.now() - this._cachedTimestamp < 5000)) {
|
||||
if (tpl._cachedCards && tpl._cachedTimestamp && (Date.now() - tpl._cachedTimestamp < 5000)) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards client: using cached results,', this._cachedCards.length, 'cards');
|
||||
console.log('dueCards client: using cached results,', tpl._cachedCards.length, 'cards');
|
||||
}
|
||||
return this._cachedCards;
|
||||
return tpl._cachedCards;
|
||||
}
|
||||
|
||||
// Get cards directly from the subscription (already sorted by the publication)
|
||||
@@ -196,10 +56,10 @@ class DueCardsComponent extends BlazeComponent {
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards client: found', cards.length, 'cards with due dates');
|
||||
console.log('dueCards client: cards details:', cards.map(c => ({
|
||||
id: c._id,
|
||||
title: c.title,
|
||||
dueAt: c.dueAt,
|
||||
console.log('dueCards client: cards details:', cards.map(c => ({
|
||||
id: c._id,
|
||||
title: c.title,
|
||||
dueAt: c.dueAt,
|
||||
boardId: c.boardId,
|
||||
members: c.members,
|
||||
assignees: c.assignees,
|
||||
@@ -208,7 +68,7 @@ class DueCardsComponent extends BlazeComponent {
|
||||
}
|
||||
|
||||
// Filter cards based on user view preference
|
||||
const allUsers = this.dueCardsView() === 'all';
|
||||
const allUsers = dueCardsView() === 'all';
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let filteredCards = cards;
|
||||
|
||||
@@ -223,11 +83,11 @@ class DueCardsComponent extends BlazeComponent {
|
||||
const isAssignee = card.assignees && card.assignees.includes(currentUser._id);
|
||||
const isAuthor = card.userId === currentUser._id;
|
||||
const matches = isMember || isAssignee || isAuthor;
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true' && matches) {
|
||||
console.log('dueCards client: card matches user:', card.title, { isMember, isAssignee, isAuthor });
|
||||
}
|
||||
|
||||
|
||||
return matches;
|
||||
});
|
||||
}
|
||||
@@ -255,15 +115,106 @@ class DueCardsComponent extends BlazeComponent {
|
||||
}
|
||||
|
||||
// Cache the results for 5 seconds to avoid re-filtering on every render
|
||||
this._cachedCards = filteredCards;
|
||||
this._cachedTimestamp = Date.now();
|
||||
tpl._cachedCards = filteredCards;
|
||||
tpl._cachedTimestamp = Date.now();
|
||||
|
||||
// Update reactive variables
|
||||
this.hasResults.set(filteredCards && filteredCards.length > 0);
|
||||
this.isLoading.set(false);
|
||||
tpl.hasResults.set(filteredCards && filteredCards.length > 0);
|
||||
tpl.isLoading.set(false);
|
||||
|
||||
return filteredCards;
|
||||
}
|
||||
}
|
||||
|
||||
DueCardsComponent.register('dueCards');
|
||||
// Store dueCardsList on the instance so helpers can call it
|
||||
this.dueCardsList = dueCardsList;
|
||||
this.dueCardsView = dueCardsView;
|
||||
|
||||
// Subscribe to the optimized due cards publication
|
||||
this.autorun(() => {
|
||||
const allUsers = dueCardsView() === 'all';
|
||||
if (tpl.subscriptionHandle) {
|
||||
tpl.subscriptionHandle.stop();
|
||||
}
|
||||
tpl.subscriptionHandle = Meteor.subscribe('dueCards', allUsers);
|
||||
|
||||
// Update loading state based on subscription
|
||||
tpl.autorun(() => {
|
||||
if (tpl.subscriptionHandle && tpl.subscriptionHandle.ready()) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards: subscription ready, loading data...');
|
||||
}
|
||||
tpl.isLoading.set(false);
|
||||
const cards = dueCardsList();
|
||||
tpl.hasResults.set(cards && cards.length > 0);
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards: subscription not ready, showing loading...');
|
||||
}
|
||||
tpl.isLoading.set(true);
|
||||
tpl.hasResults.set(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Template.dueCards.onDestroyed(function () {
|
||||
if (this.subscriptionHandle) {
|
||||
this.subscriptionHandle.stop();
|
||||
}
|
||||
});
|
||||
|
||||
Template.dueCards.helpers({
|
||||
userId() {
|
||||
return Meteor.userId();
|
||||
},
|
||||
// Return ReactiveVar so jade can use .get pattern
|
||||
searching() {
|
||||
return Template.instance().isLoading;
|
||||
},
|
||||
hasResults() {
|
||||
return Template.instance().hasResults;
|
||||
},
|
||||
hasQueryErrors() {
|
||||
return new ReactiveVar(false);
|
||||
},
|
||||
errorMessages() {
|
||||
return [];
|
||||
},
|
||||
dueCardsList() {
|
||||
const tpl = Template.instance();
|
||||
return tpl.dueCardsList ? tpl.dueCardsList() : [];
|
||||
},
|
||||
resultsText() {
|
||||
const tpl = Template.instance();
|
||||
const cards = tpl.dueCardsList ? tpl.dueCardsList() : [];
|
||||
const count = cards ? cards.length : 0;
|
||||
if (count === 1) {
|
||||
return TAPi18n.__('one-card-found');
|
||||
} else {
|
||||
// Get the translated text and manually replace %s with the count
|
||||
const baseText = TAPi18n.__('n-cards-found');
|
||||
const result = baseText.replace('%s', count);
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards: base text:', baseText, 'count:', count, 'result:', result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.dueCardsViewChangePopup.events({
|
||||
'click .js-due-cards-view-me'() {
|
||||
if (Utils && Utils.setDueCardsView) {
|
||||
Utils.setDueCardsView('me');
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
|
||||
'click .js-due-cards-view-all'() {
|
||||
if (Utils && Utils.setDueCardsView) {
|
||||
Utils.setDueCardsView('all');
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
@@ -1,8 +1,6 @@
|
||||
template(name="editor")
|
||||
a(title="{{_ 'convert-to-markdown'}}")
|
||||
| 📝
|
||||
a(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
| 📋
|
||||
a.fa.fa-brands.fa-markdown(title="{{_ 'convert-to-markdown'}}")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
textarea.editor(
|
||||
dir="auto"
|
||||
|
||||
@@ -1,5 +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 = [
|
||||
@@ -19,8 +23,8 @@ const boardSpecialHandles = [
|
||||
const specialHandleNames = specialHandles.map(m => m.username);
|
||||
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
Template.editor.onRendered(function () {
|
||||
const tpl = this;
|
||||
// Start: Copy <pre> code https://github.com/wekan/wekan/issues/5149
|
||||
// TODO: Try to make copyPre visible at Card Details after editing or closing editor or Card Details.
|
||||
// - Also this same TODO below at event, if someone gets it working.
|
||||
@@ -66,7 +70,7 @@ BlazeComponent.extendComponent({
|
||||
})
|
||||
.filter(Boolean);
|
||||
// Order: 1. Users, 2. Card-specific options, 3. Board-wide options
|
||||
callback(_.union(users, cardSpecialHandles, boardSpecialHandles));
|
||||
callback([...new Set([...users, ...cardSpecialHandles, ...boardSpecialHandles])]);
|
||||
},
|
||||
template(user) {
|
||||
if (user.profile && user.profile.fullname) {
|
||||
@@ -89,7 +93,7 @@ BlazeComponent.extendComponent({
|
||||
];
|
||||
|
||||
const enableTextarea = function() {
|
||||
const $textarea = this.$(textareaSelector);
|
||||
const $textarea = tpl.$(textareaSelector);
|
||||
autosize($textarea);
|
||||
$textarea.escapeableTextComplete(mentions);
|
||||
};
|
||||
@@ -206,11 +210,11 @@ BlazeComponent.extendComponent({
|
||||
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,
|
||||
@@ -314,29 +318,25 @@ BlazeComponent.extendComponent({
|
||||
enableTextarea();
|
||||
}
|
||||
enableTextarea();
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click a.fa.fa-copy'(event) {
|
||||
const $editor = this.$('textarea.editor');
|
||||
const promise = Utils.copyTextToClipboard($editor[0].value);
|
||||
});
|
||||
|
||||
const $tooltip = this.$('.copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
'click a.fa.fa-brands.fa-markdown'(event) {
|
||||
const $editor = this.$('textarea.editor');
|
||||
$editor[0].value = converter.convert($editor[0].value);
|
||||
},
|
||||
// TODO: Try to make copyPre visible at Card Details after editing or closing editor or Card Details.
|
||||
//'click .js-close-inlined-form'(event) {
|
||||
// Utils.copyPre();
|
||||
//},
|
||||
}
|
||||
]
|
||||
}
|
||||
}).register('editor');
|
||||
Template.editor.events({
|
||||
'click a.fa.fa-copy'(event, tpl) {
|
||||
const $editor = tpl.$('textarea.editor');
|
||||
const promise = Utils.copyTextToClipboard($editor[0].value);
|
||||
|
||||
const $tooltip = tpl.$('.copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
'click a.fa.fa-brands.fa-markdown'(event, tpl) {
|
||||
const $editor = tpl.$('textarea.editor');
|
||||
$editor[0].value = converter.convert($editor[0].value);
|
||||
},
|
||||
// TODO: Try to make copyPre visible at Card Details after editing or closing editor or Card Details.
|
||||
//'click .js-close-inlined-form'(event) {
|
||||
// Utils.copyPre();
|
||||
//},
|
||||
});
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
import { sanitizeHTML } from '/imports/lib/secureDOMPurify';
|
||||
@@ -387,20 +387,22 @@ Blaze.Template.registerHelper(
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
if (!currentBoard)
|
||||
return HTML.Raw(sanitizeHTML(content));
|
||||
const knowedUsers = _.union(currentBoard.members.map(member => {
|
||||
const u = ReactiveCache.getUser(member.userId);
|
||||
if (u) {
|
||||
member.username = u.username;
|
||||
}
|
||||
return member;
|
||||
}), [...specialHandles]);
|
||||
const knowedUsers = [...new Set([...currentBoard.members
|
||||
.filter(member => member.isActive)
|
||||
.map(member => {
|
||||
const u = ReactiveCache.getUser(member.userId);
|
||||
if (u) {
|
||||
member.username = u.username;
|
||||
}
|
||||
return member;
|
||||
}), ...specialHandles])];
|
||||
const mentionRegex = /\B@([\w.-]*)/gi;
|
||||
|
||||
let currentMention;
|
||||
while ((currentMention = mentionRegex.exec(content)) !== null) {
|
||||
const [fullMention, quoteduser, simple] = currentMention;
|
||||
const username = quoteduser || simple;
|
||||
const knowedUser = _.findWhere(knowedUsers, { username });
|
||||
const knowedUser = findWhere(knowedUsers, { username });
|
||||
if (!knowedUser) {
|
||||
continue;
|
||||
}
|
||||
@@ -410,14 +412,14 @@ Blaze.Template.registerHelper(
|
||||
if (knowedUser.userId === Meteor.userId()) {
|
||||
linkClass += ' me';
|
||||
}
|
||||
|
||||
|
||||
// For special group mentions, display translated text
|
||||
let displayText = knowedUser.username;
|
||||
if (specialHandleNames.includes(knowedUser.username)) {
|
||||
displayText = TAPi18n.__(knowedUser.username);
|
||||
linkClass = 'atMention'; // Remove js-open-member for special handles
|
||||
}
|
||||
|
||||
|
||||
// This @user mention link generation did open same Wekan
|
||||
// window in new tab, so now A is changed to U so it's
|
||||
// underlined and there is no link popup. This way also
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,78 +1,57 @@
|
||||
template(name="globalSearchHeaderBar")
|
||||
if currentUser
|
||||
h1
|
||||
| 🔍
|
||||
i.fa.fa-search
|
||||
| {{_ 'globalSearch-title'}}
|
||||
|
||||
template(name="globalSearchModalTitle")
|
||||
if currentUser
|
||||
h2
|
||||
| ⌨️
|
||||
i.fa.fa-keyboard-o
|
||||
| {{_ '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
|
||||
|
||||
Loaded 100 of 1086 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user