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 | ||
|
|
997b9a7c76 | ||
|
|
bfebee5949 | ||
|
|
bd899b2bf2 | ||
|
|
ad511bd137 | ||
|
|
603a65ef17 | ||
|
|
2fa490d83d | ||
|
|
a787bcddf3 | ||
|
|
91a936e07d | ||
|
|
4ce181d172 | ||
|
|
0f5a9c3877 | ||
|
|
545566f566 | ||
|
|
8c0b4f79d8 | ||
|
|
c413a7e860 | ||
|
|
053bf1dfb7 | ||
|
|
146905a459 | ||
|
|
55576ec177 | ||
|
|
cc35dafef5 | ||
|
|
251d49eea9 | ||
|
|
cabfeed9a6 | ||
|
|
b4b90b1c41 | ||
|
|
d337afd5d3 | ||
|
|
4473ca09f6 | ||
|
|
d30192f7f9 | ||
|
|
7291617a77 | ||
|
|
95da8966fe | ||
|
|
db7ffc98fa | ||
|
|
cb43a41b0c | ||
|
|
a7e29b92bf | ||
|
|
4899330b6e | ||
|
|
c56bc0be7f | ||
|
|
fd8ce39990 | ||
|
|
f9ed63ac76 | ||
|
|
641d4c2f7f | ||
|
|
980510d71a | ||
|
|
07001a229d | ||
|
|
e2ec50730f | ||
|
|
20884cf6c7 | ||
|
|
f0118d52e9 | ||
|
|
d4e922eed4 | ||
|
|
b88b27689a | ||
|
|
a29733be9c | ||
|
|
33e0a82967 | ||
|
|
13b5b170b6 | ||
|
|
e2f2090da1 | ||
|
|
eabb6a239d | ||
|
|
2f59e42024 | ||
|
|
20b5e2ab8f | ||
|
|
0d5dd3082c | ||
|
|
5cb712bee4 | ||
|
|
00793c39f8 | ||
|
|
e37220cde6 | ||
|
|
dd20988b1c | ||
|
|
32785376d8 | ||
|
|
4a39bcc6ee | ||
|
|
984a2dcec1 | ||
|
|
c8bdb95b38 | ||
|
|
e89f4d260c | ||
|
|
eca42f32bb | ||
|
|
6ad9770bb6 | ||
|
|
5b47340b76 | ||
|
|
69f54dddaf | ||
|
|
b3cb47ac47 | ||
|
|
69e2ad1007 | ||
|
|
40784b2054 | ||
|
|
ab49e09da6 | ||
|
|
6ed8e54dac | ||
|
|
c80d1aae4c | ||
|
|
a62b3c6ce0 | ||
|
|
8a0568ce01 | ||
|
|
c740474145 | ||
|
|
4a373b45e1 | ||
|
|
32733023ca | ||
|
|
0ffb0fa24f | ||
|
|
b5177e3870 | ||
|
|
c326e58d5e | ||
|
|
4d4046986f | ||
|
|
36cbd3a606 | ||
|
|
715d47dd2c | ||
|
|
71d84f58a4 | ||
|
|
306305a95c | ||
|
|
2bc9fa1629 | ||
|
|
fd45ae2a62 | ||
|
|
46100cfd1d | ||
|
|
1d62322b3c | ||
|
|
d784d2b633 | ||
|
|
07186e12a9 | ||
|
|
0635a663f0 | ||
|
|
dcd8d80b9d | ||
|
|
58ae2b6c68 | ||
|
|
c76a569fb0 | ||
|
|
66e79d2df7 | ||
|
|
fc06ae1000 | ||
|
|
dac7e17500 | ||
|
|
8f51fcd65a | ||
|
|
a4f8faa48e | ||
|
|
b5ab917dfb | ||
|
|
376a30f8a9 | ||
|
|
9ebf4d2426 | ||
|
|
42c14faf41 | ||
|
|
4a658dc02a | ||
|
|
f250cba405 | ||
|
|
cf6e691498 | ||
|
|
5b2e6511b0 | ||
|
|
52a9514536 | ||
|
|
ffd37b9fd9 | ||
|
|
e2b80652a5 | ||
|
|
bd37b88e4d | ||
|
|
a0adc54a08 | ||
|
|
9431b2d530 | ||
|
|
594ef40f2e | ||
|
|
aacdcea37b | ||
|
|
309a456a0d | ||
|
|
f198421f10 | ||
|
|
fa0460b1f0 | ||
|
|
9fb1aeb827 | ||
|
|
59ad67c08c | ||
|
|
ec57618c98 | ||
|
|
3b378961b0 | ||
|
|
cbb1cd78de | ||
|
|
d6834d0287 | ||
|
|
a9a89b501a | ||
|
|
7de4385160 | ||
|
|
7ac5a2bba5 | ||
|
|
e0af5db1a9 | ||
|
|
87cd07dec7 | ||
|
|
e80f8e5121 | ||
|
|
2325a5c532 | ||
|
|
b1db262b37 | ||
|
|
2e564bd076 | ||
|
|
e09e9114aa | ||
|
|
efd91a8f72 | ||
|
|
fbfde81bc8 | ||
|
|
cf62807ad5 | ||
|
|
2d87ba18b3 | ||
|
|
74f1dfde72 | ||
|
|
db4b04d837 | ||
|
|
0b0e16c3ea | ||
|
|
1d16955b6d | ||
|
|
6dfa3beb2b | ||
|
|
5cd875813f | ||
|
|
08a6f084eb | ||
|
|
181f837d8c | ||
|
|
198509e760 | ||
|
|
67cb47173c | ||
|
|
7ed76c180e | ||
|
|
f244a43771 | ||
|
|
48e856fca2 | ||
|
|
7d83cb3d0b | ||
|
|
3af3c9a89d | ||
|
|
2d3bef9033 | ||
|
|
ce9afbcaca | ||
|
|
ba79d5389d | ||
|
|
2f4c40c1db | ||
|
|
6c9e8a4aa8 | ||
|
|
223c38c50d | ||
|
|
c1981dee9b | ||
|
|
45b3373145 | ||
|
|
54a566cfb1 | ||
|
|
5a51c2940a | ||
|
|
7232070bd1 | ||
|
|
33ab8920ca | ||
|
|
db4d47cc52 | ||
|
|
e210c9973b | ||
|
|
98d05ce545 | ||
|
|
9268d80974 | ||
|
|
1a221a6967 | ||
|
|
a039bb1066 | ||
|
|
90a7a61904 | ||
|
|
58e970d685 | ||
|
|
edf0f3c05b | ||
|
|
5bbf5e0dac | ||
|
|
414b8dbf41 | ||
|
|
2e0e1e56b5 | ||
|
|
546dcdf1de | ||
|
|
58f4884ad6 | ||
|
|
95d1625a9f | ||
|
|
05fbdd559f | ||
|
|
300b653ea3 | ||
|
|
1808ea7384 | ||
|
|
4408eae158 | ||
|
|
a68993d099 | ||
|
|
ecfb0f0fdf | ||
|
|
fc3bb962f7 | ||
|
|
23630a4c67 | ||
|
|
b0a7fea724 | ||
|
|
47bbd5b4a8 | ||
|
|
a7400dca45 | ||
|
|
4227377449 | ||
|
|
c1168d181b | ||
|
|
21fb8e9164 | ||
|
|
e569c2957e | ||
|
|
4292302c3c | ||
|
|
f34e4c0e36 | ||
|
|
1790918006 | ||
|
|
400eb81206 | ||
|
|
e07e461e48 | ||
|
|
451e9f7870 | ||
|
|
3c57840340 | ||
|
|
2cbf64da33 | ||
|
|
3ef0a3e685 | ||
|
|
9ebdc82d46 | ||
|
|
fe4b36b85d | ||
|
|
b7ff370561 | ||
|
|
5686c92e05 | ||
|
|
784c5c6b0c | ||
|
|
30541260f0 | ||
|
|
59fc756a0b | ||
|
|
4e346c0ab7 | ||
|
|
19fa12bb26 | ||
|
|
e669b1b9c7 | ||
|
|
f24e4a4ce3 | ||
|
|
8b5430acb8 | ||
|
|
614cb44b55 | ||
|
|
0ce8e8b74d | ||
|
|
4ea53af76e | ||
|
|
016f17d663 | ||
|
|
07f69950a7 | ||
|
|
cec625607d | ||
|
|
a290c7b34b | ||
|
|
5b77ac1b44 | ||
|
|
41c635afb5 | ||
|
|
adbf729cb2 | ||
|
|
88ea716d63 | ||
|
|
003a07ebce | ||
|
|
d3c237bc66 | ||
|
|
bac0fa81fc | ||
|
|
a42915614a | ||
|
|
5ff9bf331f | ||
|
|
36d7b0f8a7 | ||
|
|
67c8a98f20 | ||
|
|
a81a603031 | ||
|
|
e30ce78053 | ||
|
|
3d70de94c6 | ||
|
|
1b6e8797ec | ||
|
|
70975c2944 | ||
|
|
960e2126b4 | ||
|
|
3db1305e58 | ||
|
|
f16780b5e3 | ||
|
|
37a3065f3c | ||
|
|
7ff1649d89 | ||
|
|
a39ae31b45 | ||
|
|
6302a48221 | ||
|
|
c277bee9d2 | ||
|
|
c5f5ce126d | ||
|
|
0004ae716b | ||
|
|
7f53dfac3c | ||
|
|
18003900c2 | ||
|
|
fe104791b5 | ||
|
|
6244657ca5 | ||
|
|
46866dac85 | ||
|
|
c829c073cf | ||
|
|
0772ca4036 | ||
|
|
581733d605 | ||
|
|
b02af27ac3 | ||
|
|
20af0a2ef5 | ||
|
|
c58ab5b07d | ||
|
|
e5e711c938 | ||
|
|
42594abe4e | ||
|
|
0afbdc95b4 | ||
|
|
16a74bb748 | ||
|
|
8711b476be | ||
|
|
df9fba4765 | ||
|
|
7d27139aa9 | ||
|
|
e4638d5fbc | ||
|
|
bc5854dd29 | ||
|
|
ba49d4d140 | ||
|
|
71b7dcffb5 | ||
|
|
7713e613b4 | ||
|
|
91a0aa7387 | ||
|
|
fbd6b920ef | ||
|
|
1b25d1d572 | ||
|
|
e93e72234c | ||
|
|
15d9b0ae3a | ||
|
|
550d87ac6c | ||
|
|
f8e576e890 | ||
|
|
fb8ef4d978 | ||
|
|
5127e87898 | ||
|
|
3f2d4444e4 | ||
|
|
9c7badb0eb | ||
|
|
9d9f77a731 | ||
|
|
c400ce74b1 | ||
|
|
c2e20ee4a3 | ||
|
|
ccd9034339 | ||
|
|
0a1a075f31 | ||
|
|
4aaeec9515 | ||
|
|
ea310d7508 | ||
|
|
0a2e6a0c38 | ||
|
|
f26d582018 | ||
|
|
e9a727301d | ||
|
|
d64d2f9c42 | ||
|
|
5c0d122e84 | ||
|
|
5079c853a7 | ||
|
|
b039ba12a2 | ||
|
|
3323ac6ac1 | ||
|
|
3204311ac1 | ||
|
|
0fc2ad97cd | ||
|
|
30620d0ca4 | ||
|
|
bccc22c5fe | ||
|
|
ecf2418347 | ||
|
|
0c99cb3103 | ||
|
|
034dc08269 | ||
|
|
d1a51b42f6 | ||
|
|
92bfbb2d0c | ||
|
|
91b846e2cd | ||
|
|
7fe7fb4c15 | ||
|
|
0cebd8aa4d | ||
|
|
8662c96d1c | ||
|
|
0cbc9402f3 | ||
|
|
940df02456 | ||
|
|
b4b598f542 | ||
|
|
ef19c35b5a | ||
|
|
fc98120269 | ||
|
|
b8a3d6deaf | ||
|
|
45537ede87 | ||
|
|
29a9c5bc7b | ||
|
|
7ca81285b1 | ||
|
|
49a865cdbf | ||
|
|
a0c30c35ed | ||
|
|
de20424885 | ||
|
|
f7e09ae89c | ||
|
|
c6d4600683 | ||
|
|
bd1837ee36 | ||
|
|
544b24ceb1 | ||
|
|
0825374183 | ||
|
|
b053fb8e61 | ||
|
|
ae11e80bde | ||
|
|
8e296231ba | ||
|
|
49891eff36 | ||
|
|
58df525b49 | ||
|
|
1761f43afa | ||
|
|
37d7d938c5 | ||
|
|
b7ca2310b2 | ||
|
|
c562b3969a | ||
|
|
d1d553e8d7 | ||
|
|
b6e7b258e0 | ||
|
|
c7bbe47221 | ||
|
|
347fa9e5cd | ||
|
|
07ce151508 | ||
|
|
665c9b5e52 | ||
|
|
9399a0c545 | ||
|
|
a540b12895 | ||
|
|
e29d9dcd17 | ||
|
|
1aa0d84977 | ||
|
|
7f31d7c812 | ||
|
|
4987a95d8e | ||
|
|
ef7771febb | ||
|
|
12cba0e148 | ||
|
|
c3a4052227 | ||
|
|
82f048ccef | ||
|
|
7a585a3dfb | ||
|
|
8d3b53f51d | ||
|
|
d73e006935 | ||
|
|
9536e60bd1 | ||
|
|
678ca978a3 | ||
|
|
39420877fd | ||
|
|
6ea03cfba3 | ||
|
|
9214b56aea | ||
|
|
699b4c464f | ||
|
|
9fa54a3148 | ||
|
|
f2019b1059 | ||
|
|
714bbd0fb0 | ||
|
|
80777b4663 | ||
|
|
9473c1fe41 | ||
|
|
98f141d62f | ||
|
|
85dd213b14 | ||
|
|
3cf00911f7 | ||
|
|
bddaad8346 | ||
|
|
5df4efd7ba | ||
|
|
59df6aad05 | ||
|
|
c4af4d03ac | ||
|
|
62679819d9 | ||
|
|
46d46e313c | ||
|
|
27e9d3ce47 | ||
|
|
b6b0c5fe6d | ||
|
|
87b934a955 | ||
|
|
8cc6e9b812 | ||
|
|
b7da17ff31 | ||
|
|
2dd3916f7e | ||
|
|
516552cce6 | ||
|
|
2d44881619 | ||
|
|
0acbf30b03 | ||
|
|
e61f6b1c89 | ||
|
|
973a49526f | ||
|
|
e1902d58c1 | ||
|
|
1e53125499 | ||
|
|
91fb7d9e70 | ||
|
|
eb6b42c4c9 | ||
|
|
679d210667 | ||
|
|
1e6252de7f | ||
|
|
48b645ee1e | ||
|
|
951d2e4937 | ||
|
|
1658883b78 | ||
|
|
3514335247 | ||
|
|
55bec31a3f | ||
|
|
0d36abee4e | ||
|
|
caa6e615ff | ||
|
|
cd3576b995 | ||
|
|
324f3f7794 | ||
|
|
3257110673 | ||
|
|
66b444e2b0 | ||
|
|
23860b1ee8 | ||
|
|
101048339b | ||
|
|
dc78e3b7a0 | ||
|
|
d965faa317 | ||
|
|
5d2bfab0f5 | ||
|
|
841a6eaf8c | ||
|
|
db59bb4aa4 | ||
|
|
61f7099106 | ||
|
|
ef828bdd38 | ||
|
|
1134b45b05 | ||
|
|
b06daff4c7 | ||
|
|
cea414b589 | ||
|
|
b8942b728f | ||
|
|
8e6eabd9e8 | ||
|
|
290dd6c4d1 | ||
|
|
088bc16072 | ||
|
|
daad2fbd71 | ||
|
|
3af94c2a90 | ||
|
|
a3ca76d3c4 | ||
|
|
62ede48196 | ||
|
|
390a86a7a7 | ||
|
|
09631d6b0c | ||
|
|
2947238a02 | ||
|
|
a7af4b4809 | ||
|
|
cb6afe67a7 | ||
|
|
8c5b43295d | ||
|
|
79b94824ef | ||
|
|
33e4b046e8 | ||
|
|
386aea7c78 | ||
|
|
4a7bccd983 | ||
|
|
1f0cae9e76 | ||
|
|
87ae085e6d | ||
|
|
640ac2330f | ||
|
|
2543df9425 | ||
|
|
915ab47a72 | ||
|
|
09ff287da2 | ||
|
|
2896180f80 | ||
|
|
4283b5b0e3 | ||
|
|
bbbd3abf06 | ||
|
|
dd88483ec7 | ||
|
|
00ddec7575 | ||
|
|
ab0ebab240 | ||
|
|
79e83e33ec | ||
|
|
aa402d652d | ||
|
|
690481c138 | ||
|
|
881125aa98 | ||
|
|
77eea4d494 | ||
|
|
b26e16abb8 | ||
|
|
f08c7702ee | ||
|
|
a4399c7ef4 | ||
|
|
d6e50ed9a0 | ||
|
|
6b848b318d | ||
|
|
70ce70cf0e | ||
|
|
5a79bc5ee3 | ||
|
|
5792a86959 |
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,
|
||||
"BlazeLayout": false,
|
||||
|
||||
"CollectionHooks": false,
|
||||
"DocHead": false,
|
||||
"ESSearchResults": false,
|
||||
"FastRender": false,
|
||||
"FlowRouter": false,
|
||||
"FS": false,
|
||||
"getSlug": false,
|
||||
"Migrations": false,
|
||||
"moment": false,
|
||||
"Mousetrap": false,
|
||||
"Picker": false,
|
||||
"Presence": true,
|
||||
"presences": true,
|
||||
"Ps": true,
|
||||
"ReactiveTabs": false,
|
||||
"Restivus": false,
|
||||
"SimpleSchema": false,
|
||||
"SubsManager": false,
|
||||
"T9n": false,
|
||||
"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,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: wekan
|
||||
custom: ['https://wekan.fi/commercial-support/']
|
||||
@@ -1,56 +0,0 @@
|
||||
## Issue
|
||||
|
||||
UPGRADE: https://wekan.fi/upgrade/
|
||||
|
||||
Pull requests welcome to fix any broken links at docs directory, and organizing docs/Features and their screenshots to subdirectories of each feature.
|
||||
|
||||
Please report these issues elsewhere:
|
||||
|
||||
- SECURITY ISSUES, PGP EMAIL: https://github.com/wekan/wekan/blob/main/SECURITY.md
|
||||
- UCS: https://github.com/wekan/univention/issues
|
||||
|
||||
If WeKan Snap is slow, try this: https://github.com/wekan/wekan/wiki/Cron
|
||||
|
||||
Please search existing Open and Closed issues, most questions have already been answered.
|
||||
|
||||
If you can not login for any reason: https://github.com/wekan/wekan/wiki/Forgot-Password
|
||||
Email settings, only SMTP MAIL_URL and MAIL_FROM are in use:
|
||||
https://github.com/wekan/wekan/wiki/Troubleshooting-Mail
|
||||
|
||||
### Server Setup Information
|
||||
|
||||
Please anonymize info, and do not any of your Wekan board URLs, passwords,
|
||||
API tokens etc to this public issue.
|
||||
|
||||
* Did you test in newest Wekan?:
|
||||
* Did you configure root-url correctly so Wekan cards open correctly (see https://github.com/wekan/wekan/wiki/Settings)?
|
||||
* Operating System:
|
||||
* Deployment Method (Snap/Docker/Sandstorm/bundle/source):
|
||||
* Http frontend if any (Caddy, Nginx, Apache, see config examples from Wekan GitHub wiki first):
|
||||
* Node.js Version:
|
||||
* MongoDB Version:
|
||||
* What webbrowser version are you using (Wekan should work on all modern browsers that support Javascript)?
|
||||
|
||||
### Problem description
|
||||
|
||||
Add a recorded animated gif (e.g. with https://github.com/phw/peek) about
|
||||
how it works currently, and screenshot mockups how it should work.
|
||||
|
||||
|
||||
#### Reproduction Steps
|
||||
|
||||
|
||||
|
||||
#### Logs
|
||||
|
||||
Check Right Click / Inspect / Console in you browser - generally Chromium
|
||||
based browsers show more detailed info than Firefox based browsers.
|
||||
|
||||
Please anonymize logs.
|
||||
|
||||
Snap: sudo snap logs wekan.wekan
|
||||
|
||||
Docker: sudo docker logs wekan-app
|
||||
|
||||
If logs are very long, attach them in .zip file
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
name: 🐛 Bug Report
|
||||
description: Report a bug to help improve WeKan
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for reporting a bug! Please ensure you are using the [latest version of WeKan](https://wekan.fi/install) and [Upgrade](https://wekan.fi/upgrade) before submitting.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Bug Description
|
||||
description: A clear and concise description of what the bug is. Mention versions of WeKan, Node.js, database name and version, frontend webserver version like Caddy etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: How can we recreate this issue?
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant Logs
|
||||
description: |
|
||||
- Please paste any relevant anonymized server logs or browser console errors here.
|
||||
- Snap: sudo snap logs wekan.wekan
|
||||
- Docker: sudo docker logs wekan-app
|
||||
- If logs are very long, attach them in .zip file
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context, anonymized screenshots or GIF video about the bug, and screenshot mockups about how it should work.
|
||||
@@ -0,0 +1,31 @@
|
||||
name: ✨ Feature Request
|
||||
description: Suggest a new feature for WeKan
|
||||
labels: ["Feature:Request"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Problem Statement
|
||||
description: Is your feature request related to a problem? Please describe.
|
||||
placeholder: I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: Any alternative solutions or features you've considered.
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context, like anonymized screenshot mockups about how it should work.
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
name: 🛡️ Security Issue
|
||||
description: Report a security vulnerability
|
||||
labels: ["security", "critical"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## ⚠️ IMPORTANT: Please do not report security vulnerabilities via public issues.
|
||||
|
||||
To protect the WeKan community, we ask that you report security bugs privately. This allows us to fix the issue before it can be exploited by malicious actors.
|
||||
|
||||
### How to report:
|
||||
Please read our **[Security Policy (SECURITY.md)](https://github.com/wekan/wekan/blob/main/SECURITY.md)** for the official reporting process and contact information.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
name: 🗳️ Univention (UCS) Issue
|
||||
description: Problems specifically related to the WeKan app on Univention Corporate Server
|
||||
labels: ["UCS"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 🛑 Is this a UCS-specific issue?
|
||||
|
||||
If your issue is related to the **Univention Corporate Server (UCS) integration**, packaging, or installation via the Univention App Center, it should be reported in the dedicated Univention repository.
|
||||
|
||||
### ➡️ [Report UCS Issues Here](https://github.com/wekan/univention/issues)
|
||||
|
||||
---
|
||||
**Why?**
|
||||
Reporting there ensures that the maintainers specifically focused on the UCS environment see your request.
|
||||
|
||||
If you are certain this is a **core WeKan bug** that affects all platforms (Docker, Snap, etc.), please go back and use the standard [Bug Report](https://github.com/wekan/wekan/issues/new?template=bug-report.yml) template.
|
||||
- type: textarea
|
||||
id: ucs-details
|
||||
attributes:
|
||||
label: Brief Description (Optional)
|
||||
description: If you still wish to post here, please provide a brief summary of why this is a core Wekan issue and not a UCS-specific integration bug.
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
@@ -32,13 +32,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
|
||||
uses: docker/login-action@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@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
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' }}
|
||||
|
||||
@@ -15,6 +15,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Build the Docker image
|
||||
run: docker build . --file Dockerfile --tag wekan:$(date +%s)
|
||||
@@ -0,0 +1,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"
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v5
|
||||
# uses: actions/checkout@v6
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
# needs: [lintcode]
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v5
|
||||
# uses: actions/checkout@v6
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
# needs: [lintcode,lintstyle]
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v5
|
||||
# uses: actions/checkout@v6
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
@@ -90,12 +90,12 @@ jobs:
|
||||
|
||||
# CHECKOUTS
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# CACHING
|
||||
- name: Install Meteor
|
||||
id: cache-meteor-install
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.meteor
|
||||
key: v1-meteor-${{ hashFiles('.meteor/versions') }}
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
|
||||
- name: Cache NPM dependencies
|
||||
id: cache-meteor-npm
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: v1-npm-${{ hashFiles('package-lock.json') }}
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
|
||||
- name: Cache Meteor build
|
||||
id: cache-meteor-build
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
.meteor/local/resolver-result-cache.json
|
||||
@@ -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@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage-folder
|
||||
path: .coverage/
|
||||
@@ -147,10 +152,10 @@ jobs:
|
||||
needs: [tests]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download coverage
|
||||
uses: actions/download-artifact@v5
|
||||
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
|
||||
+40
-67
@@ -3,32 +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
|
||||
easy:search
|
||||
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:flow-routing
|
||||
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
|
||||
@@ -37,60 +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
|
||||
arillo:flow-router-helpers
|
||||
audit-argument-checks@1.0.7
|
||||
kadira:dochead
|
||||
mquandalle:autofocus
|
||||
ongoworks:speakingurl
|
||||
raix:handlebar-helpers
|
||||
http@2.0.0! # force new http package
|
||||
|
||||
# Datepicker
|
||||
wekan-bootstrap-datepicker
|
||||
check@1.5.0
|
||||
random@1.2.2
|
||||
reactive-dict@1.3.2
|
||||
session@1.2.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
|
||||
pascoual:pdfkit
|
||||
lmieulet:meteor-coverage
|
||||
meteortesting:mocha@2.0.3
|
||||
aldeed:simple-schema
|
||||
ostrio:files
|
||||
# lmieulet:meteor-coverage
|
||||
meteortesting:mocha
|
||||
matb33:collection-hooks
|
||||
simple:json-routes
|
||||
kadira:flow-router
|
||||
spacebars
|
||||
service-configuration@1.3.2
|
||||
communitypackages:picker
|
||||
minifier-css@1.6.4
|
||||
blaze
|
||||
kadira:blaze-layout
|
||||
peerlibrary:blaze-components
|
||||
ejson@1.1.3
|
||||
logging@1.3.3
|
||||
service-configuration@1.3.5
|
||||
minifier-css@2.0.1
|
||||
ejson@1.2.0-beta350.7
|
||||
logging@1.3.6
|
||||
wekan-fullcalendar
|
||||
momentjs:moment@2.29.3
|
||||
wekan-fontawesome
|
||||
+1
-1
@@ -1 +1 @@
|
||||
METEOR@2.14
|
||||
METEOR@3.5-beta.7
|
||||
+113
-158
@@ -1,166 +1,121 @@
|
||||
accounts-base@2.2.10
|
||||
accounts-oauth@1.4.3
|
||||
accounts-password@2.4.0
|
||||
aldeed:collection2@2.10.0
|
||||
aldeed:collection2-core@1.2.0
|
||||
aldeed:schema-deny@1.1.0
|
||||
aldeed:schema-index@1.1.1
|
||||
aldeed:simple-schema@1.5.4
|
||||
allow-deny@1.1.1
|
||||
arillo:flow-router-helpers@0.5.2
|
||||
audit-argument-checks@1.0.7
|
||||
autoupdate@1.8.0
|
||||
babel-compiler@7.10.5
|
||||
babel-runtime@1.5.1
|
||||
base64@1.0.12
|
||||
binary-heap@1.0.11
|
||||
blaze@2.7.1
|
||||
blaze-tools@1.1.3
|
||||
boilerplate-generator@1.7.2
|
||||
caching-compiler@1.2.2
|
||||
caching-html-compiler@1.2.1
|
||||
callback-hook@1.5.1
|
||||
check@1.3.2
|
||||
coffeescript@2.7.0
|
||||
coffeescript-compiler@2.4.1
|
||||
communitypackages:picker@1.1.1
|
||||
cottz:publish-relations@2.0.8
|
||||
dburles:collection-helpers@1.1.0
|
||||
ddp@1.4.1
|
||||
ddp-client@2.6.1
|
||||
ddp-common@1.4.0
|
||||
ddp-rate-limiter@1.2.1
|
||||
ddp-server@2.7.0
|
||||
deps@1.0.12
|
||||
diff-sequence@1.1.2
|
||||
dynamic-import@0.7.3
|
||||
easy:search@2.2.1
|
||||
easysearch:components@2.2.2
|
||||
easysearch:core@2.2.2
|
||||
ecmascript@0.16.8
|
||||
ecmascript-runtime@0.8.1
|
||||
ecmascript-runtime-client@0.12.1
|
||||
ecmascript-runtime-server@0.11.0
|
||||
ejson@1.1.3
|
||||
email@2.2.5
|
||||
es5-shim@4.8.0
|
||||
fetch@0.1.4
|
||||
geojson-utils@1.0.11
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.1.3
|
||||
htmljs@1.1.1
|
||||
http@2.0.0
|
||||
id-map@1.1.1
|
||||
idmontie:migrations@1.0.3
|
||||
inter-process-messaging@0.1.1
|
||||
jquery@3.0.0
|
||||
kadira:blaze-layout@2.3.0
|
||||
kadira:dochead@1.5.0
|
||||
kadira:flow-router@2.12.1
|
||||
konecty:mongo-counter@0.0.5_3
|
||||
lmieulet:meteor-coverage@1.1.4
|
||||
localstorage@1.2.0
|
||||
logging@1.3.3
|
||||
matb33:collection-hooks@1.3.0
|
||||
mdg:validation-error@0.5.1
|
||||
meteor@1.11.5
|
||||
meteor-autosize@5.0.1
|
||||
meteor-base@1.5.1
|
||||
meteorhacks:aggregate@1.3.0
|
||||
meteorhacks:collection-utils@1.2.0
|
||||
meteorhacks:picker@1.0.3
|
||||
meteorhacks:subs-manager@1.6.4
|
||||
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.7.5
|
||||
minifiers@1.1.8-faster-rebuild.0
|
||||
minimongo@1.9.3
|
||||
modern-browsers@0.1.10
|
||||
modules@0.20.0
|
||||
modules-runtime@0.13.1
|
||||
momentjs:moment@2.29.3
|
||||
mongo@1.16.8
|
||||
mongo-decimal@0.1.3
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.8
|
||||
mongo-livedata@1.0.12
|
||||
mousetrap:mousetrap@1.4.6_1
|
||||
mquandalle:autofocus@1.0.0
|
||||
mquandalle:collection-mutations@0.1.0
|
||||
mquandalle:jade@0.4.9
|
||||
mquandalle:jade-compiler@0.4.5
|
||||
mquandalle:jquery-textcomplete@0.8.0_1
|
||||
mquandalle:mousetrap-bindglobal@0.0.1
|
||||
msavin:usercache@1.8.0
|
||||
npm-mongo@4.17.2
|
||||
oauth@2.2.1
|
||||
oauth2@1.3.2
|
||||
observe-sequence@1.0.21
|
||||
ongoworks:speakingurl@1.1.0
|
||||
ordered-dict@1.1.0
|
||||
ostrio:cookies@2.7.2
|
||||
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:files@3.0.1
|
||||
ostrio:flow-router-extra@3.12.1
|
||||
ostrio:i18n@3.2.1
|
||||
pascoual:pdfkit@1.0.7
|
||||
peerlibrary:assert@0.3.0
|
||||
peerlibrary:base-component@0.17.1
|
||||
peerlibrary:blaze-components@0.23.0
|
||||
peerlibrary:computed-field@0.10.0
|
||||
peerlibrary:data-lookup@0.3.0
|
||||
peerlibrary:reactive-field@0.6.0
|
||||
percolate:synced-cron@1.5.2
|
||||
promise@0.12.2
|
||||
raix:eventemitter@0.1.3
|
||||
raix:handlebar-helpers@0.2.5
|
||||
random@1.2.1
|
||||
rate-limit@1.1.1
|
||||
react-fast-refresh@0.2.8
|
||||
reactive-dict@1.3.1
|
||||
reactive-var@1.0.12
|
||||
reload@1.3.1
|
||||
retry@1.1.0
|
||||
routepolicy@1.1.1
|
||||
service-configuration@1.3.3
|
||||
session@1.2.1
|
||||
sha@1.0.9
|
||||
shell-server@0.5.0
|
||||
simple:authenticate-user-by-token@1.2.1
|
||||
simple:json-routes@2.3.1
|
||||
simple:rest-accounts-password@1.2.2
|
||||
simple:rest-bearer-token-parser@1.1.1
|
||||
simple:rest-json-error-handler@1.1.1
|
||||
socket-stream-client@0.5.2
|
||||
spacebars@1.4.1
|
||||
spacebars-compiler@1.3.1
|
||||
standard-minifier-js@2.8.1
|
||||
templates:tabs@2.3.0
|
||||
templating@1.4.1
|
||||
templating-compiler@1.4.1
|
||||
templating-runtime@1.5.0
|
||||
templating-tools@1.2.2
|
||||
tracker@1.3.3
|
||||
typescript@4.9.5
|
||||
ui@1.0.13
|
||||
underscore@1.0.13
|
||||
url@1.3.2
|
||||
useraccounts:core@1.16.2
|
||||
useraccounts:flow-routing@1.15.0
|
||||
useraccounts:unstyled@1.14.2
|
||||
webapp@1.13.6
|
||||
webapp-hashing@1.1.1
|
||||
wekan-accounts-cas@0.1.0
|
||||
wekan-accounts-lockout@1.0.0
|
||||
promise@1.0.0
|
||||
quave:synced-cron@2.4.0
|
||||
raix:eventemitter@1.0.0
|
||||
random@1.2.2
|
||||
rate-limit@1.2.0-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-bootstrap-datepicker@1.10.0
|
||||
wekan-accounts-sandstorm@0.9.0
|
||||
wekan-fontawesome@6.4.2
|
||||
wekan-fullcalendar@3.10.5
|
||||
wekan-ldap@0.0.2
|
||||
wekan-fullcalendar@5.11.5
|
||||
wekan-ldap@0.1.0
|
||||
wekan-markdown@1.0.9
|
||||
wekan-oidc@1.0.12
|
||||
yasaricli:slugify@0.0.7
|
||||
zimme:active-route@2.3.2
|
||||
zodern:types@1.0.10
|
||||
wekan-oidc@1.1.0
|
||||
zodern:types@1.0.13
|
||||
@@ -0,0 +1 @@
|
||||
npm-packages/
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = te_IN: te-IN, es_AR: es-AR, es_419: es-LA, es_TX: es-TX, he_IL: he-IL, zh_CN: zh-CN, ar_EG: ar-EG, cs_CZ: cs-CZ, fa_IR: fa-IR, ms_MY: ms-MY, nl_NL: nl-NL, de_CH: de-CH, en_IT: en-IT, uz_UZ: uz-UZ, fr_CH: fr-CH, hi_IN: hi-IN, et_EE: et-EE, es_PE: es-PE, es_MX: es-MX, gl_ES: gl-ES, mn_MN: mn, sl_SI: sl, zh_TW: zh-TW, ast_ES: ast-ES, es_CL: es-CL, ja_JP: ja, lv_LV: lv, ro_RO: ro-RO, az_AZ: az-AZ, cy_GB: cy-GB, gu_IN: gu-IN, pl_PL: pl-PL, vep: ve-PP, en_BR: en-BR, en@ysv: en-YS, hu_HU: hu, ko_KR: ko-KR, pt_BR: pt-BR, zh_HK: zh-HK, zu_ZA: zu-ZA, en_MY: en-MY, ja-Hira: ja-HI, fi_FI: fi, vec: ve-CC, vi_VN: vi-VN, fr_FR: fr-FR, id_ID: id, zh_Hans: zh-Hans, en_DE: en-DE, en_GB: en-GB, el_GR: el-GR, uk_UA: uk-UA, az@latin: az-LA, de_AT: de-AT, uz@Latn: uz-LA, vls: vl-SS, ar_DZ: ar-DZ, bg_BG: bg, es_PY: es-PY, fy_NL: fy-NL, uz@Arab: uz-AR, ru_UA: ru-UA, war: wa-RR, zh_CN.GB2312: zh-GB
|
||||
lang_map = te_IN: te-IN, es_AR: es-AR, es_419: es-LA, es_TX: es-TX, he_IL: he-IL, zh_CN: zh-CN, ar_EG: ar-EG, cs_CZ: cs-CZ, fa_IR: fa-IR, ms_MY: ms-MY, nl_NL: nl-NL, de_CH: de-CH, en_IT: en-IT, uz_UZ: uz-UZ, fr_CH: fr-CH, hi_IN: hi-IN, et_EE: et-EE, es_PE: es-PE, es_MX: es-MX, gl_ES: gl-ES, mn_MN: mn, zh_TW: zh-TW, ast_ES: ast-ES, es_CL: es-CL, ja_JP: ja, lv_LV: lv, ro_RO: ro-RO, az_AZ: az-AZ, cy_GB: cy-GB, gu_IN: gu-IN, pl_PL: pl-PL, vep: ve-PP, en_BR: en-BR, en@ysv: en-YS, hu_HU: hu, ko_KR: ko-KR, pt_BR: pt-BR, zh_HK: zh-HK, zu_ZA: zu-ZA, en_MY: en-MY, ja-Hira: ja-HI, fi_FI: fi, vec: ve-CC, vi_VN: vi-VN, fr_FR: fr-FR, id_ID: id, zh_Hans: zh-Hans, en_DE: en-DE, en_GB: en-GB, el_GR: el-GR, uk_UA: uk-UA, az@latin: az-LA, de_AT: de-AT, uz@Latn: uz-LA, vls: vl-SS, ar_DZ: ar-DZ, bg_BG: bg, es_PY: es-PY, fy_NL: fy-NL, uz@Arab: uz-AR, ru_UA: ru-UA, war: wa-RR, zh_CN.GB2312: zh-GB
|
||||
|
||||
[o:wekan:p:wekan:r:application]
|
||||
file_filter = imports/i18n/data/<lang>.i18n.json
|
||||
|
||||
Vendored
+4
-1
@@ -9,5 +9,8 @@
|
||||
"TERM": "xterm-256color"
|
||||
},
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"terminal.integrated.shellArgs.linux": ["-l"]
|
||||
"terminal.integrated.shellArgs.linux": [
|
||||
"-l"
|
||||
],
|
||||
"files.simpleDialog.enable": true
|
||||
}
|
||||
+1526
-14
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.02/wekan-8.02-amd64.zip"
|
||||
unzip wekan-8.02-amd64.zip
|
||||
rm wekan-8.02-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"]
|
||||
+84
-19
@@ -1,12 +1,36 @@
|
||||
About money, see [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
## About money
|
||||
|
||||
Security is very important to us. If you discover any issue regarding security, please disclose
|
||||
the information responsibly by sending an email from Protonmail to security@wekan.fi
|
||||
that is Protomail email address, or by using this PGP key
|
||||
[security-at-wekan.fi.asc](security-at-wekan.fi.asc) to security@wekan.fi
|
||||
and not by creating a GitHub issue. We will respond swiftly to fix verifiable security issues.
|
||||
See [CONTRIBUTING](CONTRIBUTING.md)
|
||||
|
||||
We thank you with a place at our hall of fame page, that is at https://wekan.fi/hall-of-fame
|
||||
## Responsible Security Disclosure
|
||||
|
||||
1. To send email, if possible, use PGP key [security-at-wekan.fi.asc](security-at-wekan.fi.asc)
|
||||
2. Send info about security issue ONLY to security@wekan.fi . NOT TO ANYWHERE ELSE. NO CC, NO BCC.
|
||||
3. Wait for new WeKan release that fixes security issue to appear to top of
|
||||
https://github.com/wekan/wekan/blob/main/CHANGELOG.md
|
||||
4. We will thank you by adding you to Hall of Fame: https://wekan.fi/hall-of-fame/
|
||||
5. All vulnerability details will be private to security@wekan.fi ,
|
||||
unless you help all WeKan platforms to have a way to upgrade, like sending
|
||||
database migrations code to security@wekan.fi or PRs to https://github.com/wekan/wekan/pulls .
|
||||
There is no benefit to Wordwide Security Community to have more details about vulnerabilities,
|
||||
if Worldwide Security Community does not help to make upgrades possible.
|
||||
6. If there some day becomes available a way to upgrade all WeKan platforms,
|
||||
this page will be updated to add permission for security researchers
|
||||
to request new GHSA or CVE ID and publish your vulnerability details at your blog, talks, etc,
|
||||
and send that info also to security@wekan.fi to be added to
|
||||
Hall of Fame: https://wekan.fi/hall-of-fame/ to get Upgrade Bonus Point Stars.
|
||||
In that case, it will become possible for security@wekan.fi to publish all
|
||||
remaining private security details, and publicly thank Worldwide Security Community.
|
||||
|
||||
## Bonus Points
|
||||
|
||||
- If you include code for fixing security issue
|
||||
|
||||
## Losing Points
|
||||
|
||||
- If you ask about [bounty](CONTRIBUTING.md). There is no bounty. WeKan is NOT Big Tech. WeKan is FLOSS.
|
||||
- If you forget to include vulnerability details.
|
||||
- If you send info about security issue to somewhere else than security@wekan.fi
|
||||
|
||||
## How should reports be formatted?
|
||||
|
||||
@@ -26,7 +50,7 @@ CWSS (optional): %cwss
|
||||
|
||||
Anyone who reports a unique security issue in scope and does not disclose it to
|
||||
a third party before we have patched and updated may be upon their approval
|
||||
added to the Wekan Hall of Fame.
|
||||
added to the WeKan Hall of Fame https://wekan.fi/hall-of-fame/
|
||||
|
||||
## Which domains are in scope?
|
||||
|
||||
@@ -63,11 +87,6 @@ and by by companies that have 30k users.
|
||||
- If you are thinking about TLS MITM, look at https://github.com/caddyserver/caddy/issues/2530
|
||||
- Let's Encrypt TLS requires publicly accessible webserver, that Let's Encrypt TLS validation servers check.
|
||||
- If firewall limits to only allowed IP addresses, you may need non-Let's Encrypt TLS cert.
|
||||
- For On Premise:
|
||||
- https://caddyserver.com/docs/automatic-https#local-https
|
||||
- https://github.com/wekan/wekan/wiki/Caddy-Webserver-Config
|
||||
- https://github.com/wekan/wekan/wiki/Azure
|
||||
- https://github.com/wekan/wekan/wiki/Traefik-and-self-signed-SSL-certs
|
||||
|
||||
## XSS
|
||||
|
||||
@@ -172,6 +191,57 @@ Meteor.startup(() => {
|
||||
- https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
|
||||
- https://wekan.github.io/hall-of-fame/filebleed/
|
||||
|
||||
### Attachments: Forced download to prevent stored XSS
|
||||
|
||||
- To prevent browser-side execution of uploaded content under the app origin, all attachment downloads are served with safe headers:
|
||||
- `Content-Type: application/octet-stream`
|
||||
- `Content-Disposition: attachment`
|
||||
- `X-Content-Type-Options: nosniff`
|
||||
- A restrictive `Content-Security-Policy` with `sandbox`
|
||||
- This means attachments are downloaded instead of rendered inline by default. This mitigates HTML/JS/SVG based stored XSS vectors.
|
||||
- Avatars and inline images remain supported but SVG uploads are blocked and never rendered inline.
|
||||
|
||||
## Users: Client update restrictions
|
||||
|
||||
- Client-side updates to user documents are limited to safe fields only:
|
||||
- `username`
|
||||
- `profile.*`
|
||||
- Sensitive fields are blocked from any client updates and can only be modified by server methods with authorization:
|
||||
- `orgs`, `teams`, `roles`, `isAdmin`, `createdThroughApi`, `loginDisabled`, `authenticationMethod`, `services.*`, `emails.*`, `sessionData.*`
|
||||
- Attempts to update forbidden fields from the client are denied.
|
||||
- Admin operations like managing org/team membership or toggling flags must use server methods that check permissions.
|
||||
|
||||
## Voting: integrity and authorization
|
||||
|
||||
- Client updates to card `vote` fields are blocked to prevent forged votes and inconsistent policy enforcement.
|
||||
- Voting is performed via a server method that enforces:
|
||||
- Authentication and board membership, or an explicit per-card flag allowing non-members to vote.
|
||||
- Only the caller's own userId is added/removed from `vote.positive`/`vote.negative`.
|
||||
- This prevents members from fabricating other users' votes and ensures non-members cannot vote unless explicitly allowed.
|
||||
|
||||
## Planning Poker: integrity and authorization
|
||||
|
||||
- Client updates to card `poker` fields are blocked. All poker actions go through server methods that enforce:
|
||||
- Authentication and board membership for configuration and results.
|
||||
- For casting a poker vote, either board membership or an explicit per-card flag allowing non-members to participate.
|
||||
- Only the caller's own userId is added/removed from the selected estimation bucket (e.g., one, two, five, etc.).
|
||||
- Methods cover setting/unsetting poker question/end, casting votes, replaying, and setting final estimation.
|
||||
|
||||
## Attachment API: authentication and DoS prevention
|
||||
|
||||
- The attachment API (`/api/attachment/*`) requires proper authentication using `X-User-Id` and `X-Auth-Token` headers.
|
||||
- Authentication validates tokens by hashing with `Accounts._hashLoginToken` and matching against stored login tokens, preventing identity spoofing.
|
||||
- Request handlers implement:
|
||||
- 30-second timeout to prevent hanging connections.
|
||||
- Request body size limits (50MB for uploads, 10MB for metadata operations).
|
||||
- Proper error handling and guaranteed response completion.
|
||||
- Request error event handlers to clean up failed connections.
|
||||
- This prevents:
|
||||
- DoS attacks via concurrent unauthenticated or malformed requests.
|
||||
- Identity spoofing by using arbitrary bearer tokens or user IDs.
|
||||
- Resource exhaustion from hanging connections or excessive payloads.
|
||||
- Access control: all attachment operations verify board membership before allowing access.
|
||||
|
||||
## Brute force login protection
|
||||
|
||||
- https://github.com/wekan/wekan/commit/23e5e1e3bd081699ce39ce5887db7e612616014d
|
||||
@@ -218,9 +288,4 @@ Typical already known or "no impact" bugs such as:
|
||||
- Email spoofing, SPF, DMARC & DKIM. Wekan does not include email server.
|
||||
|
||||
Wekan is Open Source with MIT license, and free to use also for commercial use.
|
||||
We welcome all fixes to improve security by email to security@wekan.team
|
||||
|
||||
## Bonus Points
|
||||
|
||||
If your Responsible Security Disclosure includes code for fixing security issue,
|
||||
you get bonus points, as seen on [Hall of Fame](https://wekan.github.io/hall-of-fame).
|
||||
We welcome all fixes to improve security by email to security@wekan.fi
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v8.02.0"
|
||||
appVersion: "v8.78.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
+77
-6
@@ -5,13 +5,84 @@ 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/migrationManager';
|
||||
import '/client/components/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
|
||||
// Token already lives in localStorage; cookie adds same-origin send-on-request semantics
|
||||
Meteor.startup(() => {
|
||||
const COOKIE_NAME = 'meteor_login_token';
|
||||
const cookieAttrs = () => {
|
||||
const attrs = ['Path=/', 'SameSite=Lax'];
|
||||
try {
|
||||
if (window.location && window.location.protocol === 'https:') {
|
||||
attrs.push('Secure');
|
||||
}
|
||||
} catch (_) {}
|
||||
return attrs.join('; ');
|
||||
};
|
||||
|
||||
const setCookie = (name, value) => {
|
||||
if (!value) return;
|
||||
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; ${cookieAttrs()}`;
|
||||
};
|
||||
const clearCookie = (name) => {
|
||||
document.cookie = `${encodeURIComponent(name)}=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; ${cookieAttrs()}`;
|
||||
};
|
||||
|
||||
const syncCookie = () => {
|
||||
try {
|
||||
const token = Accounts && typeof Accounts._storedLoginToken === 'function' ? Accounts._storedLoginToken() : null;
|
||||
if (token) setCookie(COOKIE_NAME, token); else clearCookie(COOKIE_NAME);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
// Initial sync on startup
|
||||
syncCookie();
|
||||
|
||||
// Keep cookie in sync on login/logout
|
||||
if (Accounts && typeof Accounts.onLogin === 'function') Accounts.onLogin(syncCookie);
|
||||
if (Accounts && typeof Accounts.onLogout === 'function') Accounts.onLogout(syncCookie);
|
||||
|
||||
// Sync across tabs/windows when localStorage changes
|
||||
window.addEventListener('storage', (ev) => {
|
||||
if (ev && typeof ev.key === 'string' && ev.key.indexOf('Meteor.loginToken') !== -1) {
|
||||
syncCookie();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Subscribe to per-user small publications
|
||||
Meteor.startup(() => {
|
||||
Tracker.autorun(() => {
|
||||
if (Meteor.userId()) {
|
||||
Meteor.subscribe('userGreyIcons');
|
||||
Meteor.subscribe('userDesktopDragHandles');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize mobile mode on startup for iOS devices
|
||||
// This ensures mobile mode is applied correctly on page load
|
||||
Tracker.afterFlush(() => {
|
||||
if (typeof Utils !== 'undefined' && Utils.initializeUserSettings) {
|
||||
Utils.initializeUserSettings();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -9,11 +9,11 @@ template(name="activities")
|
||||
+cardActivities
|
||||
|
||||
template(name="boardActivities")
|
||||
each activityData in currentBoard.activities
|
||||
each activityData in boardActivitiesList
|
||||
+activity(activity=activityData card=card mode=mode)
|
||||
|
||||
template(name="cardActivities")
|
||||
each activityData in activities
|
||||
each activityData in cardActivitiesList
|
||||
+activity(activity=activityData card=card mode=mode)
|
||||
|
||||
template(name="activity")
|
||||
@@ -189,14 +189,15 @@ template(name="activity")
|
||||
if(currentData.timeKey)
|
||||
| {{_ activity.activityType }}
|
||||
= ' '
|
||||
i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
|
||||
i(title=currentData.timeValue).activity-meta {{ displayDate currentData.timeValue 'LLL' }}
|
||||
if (currentData.timeOldValue)
|
||||
= ' '
|
||||
| {{{_ "previous_as" }}}
|
||||
= ' '
|
||||
i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
|
||||
i(title=currentData.timeOldValue).activity-meta {{ displayDate currentData.timeOldValue 'LLL' }}
|
||||
= ' @'
|
||||
else if(currentData.timeValue)
|
||||
| {{_ activity.activityType currentData.timeValue}}
|
||||
|
||||
div(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
|
||||
if($neq mode 'none')
|
||||
div(title=activity.createdAt).activity-meta {{ displayDate activity.createdAt }}
|
||||
@@ -1,168 +1,192 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Template } from 'meteor/templating';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import { getSidebarInstance } from '/client/features/sidebar/service';
|
||||
|
||||
const activitiesPerPage = 500;
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
// XXX Should we use ReactiveNumber?
|
||||
this.page = new ReactiveVar(1);
|
||||
this.loadNextPageLocked = false;
|
||||
// TODO is sidebar always available? E.g. on small screens/mobile devices
|
||||
const sidebar = Sidebar;
|
||||
sidebar && sidebar.callFirstWith(null, 'resetNextPeak');
|
||||
this.autorun(() => {
|
||||
let mode = this.data()?.mode;
|
||||
if (mode) {
|
||||
const capitalizedMode = Utils.capitalize(mode);
|
||||
let searchId;
|
||||
const showActivities = this.showActivities();
|
||||
if (mode === 'linkedcard' || mode === 'linkedboard') {
|
||||
const currentCard = Utils.getCurrentCard();
|
||||
searchId = currentCard.linkedId;
|
||||
mode = mode.replace('linked', '');
|
||||
} else if (mode === 'card') {
|
||||
searchId = Utils.getCurrentCardId();
|
||||
} else {
|
||||
searchId = Session.get(`current${capitalizedMode}`);
|
||||
}
|
||||
const limit = this.page.get() * activitiesPerPage;
|
||||
if (searchId === null) return;
|
||||
Template.activities.onCreated(function () {
|
||||
// Register with sidebar so it can call loadNextPage on us
|
||||
const Sidebar = getSidebarInstance();
|
||||
if (Sidebar) {
|
||||
Sidebar.activitiesInstance = this;
|
||||
}
|
||||
|
||||
this.subscribe('activities', mode, searchId, limit, showActivities, () => {
|
||||
this.loadNextPageLocked = false;
|
||||
|
||||
// TODO the guard can be removed as soon as the TODO above is resolved
|
||||
if (!sidebar) return;
|
||||
// If the sibear peak hasn't increased, that mean that there are no more
|
||||
// activities, and we can stop calling new subscriptions.
|
||||
// XXX This is hacky! We need to know excatly and reactively how many
|
||||
// activities there are, we probably want to denormalize this number
|
||||
// dirrectly into card and board documents.
|
||||
const nextPeakBefore = sidebar.callFirstWith(null, 'getNextPeak');
|
||||
sidebar.calculateNextPeak();
|
||||
const nextPeakAfter = sidebar.callFirstWith(null, 'getNextPeak');
|
||||
if (nextPeakBefore === nextPeakAfter) {
|
||||
sidebar.callFirstWith(null, 'resetNextPeak');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
loadNextPage() {
|
||||
// XXX Should we use ReactiveNumber?
|
||||
this.page = new ReactiveVar(1);
|
||||
this.loadNextPageLocked = false;
|
||||
this.loadNextPage = () => {
|
||||
if (this.loadNextPageLocked === false) {
|
||||
this.page.set(this.page.get() + 1);
|
||||
this.loadNextPageLocked = true;
|
||||
}
|
||||
},
|
||||
showActivities() {
|
||||
let ret = false;
|
||||
let mode = this.data()?.mode;
|
||||
};
|
||||
|
||||
// TODO is sidebar always available? E.g. on small screens/mobile devices
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar && sidebar.infiniteScrolling) {
|
||||
sidebar.infiniteScrolling.resetNextPeak();
|
||||
}
|
||||
this.autorun(() => {
|
||||
const data = Template.currentData();
|
||||
let mode = data?.mode;
|
||||
if (mode) {
|
||||
const capitalizedMode = Utils.capitalize(mode);
|
||||
let searchId;
|
||||
const showActivities = _showActivities(data);
|
||||
if (mode === 'linkedcard' || mode === 'linkedboard') {
|
||||
const currentCard = Utils.getCurrentCard();
|
||||
ret = currentCard.showActivities ?? false;
|
||||
searchId = currentCard.linkedId;
|
||||
mode = mode.replace('linked', '');
|
||||
} else if (mode === 'card') {
|
||||
ret = this.data()?.card?.showActivities ?? false;
|
||||
searchId = Utils.getCurrentCardId();
|
||||
} else {
|
||||
ret = Utils.getCurrentBoard().showActivities ?? false;
|
||||
searchId = Session.get(`current${capitalizedMode}`);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
activities() {
|
||||
const ret = this.data().card.activities();
|
||||
return ret;
|
||||
},
|
||||
}).register('activities');
|
||||
const limit = this.page.get() * activitiesPerPage;
|
||||
if (searchId === null) return;
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
this.subscribe('activities', mode, searchId, limit, showActivities, () => {
|
||||
this.loadNextPageLocked = false;
|
||||
|
||||
// TODO the guard can be removed as soon as the TODO above is resolved
|
||||
if (!sidebar || !sidebar.infiniteScrolling) return;
|
||||
// If the sidebar peak hasn't increased, that means that there are no more
|
||||
// activities, and we can stop calling new subscriptions.
|
||||
const nextPeakBefore = sidebar.infiniteScrolling.getNextPeak();
|
||||
sidebar.calculateNextPeak();
|
||||
const nextPeakAfter = sidebar.infiniteScrolling.getNextPeak();
|
||||
if (nextPeakBefore === nextPeakAfter) {
|
||||
sidebar.infiniteScrolling.resetNextPeak();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function _showActivities(data) {
|
||||
let ret = false;
|
||||
let mode = data?.mode;
|
||||
if (mode) {
|
||||
if (mode === 'linkedcard' || mode === 'linkedboard') {
|
||||
const currentCard = Utils.getCurrentCard();
|
||||
ret = currentCard.showActivities ?? false;
|
||||
} else if (mode === 'card') {
|
||||
// For card mode, get showActivities from data context or fallback to current card
|
||||
const card = data?.card || Utils.getCurrentCard();
|
||||
ret = card?.showActivities ?? false;
|
||||
} else {
|
||||
ret = Utils.getCurrentBoard().showActivities ?? false;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
Template.activities.helpers({
|
||||
showActivities() {
|
||||
const data = Template.currentData();
|
||||
return _showActivities(data);
|
||||
},
|
||||
|
||||
activities() {
|
||||
const activities = this.card?.activities?.();
|
||||
return activities || [];
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardActivities.helpers({
|
||||
boardActivitiesList() {
|
||||
const board = Utils.getCurrentBoard();
|
||||
const activities = board?.activities?.();
|
||||
return activities || [];
|
||||
},
|
||||
});
|
||||
|
||||
Template.cardActivities.helpers({
|
||||
cardActivitiesList() {
|
||||
const data = Template.currentData();
|
||||
const card = data?.card;
|
||||
const activities = card?.activities?.();
|
||||
return activities || [];
|
||||
},
|
||||
});
|
||||
|
||||
Template.activity.helpers({
|
||||
checkItem() {
|
||||
const checkItemId = this.currentData().activity.checklistItemId;
|
||||
const checkItemId = this.activity.checklistItemId;
|
||||
const checkItem = ReactiveCache.getChecklistItem(checkItemId);
|
||||
return checkItem && checkItem.title;
|
||||
},
|
||||
|
||||
boardLabelLink() {
|
||||
const data = this.currentData();
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
if (data.mode !== 'board') {
|
||||
// data.mode: card, linkedcard, linkedboard
|
||||
return createBoardLink(data.activity.board(), data.activity.listName ? data.activity.listName : null);
|
||||
if (this.mode !== 'board') {
|
||||
return createBoardLink(this.activity.board(), this.activity.listName ? this.activity.listName : null);
|
||||
}
|
||||
else if (currentBoardId != data.activity.boardId) {
|
||||
// data.mode: board
|
||||
// current activitie is linked
|
||||
return createBoardLink(data.activity.board(), data.activity.listName ? data.activity.listName : null);
|
||||
else if (currentBoardId != this.activity.boardId) {
|
||||
return createBoardLink(this.activity.board(), this.activity.listName ? this.activity.listName : null);
|
||||
}
|
||||
return TAPi18n.__('this-board');
|
||||
},
|
||||
|
||||
cardLabelLink() {
|
||||
const data = this.currentData();
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
if (data.mode == 'card') {
|
||||
// data.mode: card
|
||||
if (this.mode == 'card') {
|
||||
return TAPi18n.__('this-card');
|
||||
}
|
||||
else if (data.mode !== 'board') {
|
||||
// data.mode: linkedcard, linkedboard
|
||||
return createCardLink(data.activity.card(), null);
|
||||
else if (this.mode !== 'board') {
|
||||
return createCardLink(this.activity.card(), null);
|
||||
}
|
||||
else if (currentBoardId != data.activity.boardId) {
|
||||
// data.mode: board
|
||||
// current activitie is linked
|
||||
return createCardLink(data.activity.card(), data.activity.board().title);
|
||||
else if (currentBoardId != this.activity.boardId) {
|
||||
return createCardLink(this.activity.card(), this.activity.board().title);
|
||||
}
|
||||
return createCardLink(this.currentData().activity.card(), null);
|
||||
return createCardLink(this.activity.card(), null);
|
||||
},
|
||||
|
||||
cardLink() {
|
||||
const data = this.currentData();
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
if (data.mode !== 'board') {
|
||||
// data.mode: card, linkedcard, linkedboard
|
||||
return createCardLink(data.activity.card(), null);
|
||||
if (this.mode !== 'board') {
|
||||
return createCardLink(this.activity.card(), null);
|
||||
}
|
||||
else if (currentBoardId != data.activity.boardId) {
|
||||
// data.mode: board
|
||||
// current activitie is linked
|
||||
return createCardLink(data.activity.card(), data.activity.board().title);
|
||||
else if (currentBoardId != this.activity.boardId) {
|
||||
return createCardLink(this.activity.card(), this.activity.board().title);
|
||||
}
|
||||
return createCardLink(this.currentData().activity.card(), null);
|
||||
return createCardLink(this.activity.card(), null);
|
||||
},
|
||||
|
||||
receivedDate() {
|
||||
const receivedDate = this.currentData().activity.card();
|
||||
if (!receivedDate) return null;
|
||||
return receivedDate.receivedAt;
|
||||
const card = this.activity.card();
|
||||
if (!card) return null;
|
||||
return card.receivedAt;
|
||||
},
|
||||
|
||||
startDate() {
|
||||
const startDate = this.currentData().activity.card();
|
||||
if (!startDate) return null;
|
||||
return startDate.startAt;
|
||||
const card = this.activity.card();
|
||||
if (!card) return null;
|
||||
return card.startAt;
|
||||
},
|
||||
|
||||
dueDate() {
|
||||
const dueDate = this.currentData().activity.card();
|
||||
if (!dueDate) return null;
|
||||
return dueDate.dueAt;
|
||||
const card = this.activity.card();
|
||||
if (!card) return null;
|
||||
return card.dueAt;
|
||||
},
|
||||
|
||||
endDate() {
|
||||
const endDate = this.currentData().activity.card();
|
||||
if (!endDate) return null;
|
||||
return endDate.endAt;
|
||||
const card = this.activity.card();
|
||||
if (!card) return null;
|
||||
return card.endAt;
|
||||
},
|
||||
|
||||
lastLabel() {
|
||||
const lastLabelId = this.currentData().activity.labelId;
|
||||
const lastLabelId = this.activity.labelId;
|
||||
if (!lastLabelId) return null;
|
||||
const lastLabel = ReactiveCache.getBoard(
|
||||
this.currentData().activity.boardId,
|
||||
this.activity.boardId,
|
||||
).getLabelById(lastLabelId);
|
||||
if (lastLabel && (lastLabel.name === undefined || lastLabel.name === '')) {
|
||||
return lastLabel.color;
|
||||
@@ -175,7 +199,7 @@ BlazeComponent.extendComponent({
|
||||
|
||||
lastCustomField() {
|
||||
const lastCustomField = ReactiveCache.getCustomField(
|
||||
this.currentData().activity.customFieldId,
|
||||
this.activity.customFieldId,
|
||||
);
|
||||
if (!lastCustomField) return null;
|
||||
return lastCustomField.name;
|
||||
@@ -183,16 +207,15 @@ BlazeComponent.extendComponent({
|
||||
|
||||
lastCustomFieldValue() {
|
||||
const lastCustomField = ReactiveCache.getCustomField(
|
||||
this.currentData().activity.customFieldId,
|
||||
this.activity.customFieldId,
|
||||
);
|
||||
if (!lastCustomField) return null;
|
||||
const value = this.currentData().activity.value;
|
||||
const value = this.activity.value;
|
||||
if (
|
||||
lastCustomField.settings.dropdownItems &&
|
||||
lastCustomField.settings.dropdownItems.length > 0
|
||||
) {
|
||||
const dropDownValue = _.find(
|
||||
lastCustomField.settings.dropdownItems,
|
||||
const dropDownValue = lastCustomField.settings.dropdownItems.find(
|
||||
item => {
|
||||
return item._id === value;
|
||||
},
|
||||
@@ -203,13 +226,13 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
listLabel() {
|
||||
const activity = this.currentData().activity;
|
||||
const activity = this.activity;
|
||||
const list = activity.list();
|
||||
return (list && list.title) || activity.title;
|
||||
},
|
||||
|
||||
sourceLink() {
|
||||
const source = this.currentData().activity.source;
|
||||
const source = this.activity.source;
|
||||
if (source) {
|
||||
if (source.url) {
|
||||
return Blaze.toHTML(
|
||||
@@ -229,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) {
|
||||
|
||||
@@ -108,15 +108,12 @@
|
||||
text-decoration: none;
|
||||
height: 24px;
|
||||
}
|
||||
.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-smile-o {
|
||||
font-size: 17px;
|
||||
.comments .comment .comment-desc .reactions .open-comment-reaction-popup span {
|
||||
display: inline-block;
|
||||
font-size: clamp(14px, 2vw, 18px);
|
||||
font-weight: 500;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-plus {
|
||||
font-size: 8px;
|
||||
margin-top: -7px;
|
||||
margin-left: 1px;
|
||||
line-height: 1;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.comments .comment .comment-desc .reactions .reaction {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -25,13 +25,14 @@ template(name="comment")
|
||||
= text
|
||||
.edit-controls
|
||||
button.primary(type="submit") {{_ 'edit'}}
|
||||
.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form(title="{{_ 'close' }}")
|
||||
i.fa.fa-times-thin
|
||||
else
|
||||
.comment-text
|
||||
+viewer
|
||||
= text
|
||||
+commentReactions(reactions=reactions commentId=_id)
|
||||
span(title=createdAt).comment-meta {{ moment createdAt }}
|
||||
span(title=createdAt).comment-meta {{ displayDate createdAt }}
|
||||
if($eq currentUser._id userId)
|
||||
+editOrDeleteComment
|
||||
else if currentUser.isBoardAdmin
|
||||
@@ -54,9 +55,11 @@ template(name="commentReactions")
|
||||
span.reaction-codepoint !{reaction.reactionCodepoint}
|
||||
span.reaction-count #{reaction.userIds.length}
|
||||
if (currentUser.isBoardMember)
|
||||
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
|
||||
i.fa.fa-smile-o
|
||||
i.fa.fa-plus
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
|
||||
span(title="{{_ 'reaction' }}") 😀
|
||||
span(title="{{_ 'add' }}") ➕
|
||||
|
||||
template(name="addReactionPopup")
|
||||
.reactions-popup
|
||||
|
||||
@@ -1,94 +1,85 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import CardComments from '/models/cardComments';
|
||||
import { UnsavedEdits } from '/client/lib/unsavedEdits';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import autosize from 'autosize';
|
||||
|
||||
const commentFormIsOpen = new ReactiveVar(false);
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onDestroyed() {
|
||||
commentFormIsOpen.set(false);
|
||||
$('.note-popover').hide();
|
||||
},
|
||||
Template.commentForm.onDestroyed(function () {
|
||||
commentFormIsOpen.set(false);
|
||||
$('.note-popover').hide();
|
||||
});
|
||||
|
||||
Template.commentForm.helpers({
|
||||
commentFormIsOpen() {
|
||||
return commentFormIsOpen.get();
|
||||
},
|
||||
});
|
||||
|
||||
getInput() {
|
||||
return this.$('.js-new-comment-input');
|
||||
Template.commentForm.events({
|
||||
'submit .js-new-comment-form'(evt, tpl) {
|
||||
const input = tpl.$('.js-new-comment-input');
|
||||
const text = input.val().trim();
|
||||
const card = Template.currentData();
|
||||
let boardId = card.boardId;
|
||||
let cardId = card._id;
|
||||
if (card.isLinkedCard()) {
|
||||
boardId = ReactiveCache.getCard(card.linkedId).boardId;
|
||||
cardId = card.linkedId;
|
||||
} else if (card.isLinkedBoard()) {
|
||||
boardId = card.linkedId;
|
||||
}
|
||||
if (text) {
|
||||
CardComments.insert({
|
||||
text,
|
||||
boardId,
|
||||
cardId,
|
||||
});
|
||||
resetCommentInput(input);
|
||||
Tracker.flush();
|
||||
autosize.update(input);
|
||||
input.trigger('submitted');
|
||||
}
|
||||
evt.preventDefault();
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-new-comment-form'(evt) {
|
||||
const input = this.getInput();
|
||||
const text = input.val().trim();
|
||||
const card = this.currentData();
|
||||
let boardId = card.boardId;
|
||||
let cardId = card._id;
|
||||
if (card.isLinkedCard()) {
|
||||
boardId = ReactiveCache.getCard(card.linkedId).boardId;
|
||||
cardId = card.linkedId;
|
||||
} else if (card.isLinkedBoard()) {
|
||||
boardId = card.linkedId;
|
||||
}
|
||||
if (text) {
|
||||
CardComments.insert({
|
||||
text,
|
||||
boardId,
|
||||
cardId,
|
||||
});
|
||||
resetCommentInput(input);
|
||||
Tracker.flush();
|
||||
autosize.update(input);
|
||||
input.trigger('submitted');
|
||||
}
|
||||
evt.preventDefault();
|
||||
},
|
||||
// Pressing Ctrl+Enter should submit the form
|
||||
'keydown form textarea'(evt) {
|
||||
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
|
||||
this.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
// Pressing Ctrl+Enter should submit the form
|
||||
'keydown form textarea'(evt, tpl) {
|
||||
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
|
||||
tpl.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
}).register('commentForm');
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.comments.helpers({
|
||||
getComments() {
|
||||
const ret = this.data().comments();
|
||||
return ret;
|
||||
const data = Template.currentData();
|
||||
if (!data || typeof data.comments !== 'function') return [];
|
||||
return data.comments();
|
||||
},
|
||||
}).register("comments");
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-delete-comment': Popup.afterConfirm('deleteComment', () => {
|
||||
const commentId = this.data()._id;
|
||||
CardComments.remove(commentId);
|
||||
Popup.back();
|
||||
}),
|
||||
'submit .js-edit-comment'(evt) {
|
||||
evt.preventDefault();
|
||||
const commentText = this.currentComponent()
|
||||
.getValue()
|
||||
.trim();
|
||||
const commentId = this.data()._id;
|
||||
if (commentText) {
|
||||
CardComments.update(commentId, {
|
||||
$set: {
|
||||
text: commentText,
|
||||
},
|
||||
});
|
||||
}
|
||||
Template.comment.events({
|
||||
'click .js-delete-comment': Popup.afterConfirm('deleteComment', function () {
|
||||
const commentId = this._id;
|
||||
CardComments.remove(commentId);
|
||||
Popup.back();
|
||||
}),
|
||||
'submit .js-edit-comment'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const textarea = tpl.find('.js-edit-comment textarea,input[type=text]');
|
||||
const commentText = textarea && textarea.value ? textarea.value.trim() : '';
|
||||
const commentId = 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,24 +3,24 @@ template(name="boardConversionProgress")
|
||||
.board-conversion-modal
|
||||
.board-conversion-header
|
||||
h3
|
||||
i.fa.fa-cogs
|
||||
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-spinner.fa-spin
|
||||
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
|
||||
|
||||
@@ -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
|
||||
i.fa.fa-archive
|
||||
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-o
|
||||
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,10 +1,13 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import Boards from '/models/boards';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.subscribe('archivedBoards');
|
||||
},
|
||||
Template.archivedBoards.onCreated(function () {
|
||||
this.subscribe('archivedBoards');
|
||||
});
|
||||
|
||||
Template.archivedBoards.helpers({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
@@ -18,38 +21,34 @@ BlazeComponent.extendComponent({
|
||||
);
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-restore-board'() {
|
||||
// TODO : Make isSandstorm variable global
|
||||
const isSandstorm =
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.sandstorm;
|
||||
if (isSandstorm && Utils.getCurrentBoardId()) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
currentBoard.archive();
|
||||
}
|
||||
const board = this.currentData();
|
||||
board.restore();
|
||||
Utils.goBoardId(board._id);
|
||||
},
|
||||
'click .js-delete-board': Popup.afterConfirm('boardDelete', function() {
|
||||
Popup.back();
|
||||
const isSandstorm =
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.sandstorm;
|
||||
if (isSandstorm && Utils.getCurrentBoardId()) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
Boards.remove(currentBoard._id);
|
||||
}
|
||||
Boards.remove(this._id);
|
||||
FlowRouter.go('home');
|
||||
}),
|
||||
},
|
||||
];
|
||||
Template.archivedBoards.events({
|
||||
async 'click .js-restore-board'() {
|
||||
// TODO : Make isSandstorm variable global
|
||||
const isSandstorm =
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.sandstorm;
|
||||
if (isSandstorm && Utils.getCurrentBoardId()) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
await currentBoard.archive();
|
||||
}
|
||||
const board = this;
|
||||
await board.restore();
|
||||
Utils.goBoardId(board._id);
|
||||
},
|
||||
}).register('archivedBoards');
|
||||
'click .js-delete-board': Popup.afterConfirm('boardDelete', async function() {
|
||||
Popup.back();
|
||||
const isSandstorm =
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.sandstorm;
|
||||
if (isSandstorm && Utils.getCurrentBoardId()) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
await Boards.removeAsync(currentBoard._id);
|
||||
}
|
||||
await Boards.removeAsync(this._id);
|
||||
FlowRouter.go('home');
|
||||
}),
|
||||
});
|
||||
@@ -231,6 +231,30 @@
|
||||
font-size: 1em !important; /* Keep original icon size */
|
||||
}
|
||||
|
||||
/* Mobile iPhone: scale card details text and icons to 2x */
|
||||
body.mobile-mode.iphone-device .card-details {
|
||||
font-size: 2em !important;
|
||||
}
|
||||
body.mobile-mode.iphone-device .card-details .fa,
|
||||
body.mobile-mode.iphone-device .card-details .icon,
|
||||
body.mobile-mode.iphone-device .card-details i,
|
||||
body.mobile-mode.iphone-device .card-details .emoji-icon,
|
||||
body.mobile-mode.iphone-device .card-details a,
|
||||
body.mobile-mode.iphone-device .card-details p,
|
||||
body.mobile-mode.iphone-device .card-details span,
|
||||
body.mobile-mode.iphone-device .card-details div,
|
||||
body.mobile-mode.iphone-device .card-details button,
|
||||
body.mobile-mode.iphone-device .card-details input,
|
||||
body.mobile-mode.iphone-device .card-details select,
|
||||
body.mobile-mode.iphone-device .card-details textarea {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
/* Section titles slightly larger than content but not as big as card title */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-item-title {
|
||||
font-size: 1.1em !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Ensure scrollbars are positioned correctly */
|
||||
#content[style*="overflow-x: auto"]::-webkit-scrollbar:vertical {
|
||||
width: 12px;
|
||||
@@ -263,62 +287,106 @@
|
||||
animation: fadeIn 0.2s;
|
||||
z-index: 16;
|
||||
}
|
||||
|
||||
/* Fix for mobile Safari: ensure overlay stays behind card details */
|
||||
@media screen and (max-width: 800px) {
|
||||
.board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
|
||||
/* In desktop mode on small screens, still keep overlay behind card */
|
||||
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* In mobile mode, lower the overlay z-index to stay behind card details */
|
||||
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
|
||||
/* iPhone in desktop mode: remove overlay to avoid blocking card */
|
||||
body.desktop-mode.iphone-device .board-wrapper .board-canvas .board-overlay {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* Desktop mode: hide overlay to allow multiple cards and board interaction */
|
||||
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
.board-wrapper .board-canvas.is-dragging-active .open-minicard-composer,
|
||||
.board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked {
|
||||
display: none;
|
||||
}
|
||||
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
|
||||
.board-wrapper.mobile-view {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.board-wrapper.mobile-view .board-canvas {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.board-wrapper.mobile-view .board-canvas.mobile-view .swimlane {
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: flex;
|
||||
display: block !important;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
@media screen and (max-width: 800px),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||
.board-wrapper {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.board-wrapper .board-canvas {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.board-wrapper .board-canvas .swimlane {
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: flex;
|
||||
display: block !important;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
}
|
||||
}
|
||||
.calendar-event-green {
|
||||
@@ -496,3 +564,10 @@
|
||||
font-size: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Global file drag over state for board canvas */
|
||||
.board-canvas.file-drag-over {
|
||||
background-color: rgba(0, 123, 255, 0.05) !important;
|
||||
border: 2px dashed #007bff !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
template(name="board")
|
||||
|
||||
if isMigrating.get
|
||||
+migrationProgress
|
||||
else if isConverting.get
|
||||
|
||||
if isConverting
|
||||
+boardConversionProgress
|
||||
else if isBoardReady.get
|
||||
else if isBoardReady
|
||||
if currentBoard
|
||||
if onlyShowCurrentCard
|
||||
+cardDetails(currentCard)
|
||||
@@ -24,16 +22,16 @@ template(name="boardBody")
|
||||
// Debug information (remove in production)
|
||||
if debugBoardState
|
||||
.debug-info(style="position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.8); color: white; padding: 10px; z-index: 9999; font-size: 12px;")
|
||||
| Board: {{currentBoard.title}} | View: {{boardView}} | HasSwimlanes: {{hasSwimlanes}} | Swimlanes: {{currentBoard.swimlanes.length}}
|
||||
| {{_ 'board'}}: {{currentBoard.title}} | {{_ 'view'}}: {{boardView}} | {{_ 'has-swimlanes'}}: {{hasSwimlanes}} | {{_ 'swimlanes'}}: {{currentBoard.swimlanes.length}}
|
||||
.board-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
.board-canvas.js-swimlanes(
|
||||
class="{{#if hasSwimlanes}}dragscroll{{/if}}"
|
||||
class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
|
||||
class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
|
||||
class="{{#if draggingActive.get}}is-dragging-active{{/if}}"
|
||||
class="{{#if draggingActive}}is-dragging-active{{/if}}"
|
||||
class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}"
|
||||
class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
if showOverlay.get
|
||||
if showOverlay
|
||||
.board-overlay
|
||||
if currentBoard.isTemplatesBoard
|
||||
each currentBoard.swimlanes
|
||||
@@ -49,6 +47,8 @@ template(name="boardBody")
|
||||
+listsGroup(currentBoard)
|
||||
else if isViewCalendar
|
||||
+calendarView
|
||||
else if isViewGantt
|
||||
+ganttView
|
||||
else
|
||||
// Default view - show swimlanes if they exist, otherwise show lists
|
||||
if hasSwimlanes
|
||||
@@ -56,6 +56,10 @@ template(name="boardBody")
|
||||
+swimlane(this)
|
||||
else
|
||||
+listsGroup(currentBoard)
|
||||
//- Render multiple open cards in desktop mode
|
||||
unless isMiniScreen
|
||||
each openCards
|
||||
+cardDetails(this cardIndex=@index)
|
||||
+sidebar
|
||||
|
||||
template(name="calendarView")
|
||||
@@ -63,4 +67,4 @@ template(name="calendarView")
|
||||
.calendar-view.swimlane
|
||||
if currentCard
|
||||
+cardDetails(currentCard)
|
||||
+fullcalendar(calendarOptions)
|
||||
+fullcalendar(calendarOptions)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -49,6 +49,12 @@ THEME - NEPHRITIS
|
||||
border-bottom: 2px solid #27ae60;
|
||||
border-right: 2px solid #27ae60;
|
||||
}
|
||||
.board-color-nephritis .checklist-progress-bar {
|
||||
background-color: #d4f1dd !important;
|
||||
}
|
||||
.board-color-nephritis .checklist-progress-bar .checklist-progress {
|
||||
background-color: #27ae60 !important;
|
||||
}
|
||||
.board-color-nephritis .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e7faef;
|
||||
}
|
||||
@@ -150,6 +156,12 @@ THEME - Pomegranate
|
||||
border-bottom: 2px solid #c0392b;
|
||||
border-right: 2px solid #c0392b;
|
||||
}
|
||||
.board-color-pomegranate .checklist-progress-bar {
|
||||
background-color: #f5d5d2 !important;
|
||||
}
|
||||
.board-color-pomegranate .checklist-progress-bar .checklist-progress {
|
||||
background-color: #c0392b !important;
|
||||
}
|
||||
.board-color-pomegranate .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #faeae9;
|
||||
}
|
||||
@@ -251,6 +263,12 @@ THEME - Belize
|
||||
border-bottom: 2px solid #2980b9;
|
||||
border-right: 2px solid #2980b9;
|
||||
}
|
||||
.board-color-belize .checklist-progress-bar {
|
||||
background-color: #d1e7f5 !important;
|
||||
}
|
||||
.board-color-belize .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2980b9 !important;
|
||||
}
|
||||
.board-color-belize .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e8f3fa;
|
||||
}
|
||||
@@ -352,6 +370,12 @@ THEME - Wisteria
|
||||
border-bottom: 2px solid #8e44ad;
|
||||
border-right: 2px solid #8e44ad;
|
||||
}
|
||||
.board-color-wisteria .checklist-progress-bar {
|
||||
background-color: #e8d9f0 !important;
|
||||
}
|
||||
.board-color-wisteria .checklist-progress-bar .checklist-progress {
|
||||
background-color: #8e44ad !important;
|
||||
}
|
||||
.board-color-wisteria .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #f4ecf7;
|
||||
}
|
||||
@@ -453,6 +477,12 @@ THEME - Midnight
|
||||
border-bottom: 2px solid #2c3e50;
|
||||
border-right: 2px solid #2c3e50;
|
||||
}
|
||||
.board-color-midnight .checklist-progress-bar {
|
||||
background-color: #d2dae2 !important;
|
||||
}
|
||||
.board-color-midnight .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2c3e50 !important;
|
||||
}
|
||||
.board-color-midnight .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e6ecf1;
|
||||
}
|
||||
@@ -554,6 +584,12 @@ THEME - Pumpkin
|
||||
border-bottom: 2px solid #e67e22;
|
||||
border-right: 2px solid #e67e22;
|
||||
}
|
||||
.board-color-pumpkin .checklist-progress-bar {
|
||||
background-color: #f9e5d1 !important;
|
||||
}
|
||||
.board-color-pumpkin .checklist-progress-bar .checklist-progress {
|
||||
background-color: #e67e22 !important;
|
||||
}
|
||||
.board-color-pumpkin .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #fdf2e9;
|
||||
}
|
||||
@@ -655,6 +691,12 @@ THEME - Moderate Pink
|
||||
border-bottom: 2px solid #cd5a91;
|
||||
border-right: 2px solid #cd5a91;
|
||||
}
|
||||
.board-color-moderatepink .checklist-progress-bar {
|
||||
background-color: #f4dde8 !important;
|
||||
}
|
||||
.board-color-moderatepink .checklist-progress-bar .checklist-progress {
|
||||
background-color: #cd5a91 !important;
|
||||
}
|
||||
.board-color-moderatepink .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #faeef4;
|
||||
}
|
||||
@@ -756,6 +798,12 @@ THEME - Strong Cyan
|
||||
border-bottom: 2px solid #00aecc;
|
||||
border-right: 2px solid #00aecc;
|
||||
}
|
||||
.board-color-strongcyan .checklist-progress-bar {
|
||||
background-color: #ccf2f9 !important;
|
||||
}
|
||||
.board-color-strongcyan .checklist-progress-bar .checklist-progress {
|
||||
background-color: #00aecc !important;
|
||||
}
|
||||
.board-color-strongcyan .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e0fbff;
|
||||
}
|
||||
@@ -857,6 +905,12 @@ THEME - Lime Green
|
||||
border-bottom: 2px solid #4bbf6b;
|
||||
border-right: 2px solid #4bbf6b;
|
||||
}
|
||||
.board-color-limegreen .checklist-progress-bar {
|
||||
background-color: #daf4de !important;
|
||||
}
|
||||
.board-color-limegreen .checklist-progress-bar .checklist-progress {
|
||||
background-color: #4bbf6b !important;
|
||||
}
|
||||
.board-color-limegreen .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #edf9f0;
|
||||
}
|
||||
@@ -959,6 +1013,12 @@ THEME - Dark
|
||||
border-bottom: 2px solid #2c3e51;
|
||||
border-right: 2px solid #2c3e51;
|
||||
}
|
||||
.board-color-dark .checklist-progress-bar {
|
||||
background-color: #d2dae2 !important;
|
||||
}
|
||||
.board-color-dark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2c3e51 !important;
|
||||
}
|
||||
.board-color-dark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e6ecf1;
|
||||
}
|
||||
@@ -1162,6 +1222,12 @@ THEME - Relax
|
||||
border-bottom: 2px solid #27ae61;
|
||||
border-right: 2px solid #27ae61;
|
||||
}
|
||||
.board-color-relax .checklist-progress-bar {
|
||||
background-color: #d4f1dd !important;
|
||||
}
|
||||
.board-color-relax .checklist-progress-bar .checklist-progress {
|
||||
background-color: #27ae61 !important;
|
||||
}
|
||||
.board-color-relax .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e7faef;
|
||||
}
|
||||
@@ -1292,6 +1358,12 @@ THEME - Corteza
|
||||
border-bottom: 2px solid #568ba2;
|
||||
border-right: 2px solid #568ba2;
|
||||
}
|
||||
.board-color-corteza .checklist-progress-bar {
|
||||
background-color: #dce6ec !important;
|
||||
}
|
||||
.board-color-corteza .checklist-progress-bar .checklist-progress {
|
||||
background-color: #568ba2 !important;
|
||||
}
|
||||
.board-color-corteza .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #eef3f6;
|
||||
}
|
||||
@@ -1397,6 +1469,12 @@ THEME - Clear Blue
|
||||
border-bottom: 2px solid #499bea;
|
||||
border-right: 2px solid #499bea;
|
||||
}
|
||||
.board-color-clearblue .checklist-progress-bar {
|
||||
background-color: #daeefb !important;
|
||||
}
|
||||
.board-color-clearblue .checklist-progress-bar .checklist-progress {
|
||||
background-color: #499bea !important;
|
||||
}
|
||||
.board-color-clearblue .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e0fbff;
|
||||
}
|
||||
@@ -1425,7 +1503,7 @@ THEME - Clear Blue
|
||||
}
|
||||
.board-color-clearblue .list {
|
||||
background: rgba(255,255,255,0.35);
|
||||
margin: 10px;
|
||||
margin: 10px 0;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
}
|
||||
@@ -1660,6 +1738,12 @@ THEME - Natural
|
||||
border-bottom: 2px solid #596557;
|
||||
border-right: 2px solid #596557;
|
||||
}
|
||||
.board-color-natural .checklist-progress-bar {
|
||||
background-color: #dee0dd !important;
|
||||
}
|
||||
.board-color-natural .checklist-progress-bar .checklist-progress {
|
||||
background-color: #596557 !important;
|
||||
}
|
||||
.board-color-natural .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #eef0ee;
|
||||
}
|
||||
@@ -1770,6 +1854,12 @@ THEME - Modern
|
||||
border-bottom: 2px solid #2a80b8;
|
||||
border-right: 2px solid #2a80b8;
|
||||
}
|
||||
.board-color-modern .checklist-progress-bar {
|
||||
background-color: #d1e7f5 !important;
|
||||
}
|
||||
.board-color-modern .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2a80b8 !important;
|
||||
}
|
||||
.board-color-modern .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e8f3fa;
|
||||
}
|
||||
@@ -2062,6 +2152,12 @@ THEME - Modern Dark
|
||||
border-bottom: 2px solid #2a2a2a;
|
||||
border-right: 2px solid #2a2a2a;
|
||||
}
|
||||
.board-color-moderndark .checklist-progress-bar {
|
||||
background-color: #d1d1d1 !important;
|
||||
}
|
||||
.board-color-moderndark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2a2a2a !important;
|
||||
}
|
||||
.board-color-moderndark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #eaeaea;
|
||||
}
|
||||
@@ -2547,6 +2643,12 @@ THEME - Exodark
|
||||
border-bottom: 2px solid #dbdbdb!important;/*Fix contrast of checkbox*/
|
||||
border-right: 2px solid #dbdbdb!important;
|
||||
}
|
||||
.board-color-exodark .checklist-progress-bar {
|
||||
background-color: #cccccc !important;
|
||||
}
|
||||
.board-color-exodark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
.board-color-exodark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
@@ -2588,7 +2690,7 @@ THEME - Exodark
|
||||
background: #222;
|
||||
}
|
||||
.board-color-exodark .list {
|
||||
margin: 10px;
|
||||
margin: 10px 0;
|
||||
color: #fff;
|
||||
border-radius: 15px;
|
||||
background-color: #1c1c1c;
|
||||
@@ -3140,6 +3242,12 @@ THEME - Clean Dark
|
||||
margin-left: 3px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.board-color-cleandark .checklist-progress-bar {
|
||||
background-color: #6b6b78 !important;
|
||||
}
|
||||
.board-color-cleandark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #23232B !important;
|
||||
}
|
||||
|
||||
.board-color-cleandark .allBoards {
|
||||
white-space: nowrap;
|
||||
@@ -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;
|
||||
|
||||
@@ -505,73 +505,73 @@
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
}
|
||||
.mobile-mode .swimlane {
|
||||
display: block !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane .swimlane-header {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 0 1rem 0 !important;
|
||||
padding: 1rem !important;
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
font-weight: bold !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
}
|
||||
.mobile-mode .swimlane .swimlane-header {
|
||||
display: block !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
margin: 0 0 1rem 0 !important;
|
||||
padding: 1rem !important;
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
font-weight: bold !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane .lists {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
flex-direction: column !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
.mobile-mode .swimlane .lists {
|
||||
display: block !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
flex-direction: column !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-top: none !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
position: static !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
bottom: auto !important;
|
||||
transform: none !important;
|
||||
}
|
||||
.mobile-mode .list {
|
||||
display: block !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-top: none !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
position: static !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
bottom: auto !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list:first-child {
|
||||
margin-left: 0 !important;
|
||||
@@ -667,9 +667,9 @@
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
@@ -16,31 +16,29 @@ template(name="boardHeaderBar")
|
||||
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
|
||||
i.fa.fa-pencil-square-o
|
||||
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}" aria-label="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
if showStarCounter
|
||||
span
|
||||
= currentBoard.stars
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
span {{_ currentBoard.permission}}
|
||||
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
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(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
||||
i.fa.fa-sort
|
||||
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
|
||||
a.board-header-btn.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
if $eq watchLevel "watching"
|
||||
i.fa.fa-eye
|
||||
if $eq watchLevel "tracking"
|
||||
i.fa.fa-bell
|
||||
if $eq watchLevel "muted"
|
||||
i.fa.fa-bell-slash
|
||||
span {{_ watchLevel}}
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
if showStarCounter
|
||||
span.board-star-counter {{currentBoard.stars}}
|
||||
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
||||
i.fa.fa-sort
|
||||
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
|
||||
if isSortActive
|
||||
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
|
||||
i.fa.fa-times-thin
|
||||
@@ -61,25 +59,25 @@ template(name="boardHeaderBar")
|
||||
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
|
||||
i.fa.fa-pencil-square-o
|
||||
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
|
||||
a.board-header-btn.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
if $eq watchLevel "watching"
|
||||
i.fa.fa-eye
|
||||
if $eq watchLevel "tracking"
|
||||
i.fa.fa-bell
|
||||
if $eq watchLevel "muted"
|
||||
i.fa.fa-bell-slash
|
||||
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
||||
i.fa.fa-sort
|
||||
a.board-header-btn.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
if $eq watchLevel "watching"
|
||||
i.fa.fa-eye
|
||||
if $eq watchLevel "tracking"
|
||||
i.fa.fa-bell
|
||||
if $eq watchLevel "muted"
|
||||
i.fa.fa-bell-slash
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
||||
i.fa.fa-sort
|
||||
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
|
||||
if isSortActive
|
||||
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
|
||||
i.fa.fa-times-thin
|
||||
@@ -95,24 +93,28 @@ template(name="boardHeaderBar")
|
||||
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}}emphasis{{/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
|
||||
@@ -120,26 +122,37 @@ template(name="boardHeaderBar")
|
||||
i.fa.fa-trello
|
||||
if $eq boardView 'board-view-cal'
|
||||
i.fa.fa-calendar
|
||||
|
||||
if $eq boardView 'board-view-gantt'
|
||||
i.fa.fa-bar-chart
|
||||
span
|
||||
if $eq boardView 'board-view-swimlanes'
|
||||
| {{_ 'swimlanes'}}
|
||||
if $eq boardView 'board-view-lists'
|
||||
| {{_ 'lists'}}
|
||||
if $eq boardView 'board-view-cal'
|
||||
| {{_ 'calendar'}}
|
||||
if $eq boardView 'board-view-gantt'
|
||||
| {{_ 'gantt'}}
|
||||
if canModifyBoard
|
||||
a.board-header-btn.js-multiselection-activate(
|
||||
title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
|
||||
class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
|
||||
i.fa.fa-check-square-o
|
||||
span {{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}
|
||||
if MultiSelection.isActive
|
||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||
i.fa.fa-times-thin
|
||||
|
||||
.separator
|
||||
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
|
||||
i.fa.fa-navicon
|
||||
i.fa.fa-bars
|
||||
|
||||
template(name="boardVisibilityList")
|
||||
ul.pop-over-list
|
||||
li
|
||||
with "private"
|
||||
a.js-select-visibility
|
||||
i.fa.fa-lock.colorful
|
||||
i.fa.fa-lock
|
||||
| {{_ 'private'}}
|
||||
if visibilityCheck
|
||||
i.fa.fa-check
|
||||
@@ -148,7 +161,7 @@ template(name="boardVisibilityList")
|
||||
li
|
||||
with "public"
|
||||
a.js-select-visibility
|
||||
i.fa.fa-globe.colorful
|
||||
i.fa.fa-globe
|
||||
| {{_ 'public'}}
|
||||
if visibilityCheck
|
||||
i.fa.fa-check
|
||||
@@ -162,7 +175,7 @@ template(name="boardChangeWatchPopup")
|
||||
li
|
||||
with "watching"
|
||||
a.js-select-watch
|
||||
i.fa.fa-eye.colorful
|
||||
i.fa.fa-eye
|
||||
| {{_ 'watching'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
@@ -170,7 +183,7 @@ template(name="boardChangeWatchPopup")
|
||||
li
|
||||
with "tracking"
|
||||
a.js-select-watch
|
||||
i.fa.fa-bell.colorful
|
||||
i.fa.fa-bell
|
||||
| {{_ 'tracking'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
@@ -178,7 +191,7 @@ template(name="boardChangeWatchPopup")
|
||||
li
|
||||
with "muted"
|
||||
a.js-select-watch
|
||||
i.fa.fa-bell-slash.colorful
|
||||
i.fa.fa-bell-slash
|
||||
| {{_ 'muted'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
@@ -189,35 +202,130 @@ template(name="boardChangeViewPopup")
|
||||
li
|
||||
with "board-view-swimlanes"
|
||||
a.js-open-swimlanes-view
|
||||
i.fa.fa-th-large.colorful
|
||||
| {{_ 'board-view-swimlanes'}}
|
||||
i.fa.fa-th-large
|
||||
| {{_ 'swimlanes'}}
|
||||
if $eq Utils.boardView "board-view-swimlanes"
|
||||
i.fa.fa-check
|
||||
li
|
||||
with "board-view-lists"
|
||||
a.js-open-lists-view
|
||||
i.fa.fa-trello.colorful
|
||||
i.fa.fa-trello
|
||||
| {{_ 'board-view-lists'}}
|
||||
if $eq Utils.boardView "board-view-lists"
|
||||
i.fa.fa-check
|
||||
li
|
||||
with "board-view-cal"
|
||||
a.js-open-cal-view
|
||||
i.fa.fa-calendar.colorful
|
||||
i.fa.fa-calendar
|
||||
| {{_ 'board-view-cal'}}
|
||||
if $eq Utils.boardView "board-view-cal"
|
||||
i.fa.fa-check
|
||||
li
|
||||
with "board-view-gantt"
|
||||
a.js-open-gantt-view
|
||||
i.fa.fa-bar-chart
|
||||
| {{_ 'board-view-gantt'}}
|
||||
if $eq Utils.boardView "board-view-gantt"
|
||||
i.fa.fa-check
|
||||
|
||||
template(name="createBoard")
|
||||
form
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
|
||||
if visibilityMenuIsOpen.get
|
||||
if visibilityMenuIsOpen
|
||||
+boardVisibilityList
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility.get 'public'
|
||||
if $eq visibility 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span.fa.fa-lock.colorful
|
||||
= " "
|
||||
| {{{_ 'board-private-info'}}}
|
||||
a.js-change-visibility {{_ 'change'}}.
|
||||
a.flex.js-toggle-add-template-container
|
||||
.materialCheckBox#add-template-container
|
||||
span {{_ 'add-template-container'}}
|
||||
input.primary.wide(type="submit" value="{{_ 'create'}}")
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-import-board {{_ 'import'}}
|
||||
span.quiet
|
||||
| /
|
||||
a.js-board-template {{_ 'template'}}
|
||||
|
||||
template(name="createBoardPopup")
|
||||
form
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
|
||||
if visibilityMenuIsOpen
|
||||
+boardVisibilityList
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span.fa.fa-lock.colorful
|
||||
= " "
|
||||
| {{{_ 'board-private-info'}}}
|
||||
a.js-change-visibility {{_ 'change'}}.
|
||||
a.flex.js-toggle-add-template-container
|
||||
.materialCheckBox#add-template-container
|
||||
span {{_ 'add-template-container'}}
|
||||
input.primary.wide(type="submit" value="{{_ 'create'}}")
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-import-board {{_ 'import'}}
|
||||
span.quiet
|
||||
| /
|
||||
a.js-board-template {{_ 'template'}}
|
||||
|
||||
template(name="headerBarCreateBoardPopup")
|
||||
form
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
|
||||
if visibilityMenuIsOpen
|
||||
+boardVisibilityList
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span.fa.fa-lock.colorful
|
||||
= " "
|
||||
| {{{_ 'board-private-info'}}}
|
||||
a.js-change-visibility {{_ 'change'}}.
|
||||
a.flex.js-toggle-add-template-container
|
||||
.materialCheckBox#add-template-container
|
||||
span {{_ 'add-template-container'}}
|
||||
input.primary.wide(type="submit" value="{{_ 'create'}}")
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-import-board {{_ 'import'}}
|
||||
span.quiet
|
||||
| /
|
||||
a.js-board-template {{_ 'template'}}
|
||||
|
||||
// New popup for Template Container creation; shares the same form content
|
||||
template(name="createTemplateContainerPopup")
|
||||
form
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
|
||||
if visibilityMenuIsOpen
|
||||
+boardVisibilityList
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
@@ -238,19 +346,30 @@ template(name="createBoard")
|
||||
a.js-board-template {{_ 'template'}}
|
||||
|
||||
//template(name="listsortPopup")
|
||||
// h2
|
||||
// | {{_ 'list-sort-by'}}
|
||||
// hr
|
||||
// ul.pop-over-list
|
||||
// each value in allowedSortValues
|
||||
// li
|
||||
// a.js-sort-by(name="{{value.name}}")
|
||||
// if $eq sortby value.name
|
||||
// i(class="fa {{Direction}}")
|
||||
// | {{_ value.label }}{{_ value.shortLabel}}
|
||||
// if $eq sortby value.name
|
||||
// i(class="fa fa-check")
|
||||
|
||||
//
|
||||
h2
|
||||
//
|
||||
| {{_ 'list-sort-by'}}
|
||||
//
|
||||
hr
|
||||
//
|
||||
ul.pop-over-list
|
||||
//
|
||||
each value in allowedSortValues
|
||||
//
|
||||
li
|
||||
//
|
||||
a.js-sort-by(name="{{value.name}}")
|
||||
//
|
||||
if $eq sortby value.name
|
||||
//
|
||||
| {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
|
||||
//
|
||||
| {{_ value.label }}{{_ value.shortLabel}}
|
||||
//
|
||||
if $eq sortby value.name
|
||||
//
|
||||
i.fa.fa-check
|
||||
template(name="boardChangeTitlePopup")
|
||||
form
|
||||
label
|
||||
@@ -269,14 +388,22 @@ template(name="boardCreateRulePopup")
|
||||
template(name="cardsSortPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-sort-due {{_ 'due-date'}}
|
||||
a.js-sort-due
|
||||
i.fa.fa-calendar
|
||||
| {{_ 'due-date'}}
|
||||
hr
|
||||
li
|
||||
a.js-sort-title {{_ 'title-alphabetically'}}
|
||||
a.js-sort-title
|
||||
i.fa.fa-sort-alpha-asc
|
||||
| {{_ 'title-alphabetically'}}
|
||||
hr
|
||||
li
|
||||
a.js-sort-created-desc {{_ 'created-at-newest-first'}}
|
||||
a.js-sort-created-desc
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'created-at-newest-first'}}
|
||||
hr
|
||||
li
|
||||
a.js-sort-created-asc {{_ 'created-at-oldest-first'}}
|
||||
a.js-sort-created-asc
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'created-at-oldest-first'}}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import dragscroll from '@wekanteam/dragscroll';
|
||||
import getSlug from 'limax';
|
||||
import Boards from '/models/boards';
|
||||
import Swimlanes from '/models/swimlanes';
|
||||
import TableVisibilityModeSettings from '/models/tableVisibilityModeSettings';
|
||||
import { Filter } from '/client/lib/filter';
|
||||
import { MultiSelection } from '/client/lib/multiSelection';
|
||||
import { getSidebarInstance } from '/client/features/sidebar/service';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
/*
|
||||
const DOWNCLS = 'fa-sort-down';
|
||||
@@ -9,7 +18,8 @@ const UPCLS = 'fa-sort-up';
|
||||
const sortCardsBy = new ReactiveVar('');
|
||||
|
||||
Template.boardChangeTitlePopup.events({
|
||||
submit(event, templateInstance) {
|
||||
async submit(event, templateInstance) {
|
||||
event.preventDefault();
|
||||
const newTitle = templateInstance
|
||||
.$('.js-board-name')
|
||||
.val()
|
||||
@@ -19,22 +29,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();
|
||||
@@ -46,124 +56,130 @@ BlazeComponent.extendComponent({
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return currentBoard && currentBoard.stars >= 2;
|
||||
},
|
||||
/*
|
||||
showSort() {
|
||||
return ReactiveCache.getCurrentUser().hasSortBy();
|
||||
},
|
||||
directionClass() {
|
||||
return this.currentDirection() === -1 ? DOWNCLS : UPCLS;
|
||||
},
|
||||
changeDirection() {
|
||||
const direction = 0 - this.currentDirection() === -1 ? '-' : '';
|
||||
Meteor.call('setListSortBy', direction + this.currentListSortBy());
|
||||
},
|
||||
currentDirection() {
|
||||
return ReactiveCache.getCurrentUser().getListSortByDirection();
|
||||
},
|
||||
currentListSortBy() {
|
||||
return ReactiveCache.getCurrentUser().getListSortBy();
|
||||
},
|
||||
listSortShortDesc() {
|
||||
return `list-label-short-${this.currentListSortBy()}`;
|
||||
},
|
||||
*/
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
|
||||
'click .js-star-board'() {
|
||||
ReactiveCache.getCurrentUser().toggleBoardStar(Session.get('currentBoard'));
|
||||
},
|
||||
'click .js-open-board-menu': Popup.open('boardMenu'),
|
||||
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
|
||||
'click .js-watch-board': Popup.open('boardChangeWatch'),
|
||||
'click .js-open-archived-board'() {
|
||||
Modal.open('archivedBoards');
|
||||
},
|
||||
'click .js-toggle-board-view': Popup.open('boardChangeView'),
|
||||
'click .js-toggle-sidebar'() {
|
||||
console.log('Hamburger menu clicked');
|
||||
// Use the same approach as keyboard shortcuts
|
||||
if (typeof Sidebar !== 'undefined' && Sidebar && typeof Sidebar.toggle === 'function') {
|
||||
console.log('Using Sidebar.toggle()');
|
||||
Sidebar.toggle();
|
||||
} else {
|
||||
console.warn('Sidebar not available, trying alternative approach');
|
||||
// Try to trigger the sidebar through the global Blaze helper
|
||||
if (typeof Blaze !== 'undefined' && Blaze._globalHelpers && Blaze._globalHelpers.Sidebar) {
|
||||
const sidebar = Blaze._globalHelpers.Sidebar();
|
||||
if (sidebar && typeof sidebar.toggle === 'function') {
|
||||
console.log('Using Blaze helper Sidebar.toggle()');
|
||||
sidebar.toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-open-filter-view'() {
|
||||
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();
|
||||
},
|
||||
isSortActive() {
|
||||
return Session.get('sortBy') ? true : false;
|
||||
},
|
||||
sortCardsIcon() {
|
||||
const sortBy = Session.get('sortBy');
|
||||
if (!sortBy) {
|
||||
return '🃏'; // Card icon when nothing is selected
|
||||
}
|
||||
|
||||
// Determine which sort option is active based on sortBy object
|
||||
if (sortBy.dueAt) {
|
||||
return '📅'; // Due date icon
|
||||
} else if (sortBy.title) {
|
||||
return '🔤'; // Alphabet icon
|
||||
} else if (sortBy.createdAt) {
|
||||
return sortBy.createdAt === 1 ? '⬆️' : '⬇️'; // Up/down arrow based on direction
|
||||
}
|
||||
|
||||
return '🃏'; // Default card icon
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardHeaderBar.events({
|
||||
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
|
||||
'click .js-star-board'() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
if (boardId) {
|
||||
Meteor.call('toggleBoardStar', boardId);
|
||||
}
|
||||
},
|
||||
'click .js-open-board-menu': Popup.open('boardMenu'),
|
||||
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
|
||||
'click .js-watch-board': Popup.open('boardChangeWatch'),
|
||||
'click .js-open-archived-board'() {
|
||||
Modal.open('archivedBoards');
|
||||
},
|
||||
'click .js-toggle-board-view': Popup.open('boardChangeView'),
|
||||
'click .js-toggle-sidebar'() {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Hamburger menu clicked');
|
||||
}
|
||||
// Use the same approach as keyboard shortcuts
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar && typeof sidebar.toggle === 'function') {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Using Sidebar.toggle()');
|
||||
}
|
||||
sidebar.toggle();
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('Sidebar not available, trying alternative approach');
|
||||
}
|
||||
// Try to trigger the sidebar through the global Blaze helper
|
||||
if (typeof Blaze !== 'undefined' && Blaze._globalHelpers && Blaze._globalHelpers.Sidebar) {
|
||||
const blazeSidebar = Blaze._globalHelpers.Sidebar();
|
||||
if (blazeSidebar && typeof blazeSidebar.toggle === 'function') {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Using Blaze helper Sidebar.toggle()');
|
||||
}
|
||||
blazeSidebar.toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-open-filter-view'() {
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar) {
|
||||
sidebar.setView('filter');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
},
|
||||
'click .js-sort-cards': Popup.open('cardsSort'),
|
||||
/*
|
||||
'click .js-open-sort-view'(evt) {
|
||||
const target = evt.target;
|
||||
if (target.tagName === 'I') {
|
||||
// click on the text, popup choices
|
||||
this.changeDirection();
|
||||
} else {
|
||||
// change the sort order
|
||||
Popup.open('listsort')(evt);
|
||||
}
|
||||
},
|
||||
*/
|
||||
'click .js-filter-reset'(event) {
|
||||
event.stopPropagation();
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar) {
|
||||
sidebar.setView();
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
Filter.reset();
|
||||
},
|
||||
'click .js-sort-reset'() {
|
||||
Session.set('sortBy', '');
|
||||
},
|
||||
'click .js-open-search-view'() {
|
||||
const sidebar = getSidebarInstance();
|
||||
if (sidebar) {
|
||||
sidebar.setView('search');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
},
|
||||
'click .js-multiselection-activate'() {
|
||||
const currentCard = Utils.getCurrentCardId();
|
||||
MultiSelection.activate();
|
||||
if (currentCard) {
|
||||
MultiSelection.add(currentCard);
|
||||
}
|
||||
},
|
||||
'click .js-multiselection-reset'(event) {
|
||||
event.stopPropagation();
|
||||
MultiSelection.disable();
|
||||
},
|
||||
'click .js-log-in'() {
|
||||
FlowRouter.go('atSignIn');
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardChangeViewPopup.events({
|
||||
@@ -179,196 +195,235 @@ Template.boardChangeViewPopup.events({
|
||||
Utils.setBoardView('board-view-cal');
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
const CreateBoard = BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'createBoard';
|
||||
},
|
||||
|
||||
onCreated() {
|
||||
this.visibilityMenuIsOpen = new ReactiveVar(false);
|
||||
this.visibility = new ReactiveVar('private');
|
||||
this.boardId = new ReactiveVar('');
|
||||
Meteor.subscribe('tableVisibilityModeSettings');
|
||||
},
|
||||
|
||||
notAllowPrivateVisibilityOnly(){
|
||||
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
|
||||
},
|
||||
|
||||
visibilityCheck() {
|
||||
return this.currentData() === this.visibility.get();
|
||||
},
|
||||
|
||||
setVisibility(visibility) {
|
||||
this.visibility.set(visibility);
|
||||
this.visibilityMenuIsOpen.set(false);
|
||||
},
|
||||
|
||||
toggleVisibilityMenu() {
|
||||
this.visibilityMenuIsOpen.set(!this.visibilityMenuIsOpen.get());
|
||||
},
|
||||
|
||||
toggleAddTemplateContainer() {
|
||||
$('#add-template-container').toggleClass('is-checked');
|
||||
},
|
||||
|
||||
onSubmit(event) {
|
||||
event.preventDefault();
|
||||
const title = this.find('.js-new-board-title').value;
|
||||
|
||||
const addTemplateContainer = $('#add-template-container.is-checked').length > 0;
|
||||
if (addTemplateContainer) {
|
||||
//const templateContainerId = Meteor.call('setCreateTemplateContainer');
|
||||
//Utils.goBoardId(templateContainerId);
|
||||
//alert('niinku template ' + Meteor.call('setCreateTemplateContainer'));
|
||||
|
||||
this.boardId.set(
|
||||
Boards.insert({
|
||||
// title: TAPi18n.__('templates'),
|
||||
title: title,
|
||||
permission: 'private',
|
||||
type: 'template-container',
|
||||
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',
|
||||
},
|
||||
);
|
||||
|
||||
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(),
|
||||
});
|
||||
|
||||
Utils.goBoardId(this.boardId.get());
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-select-visibility'() {
|
||||
this.setVisibility(this.currentData());
|
||||
},
|
||||
'click .js-change-visibility': this.toggleVisibilityMenu,
|
||||
'click .js-import': Popup.open('boardImportBoard'),
|
||||
submit: this.onSubmit,
|
||||
'click .js-import-board': Popup.open('chooseBoardSource'),
|
||||
'click .js-board-template': Popup.open('searchElement'),
|
||||
'click .js-toggle-add-template-container': this.toggleAddTemplateContainer,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('createBoardPopup');
|
||||
|
||||
(class HeaderBarCreateBoard extends CreateBoard {
|
||||
onSubmit(event) {
|
||||
super.onSubmit(event);
|
||||
// Immediately star boards crated with the headerbar popup.
|
||||
ReactiveCache.getCurrentUser().toggleBoardStar(this.boardId.get());
|
||||
}
|
||||
}.register('headerBarCreateBoardPopup'));
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
notAllowPrivateVisibilityOnly(){
|
||||
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
|
||||
},
|
||||
visibilityCheck() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return this.currentData() === currentBoard.permission;
|
||||
},
|
||||
|
||||
selectBoardVisibility() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const visibility = this.currentData();
|
||||
currentBoard.setVisibility(visibility);
|
||||
'click .js-open-gantt-view'() {
|
||||
Utils.setBoardView('board-view-gantt');
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-select-visibility': this.selectBoardVisibility,
|
||||
},
|
||||
];
|
||||
// Shared setup for all create board popups
|
||||
function setupCreateBoardState(tpl) {
|
||||
tpl.visibilityMenuIsOpen = new ReactiveVar(false);
|
||||
tpl.visibility = new ReactiveVar('private');
|
||||
tpl.boardId = new ReactiveVar('');
|
||||
Meteor.subscribe('tableVisibilityModeSettings');
|
||||
}
|
||||
|
||||
function createBoardHelpers() {
|
||||
return {
|
||||
visibilityMenuIsOpen() {
|
||||
return Template.instance().visibilityMenuIsOpen.get();
|
||||
},
|
||||
visibility() {
|
||||
return Template.instance().visibility.get();
|
||||
},
|
||||
notAllowPrivateVisibilityOnly() {
|
||||
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
|
||||
},
|
||||
visibilityCheck() {
|
||||
return Template.currentData() === Template.instance().visibility.get();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function createBoardSubmit(tpl, event) {
|
||||
event.preventDefault();
|
||||
const title = tpl.find('.js-new-board-title').value;
|
||||
const slug = getSlug(title) || 'board';
|
||||
|
||||
const addTemplateContainer = $('#add-template-container.is-checked').length > 0;
|
||||
if (addTemplateContainer) {
|
||||
tpl.boardId.set(
|
||||
await Meteor.callAsync('createBoardWithInitialSwimlanes', {
|
||||
title,
|
||||
slug,
|
||||
permission: 'private',
|
||||
type: 'template-container',
|
||||
migrationVersion: 1,
|
||||
swimlanes: [
|
||||
{ title: 'Card Templates', sort: 1, type: 'template-container' },
|
||||
{ title: 'List Templates', sort: 2, type: 'template-container' },
|
||||
{ title: 'Board Templates', sort: 3, type: 'template-container' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
// Assign to space if one was selected
|
||||
const spaceId = Session.get('createBoardInWorkspace');
|
||||
if (spaceId) {
|
||||
Meteor.call('assignBoardToWorkspace', tpl.boardId.get(), spaceId, (err) => {
|
||||
if (err) console.error('Error assigning board to space:', err);
|
||||
});
|
||||
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||
}
|
||||
|
||||
FlowRouter.go('board', { id: tpl.boardId.get(), slug });
|
||||
|
||||
} else {
|
||||
const visibility = tpl.visibility.get();
|
||||
|
||||
tpl.boardId.set(
|
||||
await Meteor.callAsync('createBoardWithInitialSwimlanes', {
|
||||
title,
|
||||
slug,
|
||||
permission: visibility,
|
||||
migrationVersion: 1,
|
||||
swimlanes: [{ title: 'Default' }],
|
||||
}),
|
||||
);
|
||||
|
||||
// Assign to space if one was selected
|
||||
const spaceId = Session.get('createBoardInWorkspace');
|
||||
if (spaceId) {
|
||||
Meteor.call('assignBoardToWorkspace', tpl.boardId.get(), spaceId, (err) => {
|
||||
if (err) console.error('Error assigning board to space:', err);
|
||||
});
|
||||
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||
}
|
||||
|
||||
FlowRouter.go('board', { id: tpl.boardId.get(), slug });
|
||||
}
|
||||
}
|
||||
|
||||
function createBoardEvents() {
|
||||
return {
|
||||
'click .js-select-visibility'(event, tpl) {
|
||||
tpl.visibility.set(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);
|
||||
},
|
||||
}).register('boardChangeVisibilityPopup');
|
||||
'click .js-change-visibility'(event, tpl) {
|
||||
tpl.visibilityMenuIsOpen.set(!tpl.visibilityMenuIsOpen.get());
|
||||
},
|
||||
'click .js-import': Popup.open('boardImportBoard'),
|
||||
async submit(event, tpl) {
|
||||
await createBoardSubmit(tpl, event);
|
||||
// Immediately star boards created with the headerbar popup.
|
||||
await ReactiveCache.getCurrentUser().toggleBoardStar(tpl.boardId.get());
|
||||
},
|
||||
'click .js-import-board': Popup.open('chooseBoardSource'),
|
||||
'click .js-board-template': Popup.open('searchElement'),
|
||||
'click .js-toggle-add-template-container'() {
|
||||
$('#add-template-container').toggleClass('is-checked');
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.boardChangeVisibilityPopup.onCreated(function () {
|
||||
Meteor.subscribe('tableVisibilityModeSettings');
|
||||
});
|
||||
|
||||
Template.boardChangeVisibilityPopup.helpers({
|
||||
notAllowPrivateVisibilityOnly(){
|
||||
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
|
||||
},
|
||||
visibilityCheck() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return this === currentBoard.permission;
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardChangeVisibilityPopup.events({
|
||||
'click .js-select-visibility'() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const visibility = Template.currentData();
|
||||
if (typeof visibility === 'string') {
|
||||
currentBoard.setVisibility(visibility);
|
||||
Popup.back();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardChangeWatchPopup.helpers({
|
||||
watchLevel() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return currentBoard.getWatchLevel(Meteor.userId());
|
||||
},
|
||||
|
||||
watchCheck() {
|
||||
return this.currentData() === this.watchLevel();
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
return this === currentBoard.getWatchLevel(Meteor.userId());
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-select-watch'() {
|
||||
const level = this.currentData();
|
||||
Meteor.call(
|
||||
'watch',
|
||||
'board',
|
||||
Session.get('currentBoard'),
|
||||
level,
|
||||
(err, ret) => {
|
||||
if (!err && ret) Popup.back();
|
||||
},
|
||||
);
|
||||
Template.boardChangeWatchPopup.events({
|
||||
'click .js-select-watch'() {
|
||||
const level = 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();
|
||||
@@ -436,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 {
|
||||
@@ -8,6 +8,339 @@
|
||||
padding: 1vh 0;
|
||||
}
|
||||
|
||||
/* Two-column layout for All Boards */
|
||||
.boards-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.boards-left-menu {
|
||||
border-right: 1px solid #e0e0e0;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.boards-left-menu ul.menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.boards-left-menu .menu-item {
|
||||
margin: 4px 0;
|
||||
}
|
||||
.boards-left-menu .menu-item a {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.boards-left-menu .menu-item .menu-label {
|
||||
flex: 1;
|
||||
}
|
||||
.boards-left-menu .menu-item .menu-count {
|
||||
background: #ddd;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.boards-left-menu .menu-item.active a,
|
||||
.boards-left-menu .menu-item a:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.boards-left-menu .menu-item.active .menu-count {
|
||||
background: #bbb;
|
||||
}
|
||||
|
||||
/* Drag-over state for menu items (for dropping boards on Remaining) */
|
||||
.boards-left-menu .menu-item a.drag-over {
|
||||
background: #d0e8ff;
|
||||
border: 2px dashed #2196F3;
|
||||
}
|
||||
|
||||
.workspaces-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: bold;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.workspaces-header .js-add-space,
|
||||
.workspaces-header .js-add-workspace {
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.workspace-tree {
|
||||
list-style: none;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.workspace-node {
|
||||
margin: 2px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.workspace-node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.workspace-node.dragging > .workspace-node-content {
|
||||
opacity: 0.5;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.workspace-node.drag-over > .workspace-node-content {
|
||||
background: #d0e8ff;
|
||||
border: 2px dashed #2196F3;
|
||||
}
|
||||
|
||||
.workspace-drag-handle {
|
||||
cursor: grab;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
padding: 0 4px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.workspace-drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.workspace-node .js-select-space {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-icon .workspace-icon-value,
|
||||
.workspace-node .workspace-icon .workspace-icon-value p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-icon .emoji-icon {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-count {
|
||||
background: #ddd;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.workspace-node .js-edit-space,
|
||||
.workspace-node .js-add-subspace {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.workspace-node .js-edit-space:hover,
|
||||
.workspace-node .js-add-subspace:hover {
|
||||
opacity: 1;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.workspace-node .js-open-workspace-menu {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.workspace-node .js-open-workspace-menu:hover {
|
||||
opacity: 1;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.workspace-actions-form label,
|
||||
.workspace-subspace-form label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.workspace-actions-form input[type='text'],
|
||||
.workspace-subspace-form input[type='text'] {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.workspace-actions-form .workspace-color-label {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.workspace-actions-form .workspace-color-list {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.workspace-delete-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.workspace-delete-confirm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workspace-node.active > .workspace-node-content .js-select-space,
|
||||
.workspace-node > .workspace-node-content:hover .js-select-space {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.workspace-node.active .workspace-count {
|
||||
background: #bbb;
|
||||
}
|
||||
|
||||
.boards-right-grid {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.boards-path-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.boards-path-header .path-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.boards-path-header .multiselection-hint {
|
||||
background: #FFF3CD;
|
||||
color: #856404;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
border: 1px solid #FFE69C;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.boards-path-header .path-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.boards-path-header .path-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.boards-path-header .path-text {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn {
|
||||
padding: 6px 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn:hover {
|
||||
background: #f0f0f0;
|
||||
border-color: #bbb;
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn.emphasis {
|
||||
background: #2196F3;
|
||||
color: #fff;
|
||||
border-color: #2196F3;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.5);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn.emphasis:hover {
|
||||
background: #1976D2;
|
||||
box-shadow: 0 3px 12px rgba(33, 150, 243, 0.7);
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn-close {
|
||||
padding: 4px 10px;
|
||||
background: #f44336;
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-left: 10px; /* Extra space between MultiSelection toggle and Remove Filter */
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn-close:hover {
|
||||
background: #d32f2f;
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -103,26 +436,38 @@
|
||||
transform: rotate(4deg);
|
||||
display: block !important;
|
||||
}
|
||||
.board-list li.starred .fa-star,
|
||||
.board-list li.starred .fa-star-o {
|
||||
.board-list li.starred .is-star-active,
|
||||
.board-list li.starred .is-not-star-active {
|
||||
opacity: 1;
|
||||
color: #ffd700;
|
||||
}
|
||||
/* Show star icon on hover even for non-starred boards */
|
||||
.board-list li:hover .is-star-active,
|
||||
.board-list li:hover .is-not-star-active {
|
||||
opacity: 1;
|
||||
}
|
||||
.board-list .board-list-item {
|
||||
overflow: hidden;
|
||||
background-color: #999;
|
||||
background-color: inherit; /* Inherit board color from parent li.js-board */
|
||||
color: #f6f6f6;
|
||||
min-height: 100px;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
border-radius: 3px;
|
||||
border-radius: 0; /* No border-radius - parent .js-board has it */
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
padding: 8px;
|
||||
margin: 8px;
|
||||
padding: 36px 8px 32px 8px; /* Top padding for drag handle, bottom for checkbox */
|
||||
margin: 0; /* No margin - moved to parent .js-board */
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.board-list .board-list-item > .js-open-board {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
}
|
||||
.board-list .board-list-item.template-container {
|
||||
border: 4px solid #fff;
|
||||
}
|
||||
@@ -150,13 +495,27 @@
|
||||
.board-list .js-add-board .label {
|
||||
font-weight: normal;
|
||||
line-height: 56px;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #999; /* Darker background for better text contrast */
|
||||
border-radius: 3px;
|
||||
padding: 36px 8px 32px 8px;
|
||||
color: #fff; /* White text */
|
||||
}
|
||||
.board-list .js-add-board :hover {
|
||||
background-color: #939393;
|
||||
.board-list .js-add-board .label i {
|
||||
color: #fff; /* White icon */
|
||||
}
|
||||
.board-list .fa-star,
|
||||
.board-list .fa-star-o {
|
||||
bottom: 0;
|
||||
.board-list .js-add-board .label:hover {
|
||||
background-color: #808080; /* Even darker on hover */
|
||||
}
|
||||
.board-list .js-add-board .label:hover i {
|
||||
color: #fff; /* Keep icon white on hover */
|
||||
}
|
||||
.board-list .is-star-active,
|
||||
.board-list .is-not-star-active {
|
||||
top: 0;
|
||||
font-size: 14px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
@@ -164,7 +523,6 @@
|
||||
padding: 9px 9px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition-duration: 0.15s;
|
||||
transition-property: color, font-size, background;
|
||||
}
|
||||
@@ -212,32 +570,133 @@
|
||||
transition-duration: 0.15s;
|
||||
transition-property: color, font-size, background;
|
||||
}
|
||||
.board-list li:hover a:hover .fa-star,
|
||||
.board-list li:hover a:hover .is-star-active,
|
||||
.board-list li:hover a:hover .fa-clone,
|
||||
.board-list li:hover a:hover .fa-archive,
|
||||
.board-list li:hover a:hover .fa-star-o {
|
||||
.board-list li:hover a:hover .is-not-star-active {
|
||||
color: #fff;
|
||||
}
|
||||
.board-list li:hover a .fa-star,
|
||||
.board-list li:hover a .is-star-active,
|
||||
.board-list li:hover a .fa-clone,
|
||||
.board-list li:hover a .fa-archive,
|
||||
.board-list li:hover a .fa-star-o {
|
||||
.board-list li:hover a .is-not-star-active {
|
||||
color: #fff;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.board-list li:hover a .fa-star:hover,
|
||||
.board-list li:hover a .is-star-active:hover,
|
||||
.board-list li:hover a .fa-clone:hover,
|
||||
.board-list li:hover a .fa-archive:hover,
|
||||
.board-list li:hover a .fa-star-o:hover {
|
||||
.board-list li:hover a .is-not-star-active:hover {
|
||||
font-size: 18px;
|
||||
opacity: 1;
|
||||
}
|
||||
.board-list li:hover a .fa-star.is-star-active,
|
||||
.board-list li:hover a .fa-clone.is-star-active,
|
||||
.board-list li:hover a .fa-archive.is-star-active,
|
||||
.board-list li:hover a .fa-star-o.is-star-active {
|
||||
.board-list li:hover a .is-star-active,
|
||||
.board-list li:hover a .fa-clone,
|
||||
.board-list li:hover a .fa-archive,
|
||||
.board-list li:hover a .is-not-star-active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Board drag handle - always visible and positioned at top */
|
||||
.board-list .board-handle {
|
||||
position: absolute;
|
||||
padding: 4px 6px;
|
||||
top: 4px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
background: rgba(0,0,0,0.4);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
transition: background-color 0.2s ease;
|
||||
cursor: grab;
|
||||
opacity: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.board-list .board-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.board-list .board-handle:hover {
|
||||
background: rgba(255, 255, 0, 0.8) !important;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Multiselection checkbox on board items */
|
||||
.board-list .board-list-item .multi-selection-checkbox {
|
||||
position: absolute !important;
|
||||
bottom: 4px !important;
|
||||
left: 4px !important;
|
||||
top: auto !important;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid #fff;
|
||||
background: rgba(0,0,0,0.5);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
z-index: 11;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
transform: none !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.board-list .board-list-item .multi-selection-checkbox:hover {
|
||||
background: rgba(0,0,0,0.7);
|
||||
transform: scale(1.15) !important;
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.board-list .board-list-item .multi-selection-checkbox.is-checked {
|
||||
background: #3cb500;
|
||||
border-color: #3cb500;
|
||||
box-shadow: 0 2px 8px rgba(60, 181, 0, 0.6);
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
top: auto !important;
|
||||
left: 4px !important;
|
||||
transform: none !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.board-list .board-list-item .multi-selection-checkbox.is-checked::after {
|
||||
content: '✓';
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Grey checkboxes when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .board-list .board-list-item .multi-selection-checkbox.is-checked {
|
||||
background: #7a7a7a;
|
||||
border-color: #7a7a7a;
|
||||
box-shadow: 0 2px 8px rgba(122, 122, 122, 0.6);
|
||||
}
|
||||
|
||||
body.grey-icons-enabled .board-list.is-multiselection-active .js-board.is-checked {
|
||||
outline: 4px solid #7a7a7a;
|
||||
box-shadow: 0 4px 12px rgba(122, 122, 122, 0.4);
|
||||
}
|
||||
|
||||
.board-list.is-multiselection-active .js-board.is-checked {
|
||||
outline: 4px solid #3cb500;
|
||||
outline-offset: -4px;
|
||||
box-shadow: 0 4px 12px rgba(60, 181, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Visual hint when multiselection is active */
|
||||
.board-list.is-multiselection-active .board-list-item {
|
||||
border: 2px dashed rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
|
||||
.board-backgrounds-list .board-background-select {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
@@ -271,8 +730,19 @@
|
||||
}
|
||||
.board-backgrounds-list .board-background-select .background-box i.fa-check {
|
||||
font-size: 25px;
|
||||
color: #fff;
|
||||
color: #3cb500;
|
||||
}
|
||||
/* Grey check icons when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .board-backgrounds-list .board-background-select .background-box i.fa-check {
|
||||
color: #7a7a7a;
|
||||
}
|
||||
|
||||
/* Prevent Grey Icons from affecting checkmarks in background color list */
|
||||
body.grey-icons-enabled .checkmark-no-grey {
|
||||
filter: none !important;
|
||||
-webkit-filter: none !important;
|
||||
}
|
||||
|
||||
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
|
||||
.board-list.mobile-view {
|
||||
height: calc(100vh - 120px);
|
||||
@@ -361,6 +831,18 @@
|
||||
min-height: 100vh; /* Force content to be tall enough to scroll */
|
||||
}
|
||||
|
||||
/* Hide archive and clone board buttons in mobile view */
|
||||
.board-list.mobile-view .js-archive-board,
|
||||
.board-list.mobile-view .js-clone-board {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Change board drag handle to up-down arrow in mobile view */
|
||||
.board-list.mobile-view .board-handle.fa-arrows::before {
|
||||
content: "↕️" !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
.board-list.mobile-view::after {
|
||||
content: '';
|
||||
display: block;
|
||||
@@ -371,7 +853,8 @@
|
||||
screen and (max-device-width: 800px),
|
||||
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
|
||||
screen and (max-width: 800px) and (orientation: portrait),
|
||||
screen and (max-width: 800px) and (orientation: landscape) {
|
||||
screen and (max-width: 800px) and (orientation: landscape),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||
.board-list {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
@@ -457,7 +940,8 @@
|
||||
screen and (max-device-width: 800px),
|
||||
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
|
||||
screen and (max-width: 800px) and (orientation: portrait),
|
||||
screen and (max-width: 800px) and (orientation: landscape) {
|
||||
screen and (max-width: 800px) and (orientation: landscape),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||
.wrapper {
|
||||
font-size: 2em !important; /* 2x bigger base font size for All Boards page */
|
||||
}
|
||||
@@ -725,9 +1209,62 @@
|
||||
#resetBtn {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#resetBtn.filter-reset-btn {
|
||||
background: #f44336;
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
#resetBtn.filter-reset-btn:hover {
|
||||
background: #d32f2f;
|
||||
}
|
||||
|
||||
#resetBtn.filter-reset-btn .reset-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.js-board {
|
||||
display: block;
|
||||
background-color: #999; /* Default gray background if no color class is applied */
|
||||
border-radius: 3px; /* Rounded corners for board items */
|
||||
overflow: hidden; /* Ensure children respect rounded corners */
|
||||
margin: 8px; /* Space between board items */
|
||||
}
|
||||
|
||||
/* Reset background for add-board button */
|
||||
.js-add-board {
|
||||
background-color: transparent !important;
|
||||
margin: 8px !important; /* Keep margin for add-board */
|
||||
}
|
||||
|
||||
/* Apply board colors to li.js-board parent instead of just the link */
|
||||
.board-list .board-color-nephritis { background-color: #27ae60; }
|
||||
.board-list .board-color-pomegranate { background-color: #c0392b; }
|
||||
.board-list .board-color-belize { background-color: #2980b9; }
|
||||
.board-list .board-color-wisteria { background-color: #8e44ad; }
|
||||
.board-list .board-color-midnight { background-color: #2c3e50; }
|
||||
.board-list .board-color-pumpkin { background-color: #e67e22; }
|
||||
.board-list .board-color-moderatepink { background-color: #cd5a91; }
|
||||
.board-list .board-color-strongcyan { background-color: #00aecc; }
|
||||
.board-list .board-color-limegreen { background-color: #4bbf6b; }
|
||||
.board-list .board-color-dark { background-color: #2c3e51; }
|
||||
.board-list .board-color-relax { background-color: #27ae61; }
|
||||
.board-list .board-color-corteza { background-color: #568ba2; }
|
||||
.board-list .board-color-clearblue { background-color: #3498db; }
|
||||
.board-list .board-color-natural { background-color: #596557; }
|
||||
.board-list .board-color-modern { background-color: #2a80b8; }
|
||||
.board-list .board-color-moderndark { background-color: #2a2a2a; }
|
||||
.board-list .board-color-exodark { background-color: #222; }
|
||||
|
||||
.minicard-members {
|
||||
padding: 6px 0 6px 8px;
|
||||
width: 100%;
|
||||
@@ -757,7 +1294,8 @@
|
||||
screen and (max-device-width: 800px),
|
||||
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
|
||||
screen and (max-width: 800px) and (orientation: portrait),
|
||||
screen and (max-width: 800px) and (orientation: landscape) {
|
||||
screen and (max-width: 800px) and (orientation: landscape),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||
.wrapper {
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
@@ -824,5 +1362,17 @@
|
||||
#content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Hide archive and clone board buttons in mobile view */
|
||||
.board-list .js-archive-board,
|
||||
.board-list .js-clone-board {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Change board drag handle to up-down arrow in mobile view */
|
||||
.board-list .board-handle.fa-arrows::before {
|
||||
content: "↕️" !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,155 +2,258 @@ template(name="boardList")
|
||||
.wrapper
|
||||
.board-list-header
|
||||
|
||||
ul.AllBoardTeamsOrgs
|
||||
li.AllBoardTeams
|
||||
if userHasTeams
|
||||
select.js-AllBoardTeams#jsAllBoardTeams("multiple")
|
||||
option(value="-1") {{_ 'teams'}} :
|
||||
each teamsDatas
|
||||
option(value="{{teamId}}") {{_ teamDisplayName}}
|
||||
.boards-layout
|
||||
// Left menu
|
||||
.boards-left-menu
|
||||
ul.menu
|
||||
li(class="menu-item {{#if isSelectedMenu 'starred'}}active{{/if}}")
|
||||
a.js-select-menu(data-type="starred")
|
||||
span.menu-label
|
||||
span.emoji-icon
|
||||
i.fa.fa-star
|
||||
| {{_ 'allboards.starred'}}
|
||||
span.menu-count {{menuItemCount 'starred'}}
|
||||
li(class="menu-item {{#if isSelectedMenu 'templates'}}active{{/if}}")
|
||||
a.js-select-menu(data-type="templates")
|
||||
span.menu-label
|
||||
span.emoji-icon
|
||||
i.fa.fa-clipboard
|
||||
| {{_ 'allboards.templates'}}
|
||||
span.menu-count {{menuItemCount 'templates'}}
|
||||
li(class="menu-item {{#if isSelectedMenu 'remaining'}}active{{/if}}")
|
||||
a.js-select-menu(data-type="remaining")
|
||||
span.menu-label
|
||||
span.emoji-icon
|
||||
i.fa.fa-folder
|
||||
| {{_ 'allboards.remaining'}}
|
||||
span.menu-count {{menuItemCount 'remaining'}}
|
||||
.workspaces-header
|
||||
span
|
||||
span.emoji-icon
|
||||
i.fa.fa-folder-open
|
||||
| {{_ 'allboards.workspaces'}}
|
||||
a.js-add-workspace(title="{{_ 'allboards.add-workspace'}}") +
|
||||
// Workspaces tree
|
||||
+workspaceTree(nodes=workspacesTree selectedWorkspaceId=selectedWorkspaceId)
|
||||
|
||||
li.AllBoardOrgs
|
||||
if userHasOrgs
|
||||
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
|
||||
option(value="-1") {{_ 'organizations'}} :
|
||||
each orgsDatas
|
||||
option(value="{{orgId}}") {{orgDisplayName}}
|
||||
// Existing filter by orgs/teams (kept)
|
||||
ul.AllBoardTeamsOrgs
|
||||
li.AllBoardTeams
|
||||
if userHasTeams
|
||||
select.js-AllBoardTeams#jsAllBoardTeams("multiple")
|
||||
option(value="-1") {{_ 'teams'}} :
|
||||
each teamsDatas
|
||||
option(value="{{teamId}}") {{_ teamDisplayName}}
|
||||
|
||||
//li.AllBoardTemplates
|
||||
// if userHasTemplates
|
||||
// select.js-AllBoardTemplates#jsAllBoardTemplates("multiple")
|
||||
// option(value="-1") {{_ 'templates'}} :
|
||||
// each templatesDatas
|
||||
// option(value="{{templateId}}") {{_ templateDisplayName}}
|
||||
li.AllBoardOrgs
|
||||
if userHasOrgs
|
||||
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
|
||||
option(value="-1") {{_ 'organizations'}} :
|
||||
each orgsDatas
|
||||
option(value="{{orgId}}") {{orgDisplayName}}
|
||||
|
||||
li.AllBoardBtns
|
||||
div.AllBoardButtonsContainer
|
||||
if userHasOrgsOrTeams
|
||||
i.fa.fa-filter
|
||||
input#filterBtn(type="button" value="{{_ 'filter'}}")
|
||||
input#resetBtn(type="button" value="{{_ 'filter-clear'}}")
|
||||
li.AllBoardBtns
|
||||
div.AllBoardButtonsContainer
|
||||
if userHasOrgsOrTeams
|
||||
span.emoji-icon
|
||||
i.fa.fa-search
|
||||
input#filterBtn(type="button" value="{{_ 'filter'}}")
|
||||
button#resetBtn.filter-reset-btn
|
||||
span.reset-icon
|
||||
span.emoji-icon
|
||||
i.fa.fa-times-thin
|
||||
span {{_ 'filter-clear'}}
|
||||
|
||||
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
li.js-add-board
|
||||
a.board-list-item.label(title="{{_ 'add-board'}}")
|
||||
| {{_ 'add-board'}}
|
||||
each boards
|
||||
li(class="{{_id}}" class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
|
||||
if isInvited
|
||||
.board-list-item
|
||||
span.details
|
||||
span.board-list-item-name= title
|
||||
i.fa.js-star-board(
|
||||
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
p.board-list-item-desc {{_ 'just-invited'}}
|
||||
button.js-accept-invite.primary {{_ 'accept'}}
|
||||
button.js-decline-invite {{_ 'decline'}}
|
||||
else
|
||||
if $eq type "template-container"
|
||||
a.js-open-board.template-container.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
span.details
|
||||
span.board-list-item-name(title="{{_ 'template-container'}}")
|
||||
+viewer
|
||||
= title
|
||||
i.fa.js-star-board(
|
||||
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
p.board-list-item-desc
|
||||
+viewer
|
||||
= description
|
||||
if hasSpentTimeCards
|
||||
i.fa.js-has-spenttime-cards(
|
||||
class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
i.fa.board-handle(
|
||||
class="fa-arrows"
|
||||
title="{{_ 'drag-board'}}")
|
||||
else
|
||||
if isSandstorm
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
else if isAdministrable
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
else if currentUser.isAdmin
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
// Right boards grid
|
||||
.boards-right-grid
|
||||
.boards-path-header
|
||||
.path-left
|
||||
span.path-icon.emoji-icon {{currentMenuPath.icon}}
|
||||
span.path-text {{currentMenuPath.text}}
|
||||
if BoardMultiSelection.isActive
|
||||
span.multiselection-hint
|
||||
span.emoji-icon
|
||||
i.fa.fa-thumb-tack
|
||||
| {{_ 'multi-selection-active'}}
|
||||
.path-right
|
||||
if canModifyBoards
|
||||
if hasBoardsSelected
|
||||
button.js-archive-selected-boards.board-header-btn
|
||||
span.emoji-icon
|
||||
i.fa.fa-archive
|
||||
span {{_ 'archive-board'}}
|
||||
button.js-duplicate-selected-boards.board-header-btn
|
||||
span.emoji-icon
|
||||
i.fa.fa-clipboard
|
||||
span {{_ 'duplicate-board'}}
|
||||
a.board-header-btn.js-multiselection-activate(
|
||||
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
|
||||
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-check-square-o
|
||||
if BoardMultiSelection.isActive
|
||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-times
|
||||
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
|
||||
li.js-add-board
|
||||
if isSelectedMenu 'templates'
|
||||
a.board-list-item.label(title="{{_ 'add-template-container'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-template-container'}}
|
||||
else
|
||||
a.js-open-board.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
span.details
|
||||
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
|
||||
+viewer
|
||||
= title
|
||||
unless currentSetting.hideBoardMemberList
|
||||
if allowsBoardMemberList
|
||||
.minicard-members
|
||||
each member in boardMembers _id
|
||||
a.name
|
||||
+userAvatar(userId=member noRemove=true)
|
||||
unless currentSetting.hideCardCounterList
|
||||
if allowsCardCounterList
|
||||
.minicard-lists.flex.flex-wrap
|
||||
each list in boardLists _id
|
||||
.item
|
||||
| {{ list }}
|
||||
i.fa.js-star-board(
|
||||
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
p.board-list-item-desc
|
||||
+viewer
|
||||
= description
|
||||
if hasSpentTimeCards
|
||||
i.fa.js-has-spenttime-cards(
|
||||
class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
i.fa.board-handle(
|
||||
class="fa-arrows"
|
||||
title="{{_ 'drag-board'}}")
|
||||
else
|
||||
if isSandstorm
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
else if isAdministrable
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
else if currentUser.isAdmin
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
a.board-list-item.label(title="{{_ 'add-board'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-board'}}
|
||||
each boards
|
||||
li.js-board(class="{{_id}} {{#if isStarred}}starred{{/if}} {{colorClass}} {{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}", draggable="true")
|
||||
if isInvited
|
||||
.board-list-item
|
||||
if BoardMultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
span.details
|
||||
span.board-list-item-name= title
|
||||
span.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
span.emoji-icon
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
p.board-list-item-desc {{_ 'just-invited'}}
|
||||
button.js-accept-invite.primary {{_ 'accept'}}
|
||||
button.js-decline-invite {{_ 'decline'}}
|
||||
else
|
||||
if $eq type "template-container"
|
||||
.template-container.board-list-item
|
||||
if BoardMultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
span.board-handle(title="{{_ 'drag-board'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-arrows
|
||||
|
||||
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
span.details
|
||||
span.board-list-item-name(title="{{_ 'template-container'}}")
|
||||
+viewer
|
||||
= title
|
||||
p.board-list-item-desc
|
||||
+viewer
|
||||
= description
|
||||
if hasSpentTimeCards
|
||||
span.js-has-spenttime-cards(
|
||||
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-clock-o
|
||||
span.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
span.emoji-icon
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
else
|
||||
.board-list-item
|
||||
if BoardMultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
span.board-handle(title="{{_ 'drag-board'}}")
|
||||
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'}}")
|
||||
+viewer
|
||||
= title
|
||||
unless currentSetting.hideBoardMemberList
|
||||
if allowsBoardMemberList
|
||||
.minicard-members
|
||||
each member in boardMembers _id
|
||||
a.name
|
||||
+userAvatar(userId=member noRemove=true)
|
||||
unless currentSetting.hideCardCounterList
|
||||
if allowsCardCounterList
|
||||
.minicard-lists.flex.flex-wrap
|
||||
each list in boardLists _id
|
||||
.item
|
||||
| {{ list }}
|
||||
p.board-list-item-desc
|
||||
+viewer
|
||||
= description
|
||||
if hasSpentTimeCards
|
||||
span.js-has-spenttime-cards(
|
||||
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-clock-o
|
||||
a.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
span.emoji-icon
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
|
||||
template(name="boardListHeaderBar")
|
||||
h1 {{_ title }}
|
||||
//.board-header-btns.right
|
||||
// a.board-header-btn.js-open-archived-board
|
||||
// i.fa.fa-archive
|
||||
// span {{_ 'archives'}}
|
||||
// a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
|
||||
// i.fa.fa-clone
|
||||
// span {{_ 'templates'}}
|
||||
//
|
||||
a.board-header-btn.js-open-archived-board
|
||||
//
|
||||
i.fa.fa-archive
|
||||
//
|
||||
span {{_ 'archives'}}
|
||||
//
|
||||
a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
|
||||
//
|
||||
i.fa.fa-clone
|
||||
//
|
||||
span {{_ 'templates'}}
|
||||
|
||||
// Recursive template for workspaces tree
|
||||
template(name="workspaceTree")
|
||||
if nodes
|
||||
ul.workspace-tree.js-workspace-tree
|
||||
each nodes
|
||||
li.workspace-node(class="{{#if $eq id selectedWorkspaceId}}active{{/if}}" data-workspace-id="{{id}}" draggable="true")
|
||||
.workspace-node-content
|
||||
span.workspace-drag-handle
|
||||
span.emoji-icon
|
||||
i.fa.fa-arrows
|
||||
|
||||
a.js-select-workspace.js-select-space(data-id="{{id}}")
|
||||
span.workspace-icon
|
||||
if icon
|
||||
span.workspace-icon-value(style="{{#if iconColor}}color: {{iconColor}};{{/if}}")
|
||||
+viewer
|
||||
= icon
|
||||
else
|
||||
span.emoji-icon(style="{{#if iconColor}}color: {{iconColor}};{{/if}}")
|
||||
i.fa.fa-folder
|
||||
span.workspace-name= name
|
||||
a.js-open-workspace-menu(data-id="{{id}}" title="{{_ 'allboards.workspace-menu'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-bars
|
||||
span.workspace-count {{workspaceCount id}}
|
||||
if children
|
||||
+workspaceTree(nodes=children selectedWorkspaceId=selectedWorkspaceId)
|
||||
|
||||
template(name="workspaceActionsPopup")
|
||||
form.workspace-actions-form.js-workspace-actions-form(data-id="{{id}}")
|
||||
label
|
||||
| {{_ 'allboards.edit-workspace-name'}}
|
||||
input.js-workspace-name(type="text" value="{{workspaceName}}" required)
|
||||
label
|
||||
| {{_ 'allboards.edit-workspace-icon'}}
|
||||
input.js-workspace-icon(type="text" value="{{workspaceIcon}}")
|
||||
input.primary.wide(type="submit" value="{{_ 'save'}}")
|
||||
hr
|
||||
form.workspace-subspace-form.js-workspace-subspace-form(data-id="{{id}}")
|
||||
label
|
||||
| {{_ 'allboards.add-subworkspace-prompt'}}
|
||||
input.js-subworkspace-name(type="text")
|
||||
button.primary.wide(type="submit") {{_ 'allboards.add-subworkspace'}}
|
||||
hr
|
||||
.workspace-delete-block
|
||||
span.js-workspace-delete-error(style="color: #c00; display: block; margin: 4px 0;")
|
||||
button.js-delete-workspace.negate.wide(type="button" data-id="{{id}}") {{_ 'delete'}}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,43 @@
|
||||
.calendar-view .fc {
|
||||
--fc-button-text-color: #333;
|
||||
--fc-button-bg-color: #f5f5f5;
|
||||
--fc-button-border-color: rgba(0, 0, 0, 0.2);
|
||||
--fc-button-hover-bg-color: #e6e6e6;
|
||||
--fc-button-hover-border-color: rgba(0, 0, 0, 0.25);
|
||||
--fc-button-active-bg-color: #d9d9d9;
|
||||
--fc-button-active-border-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.calendar-view .fc .fc-button-primary {
|
||||
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||
0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
background-image: linear-gradient(to bottom, #fff 0%, #e6e6e6 100%);
|
||||
}
|
||||
|
||||
.calendar-view .fc .fc-button-primary:focus,
|
||||
.calendar-view .fc .fc-button-primary:not(:disabled).fc-button-active,
|
||||
.calendar-view .fc .fc-button-primary:not(:disabled):active {
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),
|
||||
0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.calendar-create-close {
|
||||
min-height: auto !important;
|
||||
min-width: auto !important;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
color: #666 !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.calendar-create-close:hover,
|
||||
.calendar-create-close:focus {
|
||||
background: transparent !important;
|
||||
color: #111 !important;
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ template(name="miniboard")
|
||||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
.fa.fa-arrows
|
||||
span.drag-handle(title="{{_ 'dragBoard'}}")
|
||||
i.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
||||
@@ -0,0 +1,263 @@
|
||||
/* Original Positions View Styles */
|
||||
.original-positions-view {
|
||||
margin: 10px 0;
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.original-positions-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.original-positions-header .btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.original-positions-content {
|
||||
background-color: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.original-positions-loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.original-positions-loading i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.original-positions-filters {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.original-positions-filters .btn-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.original-positions-filters .btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.original-positions-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.original-position-item {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
padding: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.original-position-item:hover {
|
||||
background-color: #e9ecef;
|
||||
border-color: #ced4da;
|
||||
}
|
||||
|
||||
.original-position-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.original-position-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.original-position-item-header i {
|
||||
color: #6c757d;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.entity-type {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.entity-name {
|
||||
color: #212529;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.entity-id {
|
||||
color: #6c757d;
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.original-position-item-details {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.original-position-description {
|
||||
color: #495057;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.original-title {
|
||||
color: #6c757d;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
padding: 4px 6px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.original-title strong {
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.original-position-date {
|
||||
color: #6c757d;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.no-original-positions {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.no-original-positions i {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.original-positions-view {
|
||||
margin: 5px 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.original-positions-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.original-positions-header .btn {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.original-positions-filters .btn-group {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.original-position-item-header {
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.entity-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.original-position-item-details {
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.original-positions-view {
|
||||
background-color: #2d3748;
|
||||
border-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.original-positions-content {
|
||||
background-color: #1a202c;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
.original-position-item {
|
||||
background-color: #2d3748;
|
||||
border-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.original-position-item:hover {
|
||||
background-color: #4a5568;
|
||||
border-color: #718096;
|
||||
}
|
||||
|
||||
.original-position-item-header {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.original-position-item-header i {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.entity-name {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.entity-id {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.original-position-description {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.original-title {
|
||||
background-color: #4a5568;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.original-title strong {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.original-position-date {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.no-original-positions {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.no-original-positions i {
|
||||
color: #718096;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
template(name="originalPositionsView")
|
||||
.original-positions-view
|
||||
.original-positions-header
|
||||
button.btn.btn-sm.btn-outline-secondary.js-toggle-original-positions
|
||||
i.fa.fa-history
|
||||
if isShowingOriginalPositions
|
||||
| Hide Original Positions
|
||||
else
|
||||
| Show Original Positions
|
||||
|
||||
if isShowingOriginalPositions
|
||||
button.btn.btn-sm.btn-outline-primary.js-refresh-history
|
||||
i.fa.fa-refresh
|
||||
| Refresh
|
||||
|
||||
if isShowingOriginalPositions
|
||||
.original-positions-content
|
||||
if isLoading
|
||||
.original-positions-loading
|
||||
i.fa.fa-spinner.fa-spin
|
||||
| Loading original positions...
|
||||
else
|
||||
.original-positions-filters
|
||||
.btn-group.btn-group-sm(role="group")
|
||||
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'all'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="all")
|
||||
| All
|
||||
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'swimlane'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="swimlane")
|
||||
i.fa.fa-bars
|
||||
| Swimlanes
|
||||
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'list'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="list")
|
||||
i.fa.fa-columns
|
||||
| Lists
|
||||
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'card'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="card")
|
||||
i.fa.fa-sticky-note
|
||||
| Cards
|
||||
|
||||
.original-positions-list
|
||||
each getFilteredHistory
|
||||
.original-position-item
|
||||
.original-position-item-header
|
||||
i.fa(class="{{getEntityTypeIcon entityType}}")
|
||||
span.entity-type {{getEntityTypeLabel entityType}}
|
||||
span.entity-name {{getEntityDisplayName this}}
|
||||
span.entity-id ({{entityId}})
|
||||
.original-position-item-details
|
||||
.original-position-description
|
||||
| {{getEntityOriginalPositionDescription this}}
|
||||
if originalTitle
|
||||
.original-title
|
||||
strong Original title:
|
||||
| {{originalTitle}}
|
||||
.original-position-date
|
||||
small.text-muted Created: {{formatDate createdAt}}
|
||||
else
|
||||
.no-original-positions
|
||||
i.fa.fa-info-circle
|
||||
| No original position data available for this board.
|
||||
@@ -0,0 +1,134 @@
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
/**
|
||||
* Component to display original positions for all entities on a board
|
||||
*/
|
||||
|
||||
Template.originalPositionsView.onCreated(function () {
|
||||
this.showOriginalPositions = new ReactiveVar(false);
|
||||
this.boardHistory = new ReactiveVar([]);
|
||||
this.isLoading = new ReactiveVar(false);
|
||||
this.filterType = new ReactiveVar('all'); // 'all', 'swimlane', 'list', 'card'
|
||||
|
||||
const tpl = this;
|
||||
|
||||
this.loadBoardHistory = function () {
|
||||
const boardId = Session.get('currentBoard');
|
||||
if (!boardId) return;
|
||||
|
||||
tpl.isLoading.set(true);
|
||||
|
||||
Meteor.call('positionHistory.getBoardHistory', boardId, (error, result) => {
|
||||
tpl.isLoading.set(false);
|
||||
if (error) {
|
||||
console.error('Error loading board history:', error);
|
||||
tpl.boardHistory.set([]);
|
||||
} else {
|
||||
tpl.boardHistory.set(result);
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
Template.originalPositionsView.onRendered(function () {
|
||||
this.loadBoardHistory();
|
||||
});
|
||||
|
||||
Template.originalPositionsView.helpers({
|
||||
isShowingOriginalPositions() {
|
||||
return Template.instance().showOriginalPositions.get();
|
||||
},
|
||||
|
||||
isLoading() {
|
||||
return Template.instance().isLoading.get();
|
||||
},
|
||||
|
||||
getBoardHistory() {
|
||||
return Template.instance().boardHistory.get();
|
||||
},
|
||||
|
||||
getFilteredHistory() {
|
||||
const tpl = Template.instance();
|
||||
const history = tpl.boardHistory.get();
|
||||
const filterType = tpl.filterType.get();
|
||||
|
||||
if (filterType === 'all') {
|
||||
return history;
|
||||
}
|
||||
|
||||
return history.filter(item => item.entityType === filterType);
|
||||
},
|
||||
|
||||
isFilterType(type) {
|
||||
return Template.instance().filterType.get() === type;
|
||||
},
|
||||
|
||||
getEntityDisplayName(entity) {
|
||||
const position = entity.originalPosition || {};
|
||||
return position.title || `Entity ${entity.entityId}`;
|
||||
},
|
||||
|
||||
getEntityOriginalPositionDescription(entity) {
|
||||
const position = entity.originalPosition || {};
|
||||
let description = `Position: ${position.sort || 0}`;
|
||||
|
||||
if (entity.entityType === 'list' && entity.originalSwimlaneId) {
|
||||
description += ` in swimlane ${entity.originalSwimlaneId}`;
|
||||
} else if (entity.entityType === 'card') {
|
||||
if (entity.originalSwimlaneId) {
|
||||
description += ` in swimlane ${entity.originalSwimlaneId}`;
|
||||
}
|
||||
if (entity.originalListId) {
|
||||
description += ` in list ${entity.originalListId}`;
|
||||
}
|
||||
}
|
||||
|
||||
return description;
|
||||
},
|
||||
|
||||
getEntityTypeIcon(entityType) {
|
||||
switch (entityType) {
|
||||
case 'swimlane':
|
||||
return 'fa-bars';
|
||||
case 'list':
|
||||
return 'fa-columns';
|
||||
case 'card':
|
||||
return 'fa-sticky-note';
|
||||
default:
|
||||
return 'fa-question';
|
||||
}
|
||||
},
|
||||
|
||||
getEntityTypeLabel(entityType) {
|
||||
switch (entityType) {
|
||||
case 'swimlane':
|
||||
return 'Swimlane';
|
||||
case 'list':
|
||||
return 'List';
|
||||
case 'card':
|
||||
return 'Card';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
return new Date(date).toLocaleString();
|
||||
},
|
||||
});
|
||||
|
||||
Template.originalPositionsView.events({
|
||||
'click .js-toggle-original-positions'(evt, tpl) {
|
||||
tpl.showOriginalPositions.set(!tpl.showOriginalPositions.get());
|
||||
},
|
||||
|
||||
'click .js-refresh-history'(evt, tpl) {
|
||||
tpl.loadBoardHistory();
|
||||
},
|
||||
|
||||
'click .js-filter-type'(evt, tpl) {
|
||||
const type = evt.currentTarget.dataset.filterType;
|
||||
tpl.filterType.set(type);
|
||||
},
|
||||
});
|
||||
@@ -55,6 +55,12 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.attachment-actions a {
|
||||
margin-left: 16px;
|
||||
}
|
||||
.attachment-actions a:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
.add-attachment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -106,6 +112,9 @@
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 4em;
|
||||
position: absolute;
|
||||
right: 50px;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
/* Upload progress indicators for drag-and-drop uploads */
|
||||
@@ -241,10 +250,6 @@
|
||||
.js-card-details.is-dragging-over {
|
||||
border: 2px dashed #007bff !important;
|
||||
background: rgba(0, 123, 255, 0.05) !important;
|
||||
}
|
||||
top: 0;
|
||||
right: 8px;
|
||||
position: absolute;
|
||||
}
|
||||
.attachment-arrow {
|
||||
font-size: 4em;
|
||||
@@ -253,6 +258,20 @@
|
||||
align-self: center;
|
||||
margin: 0 20px;
|
||||
}
|
||||
#prev-attachment {
|
||||
font-size: 4em;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
margin-left: 70px;
|
||||
}
|
||||
#next-attachment {
|
||||
font-size: 4em;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
margin-right: 70px;
|
||||
}
|
||||
#viewer-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -266,6 +285,13 @@
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
#video-viewer {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
#audio-viewer {
|
||||
max-width: 100%;
|
||||
}
|
||||
#pdf-viewer {
|
||||
width: 40vw;
|
||||
height: 100%;
|
||||
@@ -300,9 +326,19 @@
|
||||
}
|
||||
#prev-attachment {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
bottom: 2.2em;
|
||||
font-size: 1.6em;
|
||||
padding: 16px;
|
||||
margin-left: 0;
|
||||
}
|
||||
#next-attachment {
|
||||
right: 0;
|
||||
position: absolute;
|
||||
bottom: 2.2em;
|
||||
font-size: 1.6em;
|
||||
padding: 16px;
|
||||
margin-right: 0;
|
||||
}
|
||||
#pdf-viewer {
|
||||
width: 100%;
|
||||
@@ -336,36 +372,3 @@
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Attachment migration styles */
|
||||
.attachment-item.migrating {
|
||||
position: relative;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.attachment-migration-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.migration-spinner {
|
||||
font-size: 24px;
|
||||
color: #007cba;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.migration-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -34,10 +34,11 @@ template(name="attachmentViewer")
|
||||
#viewer-overlay.hidden
|
||||
#viewer-top-bar
|
||||
span#attachment-name
|
||||
a#viewer-close.fa.fa-times-thin
|
||||
a#viewer-close
|
||||
i.fa.fa-times-thin
|
||||
|
||||
#viewer-container
|
||||
i.fa.fa-chevron-left.attachment-arrow#prev-attachment
|
||||
i.fa.fa-caret-left#prev-attachment
|
||||
#viewer-content
|
||||
img#image-viewer.hidden
|
||||
video#video-viewer.hidden(controls="true")
|
||||
@@ -45,7 +46,7 @@ template(name="attachmentViewer")
|
||||
object#pdf-viewer.hidden(type="application/pdf")
|
||||
span.pdf-preview-error {{_ 'preview-pdf-not-supported' }}
|
||||
object#txt-viewer.hidden(type="text/plain")
|
||||
i.fa.fa-chevron-right.attachment-arrow#next-attachment
|
||||
i.fa.fa-caret-right#next-attachment
|
||||
|
||||
template(name="attachmentGallery")
|
||||
|
||||
@@ -53,7 +54,7 @@ template(name="attachmentGallery")
|
||||
|
||||
if canModifyCard
|
||||
a.attachment-item.add-attachment.js-add-attachment
|
||||
i.fa.fa-plus.icon
|
||||
i.fa.fa-plus
|
||||
|
||||
each attachments
|
||||
|
||||
@@ -85,18 +86,22 @@ template(name="attachmentGallery")
|
||||
b
|
||||
= name
|
||||
span.file-size ({{fileSize size}})
|
||||
if uploaderName
|
||||
div.attachment-uploader {{uploaderName}}
|
||||
if uploadedAt
|
||||
div.attachment-uploaded-at {{uploadedAt}}
|
||||
.attachment-actions
|
||||
a.js-download(href="{{link}}?download=true", download="{{name}}")
|
||||
i.fa.fa-download.icon(title="{{_ 'download'}}")
|
||||
a.js-download(href="{{link}}?download=true", download="{{name}}", title="{{_ 'download'}}")
|
||||
i.fa.fa-arrow-down
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isWorker
|
||||
a.js-rename
|
||||
i.fa.fa-pencil-square-o.icon(title="{{_ 'rename'}}")
|
||||
a.js-confirm-delete
|
||||
i.fa.fa-trash.icon(title="{{_ 'delete'}}")
|
||||
a.fa.fa-navicon.icon.js-open-attachment-menu(data-attachment-link="{{link}}" title="{{_ 'attachmentActionsPopup-title'}}")
|
||||
|
||||
a.js-rename(title="{{_ 'rename'}}")
|
||||
i.fa.fa-pencil-square-o
|
||||
a.js-confirm-delete(title="{{_ 'delete'}}")
|
||||
i.fa.fa-trash
|
||||
a.js-open-attachment-menu(data-attachment-link="{{link}}", title="{{_ 'attachmentActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
// Migration spinner overlay
|
||||
if isAttachmentMigrating _id
|
||||
.attachment-migration-overlay
|
||||
@@ -109,12 +114,8 @@ template(name="attachmentActionsPopup")
|
||||
li
|
||||
if isImage
|
||||
a(class="{{#if isCover}}js-remove-cover{{else}}js-add-cover{{/if}}")
|
||||
i.fa.fa-book
|
||||
i.fa.fa-picture-o
|
||||
if isCover
|
||||
| {{_ 'remove-cover'}}
|
||||
else
|
||||
| {{_ 'add-cover'}}
|
||||
| {{#if isCover}}{{_ 'remove-cover'}}{{else}}{{_ 'add-cover'}}{{/if}}
|
||||
if currentUser.isBoardAdmin
|
||||
if isImage
|
||||
a(class="{{#if isBackgroundImage}}js-remove-background-image{{else}}js-add-background-image{{/if}}")
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
@@ -343,7 +362,7 @@ export function handleFileUpload(card, files) {
|
||||
}
|
||||
|
||||
// Check if user can modify the card
|
||||
if (!card.canModifyCard()) {
|
||||
if (!Utils.canModifyCard()) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('User does not have permission to modify this card');
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -22,7 +22,7 @@ template(name="cardCustomField-text")
|
||||
= value
|
||||
.edit-controls.clearfix
|
||||
button.primary(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
if value
|
||||
@@ -41,7 +41,7 @@ template(name="cardCustomField-number")
|
||||
input(type="number" value=data.value)
|
||||
.edit-controls.clearfix
|
||||
button.primary(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
if value
|
||||
@@ -55,10 +55,11 @@ template(name="cardCustomField-number")
|
||||
template(name="cardCustomField-checkbox")
|
||||
.js-checklist-item.checklist-item(class="{{#if data.value }}is-checked{{/if}}")
|
||||
if canModifyCard
|
||||
.check-box-container
|
||||
.check-box.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
|
||||
span.check-box-unicode
|
||||
i.fa(class="{{#if data.value}}fa-check-square{{else}}fa-square-o{{/if}}")
|
||||
else
|
||||
.materialCheckBox(class="{{#if data.value }}is-checked{{/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
|
||||
@@ -66,7 +67,7 @@ template(name="cardCustomField-currency")
|
||||
input(type="text" value=data.value autofocus)
|
||||
.edit-controls.clearfix
|
||||
button.primary(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
if value
|
||||
@@ -98,6 +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")
|
||||
@@ -113,7 +129,7 @@ template(name="cardCustomField-dropdown")
|
||||
= name
|
||||
.edit-controls.clearfix
|
||||
button.primary(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
if value
|
||||
@@ -134,7 +150,7 @@ template(name="cardCustomField-stringtemplate")
|
||||
input.js-card-customfield-stringtemplate-item.last(type="text" value="" placeholder="{{_ 'custom-field-stringtemplate-item-placeholder'}}" autofocus)
|
||||
.edit-controls.clearfix
|
||||
button.primary(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
if value
|
||||
|
||||
@@ -1,12 +1,41 @@
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { DatePicker } from '/client/lib/datepicker';
|
||||
import Cards from '/models/cards';
|
||||
import {
|
||||
setupDatePicker,
|
||||
datePickerRendered,
|
||||
datePickerHelpers,
|
||||
datePickerEvents,
|
||||
} from '/client/lib/datepicker';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatDateByUserPreference,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
import { CustomFieldStringTemplate } from '/client/lib/customFields'
|
||||
import { getCurrentCardFromContext } from '/client/lib/currentCard';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { getSidebarInstance } from '/client/features/sidebar/service';
|
||||
|
||||
Template.cardCustomFieldsPopup.helpers({
|
||||
hasCustomField() {
|
||||
const card = Utils.getCurrentCard();
|
||||
const card = getCurrentCardFromContext();
|
||||
if (!card) return false;
|
||||
const customFieldId = this._id;
|
||||
return card.customFieldIndex(customFieldId) > -1;
|
||||
},
|
||||
@@ -14,315 +43,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-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(moment());
|
||||
window.setInterval(() => {
|
||||
self.now.set(moment());
|
||||
}, 60000);
|
||||
Template['cardCustomField-date'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
const self = this;
|
||||
self.date = ReactiveVar();
|
||||
self.now = ReactiveVar(now());
|
||||
window.setInterval(() => {
|
||||
self.now.set(now());
|
||||
}, 60000);
|
||||
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().value));
|
||||
});
|
||||
}
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().value));
|
||||
});
|
||||
});
|
||||
|
||||
Template['cardCustomField-date'].helpers({
|
||||
showWeek() {
|
||||
return this.date.get().week().toString();
|
||||
}
|
||||
|
||||
return getISOWeek(Template.instance().date.get()).toString();
|
||||
},
|
||||
showWeekOfYear() {
|
||||
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
return user.isShowWeekOfYear();
|
||||
},
|
||||
showDate() {
|
||||
// this will start working once mquandalle:moment
|
||||
// is updated to at least moment.js 2.10.5
|
||||
// until then, the date is displayed in the "L" format
|
||||
return this.date.get().calendar(null, {
|
||||
sameElse: 'llll',
|
||||
});
|
||||
}
|
||||
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
showISODate() {
|
||||
return this.date.get().toISOString();
|
||||
}
|
||||
|
||||
return Template.instance().date.get().toISOString();
|
||||
},
|
||||
classes() {
|
||||
const tpl = Template.instance();
|
||||
if (
|
||||
this.date.get().isBefore(this.now.get(), 'minute') &&
|
||||
this.now.get().isBefore(this.data().value)
|
||||
isBefore(tpl.date.get(), tpl.now.get(), 'minute') &&
|
||||
isBefore(tpl.now.get(), this.value, 'minute')
|
||||
) {
|
||||
return 'current';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
},
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
|
||||
}
|
||||
return `${TAPi18n.__('card-start-on')} ${Template.instance().date.get().toLocaleString()}`;
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-edit-date': Popup.open('cardCustomField-date'),
|
||||
},
|
||||
];
|
||||
}
|
||||
}.register('cardCustomField-date'));
|
||||
Template['cardCustomField-date'].events({
|
||||
'click .js-edit-date': Popup.open('cardCustomField-date'),
|
||||
});
|
||||
|
||||
// cardCustomField-datePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.card = Utils.getCurrentCard();
|
||||
self.customFieldId = this.data()._id;
|
||||
this.data().value && this.date.set(moment(this.data().value));
|
||||
}
|
||||
Template['cardCustomField-datePopup'].onCreated(function () {
|
||||
const data = Template.currentData();
|
||||
setupDatePicker(this, {
|
||||
initialDate: data.value ? data.value : undefined,
|
||||
});
|
||||
// Override card and store customFieldId for store/delete callbacks
|
||||
this.datePicker.card = getCurrentCardFromContext();
|
||||
this.customFieldId = data._id;
|
||||
});
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setCustomField(this.customFieldId, date);
|
||||
}
|
||||
Template['cardCustomField-datePopup'].onRendered(function () {
|
||||
datePickerRendered(this);
|
||||
});
|
||||
|
||||
_deleteDate() {
|
||||
this.card.setCustomField(this.customFieldId, '');
|
||||
}
|
||||
}.register('cardCustomField-datePopup'));
|
||||
Template['cardCustomField-datePopup'].helpers(datePickerHelpers());
|
||||
|
||||
Template['cardCustomField-datePopup'].events(datePickerEvents({
|
||||
storeDate(date) {
|
||||
this.datePicker.card.setCustomField(this.customFieldId, date);
|
||||
},
|
||||
deleteDate() {
|
||||
this.datePicker.card.setCustomField(this.customFieldId, '');
|
||||
},
|
||||
}));
|
||||
|
||||
// cardCustomField-dropdown
|
||||
(class extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
this._items = this.data().definition.settings.dropdownItems;
|
||||
this.items = this._items.slice(0);
|
||||
this.items.unshift({
|
||||
_id: '',
|
||||
name: TAPi18n.__('custom-field-dropdown-none'),
|
||||
});
|
||||
}
|
||||
Template['cardCustomField-dropdown'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
this._items = Template.currentData().definition.settings.dropdownItems;
|
||||
this.items = this._items.slice(0);
|
||||
this.items.unshift({
|
||||
_id: '',
|
||||
name: TAPi18n.__('custom-field-dropdown-none'),
|
||||
});
|
||||
});
|
||||
|
||||
Template['cardCustomField-dropdown'].helpers({
|
||||
items() {
|
||||
return Template.instance().items;
|
||||
},
|
||||
selectedItem() {
|
||||
const selected = this._items.find(item => {
|
||||
return item._id === this.data().value;
|
||||
const tpl = Template.instance();
|
||||
const selected = tpl._items.find(item => {
|
||||
return item._id === this.value;
|
||||
});
|
||||
return selected
|
||||
? selected.name
|
||||
: TAPi18n.__('custom-field-dropdown-unknown');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-customfield-dropdown'(event) {
|
||||
event.preventDefault();
|
||||
const value = this.find('select').value;
|
||||
this.card.setCustomField(this.customFieldId, value);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}.register('cardCustomField-dropdown'));
|
||||
Template['cardCustomField-dropdown'].events({
|
||||
'submit .js-card-customfield-dropdown'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const value = tpl.find('select').value;
|
||||
tpl.card.setCustomField(tpl.customFieldId, value);
|
||||
},
|
||||
});
|
||||
|
||||
// cardCustomField-stringtemplate
|
||||
class CardCustomFieldStringTemplate extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
|
||||
this.customField = new CustomFieldStringTemplate(this.data().definition);
|
||||
|
||||
this.stringtemplateItems = new ReactiveVar(this.data().value ?? []);
|
||||
}
|
||||
Template['cardCustomField-stringtemplate'].onCreated(function () {
|
||||
this.card = getCurrentCardFromContext();
|
||||
this.customFieldId = Template.currentData()._id;
|
||||
this.customField = new CustomFieldStringTemplate(Template.currentData().definition);
|
||||
this.stringtemplateItems = new ReactiveVar(Template.currentData().value ?? []);
|
||||
});
|
||||
|
||||
Template['cardCustomField-stringtemplate'].helpers({
|
||||
formattedValue() {
|
||||
const ret = this.customField.getFormattedValue(this.data().value);
|
||||
const tpl = Template.instance();
|
||||
const ret = tpl.customField.getFormattedValue(this.value);
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
stringtemplateItems() {
|
||||
return Template.instance().stringtemplateItems.get();
|
||||
},
|
||||
});
|
||||
|
||||
getItems() {
|
||||
return Array.from(this.findAll('input'))
|
||||
.map(input => input.value)
|
||||
.filter(value => !!value.trim());
|
||||
}
|
||||
Template['cardCustomField-stringtemplate'].events({
|
||||
'submit .js-card-customfield-stringtemplate'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const items = tpl.stringtemplateItems.get();
|
||||
tpl.card.setCustomField(tpl.customFieldId, items);
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-customfield-stringtemplate'(event) {
|
||||
event.preventDefault();
|
||||
const items = this.stringtemplateItems.get();
|
||||
this.card.setCustomField(this.customFieldId, items);
|
||||
},
|
||||
'keydown .js-card-customfield-stringtemplate-item'(event, tpl) {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
|
||||
'keydown .js-card-customfield-stringtemplate-item'(event) {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
if (event.target.value.trim() || event.metaKey || event.ctrlKey) {
|
||||
const inputLast = tpl.find('input.last');
|
||||
|
||||
if (event.target.value.trim() || event.metaKey || event.ctrlKey) {
|
||||
const inputLast = this.find('input.last');
|
||||
let items = Array.from(tpl.findAll('input'))
|
||||
.map(input => input.value)
|
||||
.filter(value => !!value.trim());
|
||||
|
||||
let items = this.getItems();
|
||||
if (event.target === inputLast) {
|
||||
inputLast.value = '';
|
||||
} else if (event.target.nextSibling === inputLast) {
|
||||
inputLast.focus();
|
||||
} else {
|
||||
event.target.blur();
|
||||
|
||||
if (event.target === inputLast) {
|
||||
inputLast.value = '';
|
||||
} else if (event.target.nextSibling === inputLast) {
|
||||
inputLast.focus();
|
||||
} else {
|
||||
event.target.blur();
|
||||
const idx = Array.from(tpl.findAll('input')).indexOf(
|
||||
event.target,
|
||||
);
|
||||
items.splice(idx + 1, 0, '');
|
||||
|
||||
const idx = Array.from(this.findAll('input')).indexOf(
|
||||
event.target,
|
||||
);
|
||||
items.splice(idx + 1, 0, '');
|
||||
Tracker.afterFlush(() => {
|
||||
const element = tpl.findAll('input')[idx + 1];
|
||||
element.focus();
|
||||
element.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
Tracker.afterFlush(() => {
|
||||
const element = this.findAll('input')[idx + 1];
|
||||
element.focus();
|
||||
element.value = '';
|
||||
});
|
||||
}
|
||||
tpl.stringtemplateItems.set(items);
|
||||
}
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
tpl.find('button[type=submit]').click();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
this.stringtemplateItems.set(items);
|
||||
}
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
this.find('button[type=submit]').click();
|
||||
}
|
||||
}
|
||||
},
|
||||
'blur .js-card-customfield-stringtemplate-item'(event, tpl) {
|
||||
if (
|
||||
!event.target.value.trim() ||
|
||||
event.target === tpl.find('input.last')
|
||||
) {
|
||||
const items = Array.from(tpl.findAll('input'))
|
||||
.map(input => input.value)
|
||||
.filter(value => !!value.trim());
|
||||
tpl.stringtemplateItems.set(items);
|
||||
tpl.find('input.last').value = '';
|
||||
}
|
||||
},
|
||||
|
||||
'blur .js-card-customfield-stringtemplate-item'(event) {
|
||||
if (
|
||||
!event.target.value.trim() ||
|
||||
event.target === this.find('input.last')
|
||||
) {
|
||||
const items = this.getItems();
|
||||
this.stringtemplateItems.set(items);
|
||||
this.find('input.last').value = '';
|
||||
}
|
||||
},
|
||||
|
||||
'click .js-close-inlined-form'(event) {
|
||||
this.stringtemplateItems.set(this.data().value ?? []);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
CardCustomFieldStringTemplate.register('cardCustomField-stringtemplate');
|
||||
'click .js-close-inlined-form'(event, tpl) {
|
||||
tpl.stringtemplateItems.set(Template.currentData().value ?? []);
|
||||
},
|
||||
});
|
||||
@@ -8,57 +8,134 @@
|
||||
.card-date.is-active {
|
||||
background-color: #b3b3b3;
|
||||
}
|
||||
.card-date.current,
|
||||
.card-date.almost-due,
|
||||
.card-date.due,
|
||||
.card-date.long-overdue {
|
||||
/* Date status colors - red = overdue, amber = due soon, no shade = not due */
|
||||
.card-date.overdue {
|
||||
background-color: #ff4444; /* Red for overdue */
|
||||
color: #fff;
|
||||
}
|
||||
.card-date.overdue:hover,
|
||||
.card-date.overdue.is-active {
|
||||
background-color: #cc3333;
|
||||
}
|
||||
|
||||
.card-date.due-soon {
|
||||
background-color: #ffaa00; /* Amber for due soon */
|
||||
color: #000;
|
||||
}
|
||||
.card-date.due-soon:hover,
|
||||
.card-date.due-soon.is-active {
|
||||
background-color: #e69900;
|
||||
}
|
||||
|
||||
.card-date.not-due {
|
||||
/* No special background - uses default date type colors */
|
||||
}
|
||||
|
||||
.card-date.current {
|
||||
background-color: #5ba639;
|
||||
background-color: #5ba639; /* Green for current/active */
|
||||
color: #fff;
|
||||
}
|
||||
.card-date.current:hover,
|
||||
.card-date.current.is-active {
|
||||
background-color: #46802c;
|
||||
}
|
||||
.card-date.almost-due {
|
||||
background-color: #edc909;
|
||||
|
||||
.card-date.completed {
|
||||
background-color: #90ee90; /* Light green for completed */
|
||||
color: #000;
|
||||
}
|
||||
.card-date.almost-due:hover,
|
||||
.card-date.almost-due.is-active {
|
||||
background-color: #bc9f07;
|
||||
.card-date.completed:hover,
|
||||
.card-date.completed.is-active {
|
||||
background-color: #7dd87d;
|
||||
}
|
||||
.card-date.due {
|
||||
background-color: #fa3f00;
|
||||
|
||||
.card-date.completed-early {
|
||||
background-color: #4caf50; /* Green for completed early */
|
||||
color: #fff;
|
||||
}
|
||||
.card-date.due:hover,
|
||||
.card-date.due.is-active {
|
||||
background-color: #c73200;
|
||||
.card-date.completed-early:hover,
|
||||
.card-date.completed-early.is-active {
|
||||
background-color: #45a049;
|
||||
}
|
||||
.card-date.long-overdue {
|
||||
background-color: #fd5d47;
|
||||
|
||||
.card-date.completed-late {
|
||||
background-color: #ff9800; /* Orange for completed late */
|
||||
color: #fff;
|
||||
}
|
||||
.card-date.long-overdue:hover,
|
||||
.card-date.long-overdue.is-active {
|
||||
background-color: #fd3e24;
|
||||
.card-date.completed-late:hover,
|
||||
.card-date.completed-late.is-active {
|
||||
background-color: #f57c00;
|
||||
}
|
||||
|
||||
.card-date.completed-on-time {
|
||||
background-color: #2196f3; /* Blue for completed on time */
|
||||
color: #fff;
|
||||
}
|
||||
.card-date.completed-on-time:hover,
|
||||
.card-date.completed-on-time.is-active {
|
||||
background-color: #1976d2;
|
||||
}
|
||||
|
||||
/* Date type specific colors */
|
||||
.card-date.received-date {
|
||||
background-color: #dbdbdb; /* Light grey for received */
|
||||
}
|
||||
|
||||
.card-date.received-date:hover,
|
||||
.card-date.received-date.is-active {
|
||||
background-color: #b3b3b3;
|
||||
}
|
||||
|
||||
.card-date.start-date {
|
||||
background-color: #90ee90; /* Light green for start */
|
||||
color: #000; /* Black text for start */
|
||||
}
|
||||
|
||||
.card-date.start-date:hover,
|
||||
.card-date.start-date.is-active {
|
||||
background-color: #7dd87d;
|
||||
}
|
||||
|
||||
.card-date.due-date {
|
||||
background-color: #ffd700; /* Yellow for due */
|
||||
color: #000; /* Black text for due */
|
||||
}
|
||||
|
||||
.card-date.due-date:hover,
|
||||
.card-date.due-date.is-active {
|
||||
background-color: #e6c200;
|
||||
}
|
||||
|
||||
.card-date.end-date {
|
||||
background-color: #ffb3b3; /* Light red for end */
|
||||
color: #000; /* Black text for end */
|
||||
}
|
||||
|
||||
.card-date.end-date:hover,
|
||||
.card-date.end-date.is-active {
|
||||
background-color: #ff9999;
|
||||
}
|
||||
.card-date.end-date time::before {
|
||||
content: "\f253";
|
||||
content: "🏁"; /* Finish flag - represents end/completion */
|
||||
}
|
||||
.card-date.due-date time::before {
|
||||
content: "\f090";
|
||||
content: "⏰"; /* Alarm clock - represents due/deadline */
|
||||
}
|
||||
.card-date.start-date time::before {
|
||||
content: "\f251";
|
||||
content: "🚀"; /* Rocket - represents start/launch */
|
||||
}
|
||||
.card-date.received-date time::before {
|
||||
content: "\f08b";
|
||||
content: "📥"; /* Inbox tray - represents received/incoming */
|
||||
}
|
||||
|
||||
/* Generic date badge and custom field date */
|
||||
.card-date:not(.received-date):not(.start-date):not(.due-date):not(.end-date) time::before {
|
||||
/*content: "📅"; // Calendar - represents generic date */
|
||||
}
|
||||
.card-date time::before {
|
||||
font: normal normal normal 14px/1 FontAwesome;
|
||||
font-size: inherit;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
margin-right: 0.3em;
|
||||
display: inline-block;
|
||||
}
|
||||
.customfield-date {
|
||||
display: block;
|
||||
|
||||
@@ -14,6 +14,70 @@ template(name="dateBadge")
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardReceivedDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardStartDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardDueDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardEndDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="dateCustomField")
|
||||
a(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
@@ -21,3 +85,205 @@ template(name="dateCustomField")
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="cardCustomFieldDate")
|
||||
a(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="voteEndDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="pokerEndDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="minicardReceivedDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date.received-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date.received-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="minicardStartDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date.start-date(title="{{_ 'card-start'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date.start-date(title="{{_ 'card-start'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="minicardDueDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date.due-date(title="{{_ 'card-due'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date.due-date(title="{{_ 'card-due'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="minicardEndDate")
|
||||
if canModifyCard
|
||||
a.js-edit-date.card-date.end-date(title="{{_ 'card-end'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
else
|
||||
a.card-date.end-date(title="{{_ 'card-end'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="minicardCustomFieldDate")
|
||||
a(title="{{_ 'date'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
|
||||
time(datetime="{{showISODate}}")
|
||||
| {{showDate}}
|
||||
if showWeekOfYear
|
||||
b
|
||||
| {{showWeek}}
|
||||
|
||||
template(name="editCardReceivedDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="editCardStartDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="editCardDueDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="editCardEndDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="editVoteEndDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
|
||||
template(name="editPokerEndDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
+593
-348
@@ -1,399 +1,644 @@
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { DatePicker } from '/client/lib/datepicker';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import {
|
||||
setupDatePicker,
|
||||
datePickerRendered,
|
||||
datePickerHelpers,
|
||||
datePickerEvents,
|
||||
} from '/client/lib/datepicker';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatDateByUserPreference,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar,
|
||||
diff
|
||||
} from '/imports/lib/dateUtils';
|
||||
|
||||
// --- DatePicker popups (edit date forms) ---
|
||||
|
||||
// editCardReceivedDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
this.data().getReceived() &&
|
||||
this.date.set(moment(this.data().getReceived()));
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setReceived(moment(date).format('YYYY-MM-DD HH:mm'));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
this.card.unsetReceived();
|
||||
}
|
||||
}.register('editCardReceivedDatePopup'));
|
||||
|
||||
// editCardStartDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
this.data().getStart() && this.date.set(moment(this.data().getStart()));
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
super.onRendered();
|
||||
if (moment.isDate(this.card.getReceived())) {
|
||||
this.$('.js-datepicker').datepicker(
|
||||
'setStartDate',
|
||||
this.card.getReceived(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setStart(moment(date).format('YYYY-MM-DD HH:mm'));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
this.card.unsetStart();
|
||||
}
|
||||
}.register('editCardStartDatePopup'));
|
||||
|
||||
// editCardDueDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated('1970-01-01 17:00:00');
|
||||
this.data().getDue() && this.date.set(moment(this.data().getDue()));
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
super.onRendered();
|
||||
if (moment.isDate(this.card.getStart())) {
|
||||
this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
|
||||
}
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setDue(moment(date).format('YYYY-MM-DD HH:mm'));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
this.card.unsetDue();
|
||||
}
|
||||
}.register('editCardDueDatePopup'));
|
||||
|
||||
// editCardEndDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
this.data().getEnd() && this.date.set(moment(this.data().getEnd()));
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
super.onRendered();
|
||||
if (moment.isDate(this.card.getStart())) {
|
||||
this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
|
||||
}
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setEnd(moment(date).format('YYYY-MM-DD HH:mm'));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
this.card.unsetEnd();
|
||||
}
|
||||
}.register('editCardEndDatePopup'));
|
||||
|
||||
// Display received, start, due & end dates
|
||||
const CardDate = BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'dateBadge';
|
||||
},
|
||||
|
||||
onCreated() {
|
||||
const self = this;
|
||||
self.date = ReactiveVar();
|
||||
self.now = ReactiveVar(moment());
|
||||
window.setInterval(() => {
|
||||
self.now.set(moment());
|
||||
}, 60000);
|
||||
},
|
||||
|
||||
showWeek() {
|
||||
return this.date.get().week().toString();
|
||||
},
|
||||
|
||||
showWeekOfYear() {
|
||||
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
|
||||
},
|
||||
|
||||
showDate() {
|
||||
// this will start working once mquandalle:moment
|
||||
// is updated to at least moment.js 2.10.5
|
||||
// until then, the date is displayed in the "L" format
|
||||
return this.date.get().calendar(null, {
|
||||
sameElse: 'llll',
|
||||
});
|
||||
},
|
||||
|
||||
showISODate() {
|
||||
return this.date.get().toISOString();
|
||||
},
|
||||
Template.editCardReceivedDatePopup.onCreated(function () {
|
||||
const card = Template.currentData();
|
||||
setupDatePicker(this, {
|
||||
defaultTime: formatDateTime(now()),
|
||||
initialDate: card.getReceived() ? card.getReceived() : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
class CardReceivedDate extends CardDate {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getReceived()));
|
||||
});
|
||||
}
|
||||
Template.editCardReceivedDatePopup.onRendered(function () {
|
||||
datePickerRendered(this);
|
||||
});
|
||||
|
||||
Template.editCardReceivedDatePopup.helpers(datePickerHelpers());
|
||||
|
||||
Template.editCardReceivedDatePopup.events(datePickerEvents({
|
||||
storeDate(date) {
|
||||
this.datePicker.card.setReceived(date);
|
||||
},
|
||||
deleteDate() {
|
||||
this.datePicker.card.unsetReceived();
|
||||
},
|
||||
}));
|
||||
|
||||
// editCardStartDatePopup
|
||||
Template.editCardStartDatePopup.onCreated(function () {
|
||||
const card = Template.currentData();
|
||||
setupDatePicker(this, {
|
||||
defaultTime: formatDateTime(now()),
|
||||
initialDate: card.getStart() ? card.getStart() : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
Template.editCardStartDatePopup.onRendered(function () {
|
||||
datePickerRendered(this);
|
||||
});
|
||||
|
||||
Template.editCardStartDatePopup.helpers(datePickerHelpers());
|
||||
|
||||
Template.editCardStartDatePopup.events(datePickerEvents({
|
||||
storeDate(date) {
|
||||
this.datePicker.card.setStart(date);
|
||||
},
|
||||
deleteDate() {
|
||||
this.datePicker.card.unsetStart();
|
||||
},
|
||||
}));
|
||||
|
||||
// editCardDueDatePopup
|
||||
Template.editCardDueDatePopup.onCreated(function () {
|
||||
const card = Template.currentData();
|
||||
setupDatePicker(this, {
|
||||
defaultTime: '1970-01-01 17:00:00',
|
||||
initialDate: card.getDue() ? card.getDue() : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
Template.editCardDueDatePopup.onRendered(function () {
|
||||
datePickerRendered(this);
|
||||
});
|
||||
|
||||
Template.editCardDueDatePopup.helpers(datePickerHelpers());
|
||||
|
||||
Template.editCardDueDatePopup.events(datePickerEvents({
|
||||
storeDate(date) {
|
||||
this.datePicker.card.setDue(date);
|
||||
},
|
||||
deleteDate() {
|
||||
this.datePicker.card.unsetDue();
|
||||
},
|
||||
}));
|
||||
|
||||
// editCardEndDatePopup
|
||||
Template.editCardEndDatePopup.onCreated(function () {
|
||||
const card = Template.currentData();
|
||||
setupDatePicker(this, {
|
||||
defaultTime: formatDateTime(now()),
|
||||
initialDate: card.getEnd() ? card.getEnd() : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
Template.editCardEndDatePopup.onRendered(function () {
|
||||
datePickerRendered(this);
|
||||
});
|
||||
|
||||
Template.editCardEndDatePopup.helpers(datePickerHelpers());
|
||||
|
||||
Template.editCardEndDatePopup.events(datePickerEvents({
|
||||
storeDate(date) {
|
||||
this.datePicker.card.setEnd(date);
|
||||
},
|
||||
deleteDate() {
|
||||
this.datePicker.card.unsetEnd();
|
||||
},
|
||||
}));
|
||||
|
||||
// --- Card date badge display helpers ---
|
||||
|
||||
// Shared onCreated logic for card date badge templates
|
||||
function cardDateOnCreated(tpl) {
|
||||
tpl.date = new ReactiveVar();
|
||||
tpl.now = new ReactiveVar(now());
|
||||
window.setInterval(() => {
|
||||
tpl.now.set(now());
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
// Shared helpers for card date badge templates
|
||||
function cardDateHelpers(extraHelpers) {
|
||||
const base = {
|
||||
showWeek() {
|
||||
return getISOWeek(Template.instance().date.get()).toString();
|
||||
},
|
||||
showWeekOfYear() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
return user.isShowWeekOfYear();
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
showISODate() {
|
||||
return Template.instance().date.get().toISOString();
|
||||
},
|
||||
};
|
||||
return Object.assign(base, extraHelpers);
|
||||
}
|
||||
|
||||
// cardReceivedDate
|
||||
Template.cardReceivedDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getReceived()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.cardReceivedDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
const tpl = Template.instance();
|
||||
let classes = 'received-date ';
|
||||
const dueAt = this.data().getDue();
|
||||
const endAt = this.data().getEnd();
|
||||
const startAt = this.data().getStart();
|
||||
const theDate = this.date.get();
|
||||
// if dueAt, endAt and startAt exist & are > receivedAt, receivedAt doesn't need to be flagged
|
||||
const data = Template.currentData();
|
||||
const dueAt = data.getDue();
|
||||
const endAt = data.getEnd();
|
||||
const startAt = data.getStart();
|
||||
const theDate = tpl.date.get();
|
||||
|
||||
if (
|
||||
(startAt && theDate.isAfter(startAt)) ||
|
||||
(endAt && theDate.isAfter(endAt)) ||
|
||||
(dueAt && theDate.isAfter(dueAt))
|
||||
)
|
||||
classes += 'long-overdue';
|
||||
else classes += 'current';
|
||||
(startAt && isAfter(theDate, startAt)) ||
|
||||
(endAt && isAfter(theDate, endAt)) ||
|
||||
(dueAt && isAfter(theDate, dueAt))
|
||||
) {
|
||||
classes += 'overdue';
|
||||
} else {
|
||||
classes += 'not-due';
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
},
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-received-on')} ${this.date
|
||||
.get()
|
||||
.format('LLLL')}`;
|
||||
}
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
|
||||
},
|
||||
}));
|
||||
|
||||
events() {
|
||||
return super.events().concat({
|
||||
'click .js-edit-date': Popup.open('editCardReceivedDate'),
|
||||
});
|
||||
}
|
||||
}
|
||||
CardReceivedDate.register('cardReceivedDate');
|
||||
Template.cardReceivedDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardReceivedDate'),
|
||||
});
|
||||
|
||||
class CardStartDate extends CardDate {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getStart()));
|
||||
});
|
||||
}
|
||||
// cardStartDate
|
||||
Template.cardStartDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getStart()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.cardStartDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
let classes = 'start-date' + ' ';
|
||||
const dueAt = this.data().getDue();
|
||||
const endAt = this.data().getEnd();
|
||||
const theDate = this.date.get();
|
||||
const now = this.now.get();
|
||||
// if dueAt or endAt exist & are > startAt, startAt doesn't need to be flagged
|
||||
if ((endAt && theDate.isAfter(endAt)) || (dueAt && theDate.isAfter(dueAt)))
|
||||
classes += 'long-overdue';
|
||||
else if (theDate.isAfter(now)) classes += '';
|
||||
else classes += 'current';
|
||||
const tpl = Template.instance();
|
||||
let classes = 'start-date ';
|
||||
const data = Template.currentData();
|
||||
const dueAt = data.getDue();
|
||||
const endAt = data.getEnd();
|
||||
const theDate = tpl.date.get();
|
||||
const nowVal = tpl.now.get();
|
||||
|
||||
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
|
||||
classes += 'overdue';
|
||||
} else if (isAfter(theDate, nowVal)) {
|
||||
classes += 'not-due';
|
||||
} else {
|
||||
classes += 'current';
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
},
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
|
||||
}
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
|
||||
},
|
||||
}));
|
||||
|
||||
events() {
|
||||
return super.events().concat({
|
||||
'click .js-edit-date': Popup.open('editCardStartDate'),
|
||||
});
|
||||
}
|
||||
}
|
||||
CardStartDate.register('cardStartDate');
|
||||
Template.cardStartDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardStartDate'),
|
||||
});
|
||||
|
||||
class CardDueDate extends CardDate {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getDue()));
|
||||
});
|
||||
}
|
||||
// cardDueDate
|
||||
Template.cardDueDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getDue()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.cardDueDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
let classes = 'due-date' + ' ';
|
||||
const endAt = this.data().getEnd();
|
||||
const theDate = this.date.get();
|
||||
const now = this.now.get();
|
||||
// if the due date is after the end date, green - done early
|
||||
if (endAt && theDate.isAfter(endAt)) classes += 'current';
|
||||
// if there is an end date, don't need to flag the due date
|
||||
else if (endAt) classes += '';
|
||||
else if (now.diff(theDate, 'days') >= 2) classes += 'long-overdue';
|
||||
else if (now.diff(theDate, 'minute') >= 0) classes += 'due';
|
||||
else if (now.diff(theDate, 'days') >= -1) classes += 'almost-due';
|
||||
const tpl = Template.instance();
|
||||
let classes = 'due-date ';
|
||||
const data = Template.currentData();
|
||||
const endAt = data.getEnd();
|
||||
const theDate = tpl.date.get();
|
||||
const nowVal = tpl.now.get();
|
||||
|
||||
if (endAt && isBefore(endAt, theDate)) {
|
||||
classes += 'completed-early';
|
||||
} else if (endAt) {
|
||||
classes += 'completed';
|
||||
} else {
|
||||
const daysDiff = diff(theDate, nowVal, 'days');
|
||||
|
||||
if (daysDiff < 0) {
|
||||
classes += 'overdue';
|
||||
} else if (daysDiff <= 1) {
|
||||
classes += 'due-soon';
|
||||
} else {
|
||||
classes += 'not-due';
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
},
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-due-on')} ${this.date.get().format('LLLL')}`;
|
||||
}
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${TAPi18n.__('card-due-on')} ${formattedDate}`;
|
||||
},
|
||||
}));
|
||||
|
||||
events() {
|
||||
return super.events().concat({
|
||||
'click .js-edit-date': Popup.open('editCardDueDate'),
|
||||
});
|
||||
}
|
||||
}
|
||||
CardDueDate.register('cardDueDate');
|
||||
Template.cardDueDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardDueDate'),
|
||||
});
|
||||
|
||||
class CardEndDate extends CardDate {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getEnd()));
|
||||
});
|
||||
}
|
||||
// cardEndDate
|
||||
Template.cardEndDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getEnd()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.cardEndDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
let classes = 'end-date' + ' ';
|
||||
const dueAt = this.data().getDue();
|
||||
const theDate = this.date.get();
|
||||
if (!dueAt) classes += '';
|
||||
else if (theDate.isBefore(dueAt)) classes += 'current';
|
||||
else if (theDate.isAfter(dueAt)) classes += 'due';
|
||||
const tpl = Template.instance();
|
||||
let classes = 'end-date ';
|
||||
const data = Template.currentData();
|
||||
const dueAt = data.getDue();
|
||||
const theDate = tpl.date.get();
|
||||
|
||||
if (!dueAt) {
|
||||
classes += 'completed';
|
||||
} else if (isBefore(theDate, dueAt)) {
|
||||
classes += 'completed-early';
|
||||
} else if (isAfter(theDate, dueAt)) {
|
||||
classes += 'completed-late';
|
||||
} else {
|
||||
classes += 'completed-on-time';
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
},
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
|
||||
}
|
||||
const tpl = Template.instance();
|
||||
return `${TAPi18n.__('card-end-on')} ${format(tpl.date.get(), 'LLLL')}`;
|
||||
},
|
||||
}));
|
||||
|
||||
events() {
|
||||
return super.events().concat({
|
||||
'click .js-edit-date': Popup.open('editCardEndDate'),
|
||||
});
|
||||
}
|
||||
}
|
||||
CardEndDate.register('cardEndDate');
|
||||
Template.cardEndDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardEndDate'),
|
||||
});
|
||||
|
||||
class CardCustomFieldDate extends CardDate {
|
||||
template() {
|
||||
return 'dateCustomField';
|
||||
}
|
||||
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().value));
|
||||
});
|
||||
}
|
||||
|
||||
showWeek() {
|
||||
return this.date.get().week().toString();
|
||||
}
|
||||
|
||||
showWeekOfYear() {
|
||||
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
|
||||
}
|
||||
// cardCustomFieldDate
|
||||
Template.cardCustomFieldDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().value));
|
||||
});
|
||||
});
|
||||
|
||||
Template.cardCustomFieldDate.helpers(cardDateHelpers({
|
||||
showDate() {
|
||||
const tpl = Template.instance();
|
||||
// this will start working once mquandalle:moment
|
||||
// is updated to at least moment.js 2.10.5
|
||||
// until then, the date is displayed in the "L" format
|
||||
return this.date.get().calendar(null, {
|
||||
return tpl.date.get().calendar(null, {
|
||||
sameElse: 'llll',
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
showTitle() {
|
||||
return `${this.date.get().format('LLLL')}`;
|
||||
}
|
||||
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${formattedDate}`;
|
||||
},
|
||||
classes() {
|
||||
return 'customfield-date';
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
events() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
CardCustomFieldDate.register('cardCustomFieldDate');
|
||||
// --- Minicard date templates ---
|
||||
|
||||
(class extends CardReceivedDate {
|
||||
showDate() {
|
||||
return this.date.get().format('L');
|
||||
}
|
||||
}.register('minicardReceivedDate'));
|
||||
// minicardReceivedDate
|
||||
Template.minicardReceivedDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getReceived()));
|
||||
});
|
||||
});
|
||||
|
||||
(class extends CardStartDate {
|
||||
showDate() {
|
||||
return this.date.get().format('YYYY-MM-DD HH:mm');
|
||||
}
|
||||
}.register('minicardStartDate'));
|
||||
|
||||
(class extends CardDueDate {
|
||||
showDate() {
|
||||
return this.date.get().format('YYYY-MM-DD HH:mm');
|
||||
}
|
||||
}.register('minicardDueDate'));
|
||||
|
||||
(class extends CardEndDate {
|
||||
showDate() {
|
||||
return this.date.get().format('YYYY-MM-DD HH:mm');
|
||||
}
|
||||
}.register('minicardEndDate'));
|
||||
|
||||
(class extends CardCustomFieldDate {
|
||||
showDate() {
|
||||
return this.date.get().format('L');
|
||||
}
|
||||
}.register('minicardCustomFieldDate'));
|
||||
|
||||
class VoteEndDate extends CardDate {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getVoteEnd()));
|
||||
});
|
||||
}
|
||||
Template.minicardReceivedDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
const classes = 'end-date' + ' ';
|
||||
const tpl = Template.instance();
|
||||
let classes = 'received-date ';
|
||||
const data = Template.currentData();
|
||||
const dueAt = data.getDue();
|
||||
const endAt = data.getEnd();
|
||||
const startAt = data.getStart();
|
||||
const theDate = tpl.date.get();
|
||||
|
||||
if (
|
||||
(startAt && isAfter(theDate, startAt)) ||
|
||||
(endAt && isAfter(theDate, endAt)) ||
|
||||
(dueAt && isAfter(theDate, dueAt))
|
||||
) {
|
||||
classes += 'overdue';
|
||||
} else {
|
||||
classes += 'not-due';
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
showDate() {
|
||||
return this.date.get().format('L LT');
|
||||
}
|
||||
},
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
|
||||
}
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
}));
|
||||
|
||||
events() {
|
||||
return super.events().concat({
|
||||
'click .js-edit-date': Popup.open('editVoteEndDate'),
|
||||
});
|
||||
}
|
||||
}
|
||||
VoteEndDate.register('voteEndDate');
|
||||
Template.minicardReceivedDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardReceivedDate'),
|
||||
});
|
||||
|
||||
class PokerEndDate extends CardDate {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getPokerEnd()));
|
||||
});
|
||||
}
|
||||
// minicardStartDate
|
||||
Template.minicardStartDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getStart()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.minicardStartDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
const classes = 'end-date' + ' ';
|
||||
return classes;
|
||||
}
|
||||
showDate() {
|
||||
return this.date.get().format('l LT');
|
||||
}
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
|
||||
}
|
||||
const tpl = Template.instance();
|
||||
let classes = 'start-date ';
|
||||
const data = Template.currentData();
|
||||
const dueAt = data.getDue();
|
||||
const endAt = data.getEnd();
|
||||
const theDate = tpl.date.get();
|
||||
const nowVal = tpl.now.get();
|
||||
|
||||
events() {
|
||||
return super.events().concat({
|
||||
'click .js-edit-date': Popup.open('editPokerEndDate'),
|
||||
});
|
||||
}
|
||||
}
|
||||
PokerEndDate.register('pokerEndDate');
|
||||
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
|
||||
classes += 'overdue';
|
||||
} else if (isAfter(theDate, nowVal)) {
|
||||
classes += 'not-due';
|
||||
} else {
|
||||
classes += 'current';
|
||||
}
|
||||
return classes;
|
||||
},
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
}));
|
||||
|
||||
Template.minicardStartDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardStartDate'),
|
||||
});
|
||||
|
||||
// minicardDueDate
|
||||
Template.minicardDueDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getDue()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.minicardDueDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
const tpl = Template.instance();
|
||||
let classes = 'due-date ';
|
||||
const data = Template.currentData();
|
||||
const endAt = data.getEnd();
|
||||
const theDate = tpl.date.get();
|
||||
const nowVal = tpl.now.get();
|
||||
|
||||
if (endAt && isBefore(endAt, theDate)) {
|
||||
classes += 'completed-early';
|
||||
} else if (endAt) {
|
||||
classes += 'completed';
|
||||
} else {
|
||||
const daysDiff = diff(theDate, nowVal, 'days');
|
||||
|
||||
if (daysDiff < 0) {
|
||||
classes += 'overdue';
|
||||
} else if (daysDiff <= 1) {
|
||||
classes += 'due-soon';
|
||||
} else {
|
||||
classes += 'not-due';
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
},
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${TAPi18n.__('card-due-on')} ${formattedDate}`;
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
}));
|
||||
|
||||
Template.minicardDueDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardDueDate'),
|
||||
});
|
||||
|
||||
// minicardEndDate
|
||||
Template.minicardEndDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getEnd()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.minicardEndDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
const tpl = Template.instance();
|
||||
let classes = 'end-date ';
|
||||
const data = Template.currentData();
|
||||
const dueAt = data.getDue();
|
||||
const theDate = tpl.date.get();
|
||||
|
||||
if (!dueAt) {
|
||||
classes += 'completed';
|
||||
} else if (isBefore(theDate, dueAt)) {
|
||||
classes += 'completed-early';
|
||||
} else if (isAfter(theDate, dueAt)) {
|
||||
classes += 'completed-late';
|
||||
} else {
|
||||
classes += 'completed-on-time';
|
||||
}
|
||||
return classes;
|
||||
},
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
return `${TAPi18n.__('card-end-on')} ${format(tpl.date.get(), 'LLLL')}`;
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
}));
|
||||
|
||||
Template.minicardEndDate.events({
|
||||
'click .js-edit-date': Popup.open('editCardEndDate'),
|
||||
});
|
||||
|
||||
// minicardCustomFieldDate
|
||||
Template.minicardCustomFieldDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().value));
|
||||
});
|
||||
});
|
||||
|
||||
Template.minicardCustomFieldDate.helpers(cardDateHelpers({
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
|
||||
return `${formattedDate}`;
|
||||
},
|
||||
classes() {
|
||||
return 'customfield-date';
|
||||
},
|
||||
}));
|
||||
|
||||
// --- Vote and Poker end date badge templates ---
|
||||
|
||||
// voteEndDate
|
||||
Template.voteEndDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getVoteEnd()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.voteEndDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
return 'end-date ';
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
return `${TAPi18n.__('card-end-on')} ${tpl.date.get().toLocaleString()}`;
|
||||
},
|
||||
}));
|
||||
|
||||
Template.voteEndDate.events({
|
||||
'click .js-edit-date': Popup.open('editVoteEndDate'),
|
||||
});
|
||||
|
||||
// pokerEndDate
|
||||
Template.pokerEndDate.onCreated(function () {
|
||||
cardDateOnCreated(this);
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(new Date(Template.currentData().getPokerEnd()));
|
||||
});
|
||||
});
|
||||
|
||||
Template.pokerEndDate.helpers(cardDateHelpers({
|
||||
classes() {
|
||||
return 'end-date ';
|
||||
},
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
|
||||
},
|
||||
showTitle() {
|
||||
const tpl = Template.instance();
|
||||
return `${TAPi18n.__('card-end-on')} ${format(tpl.date.get(), 'LLLL')}`;
|
||||
},
|
||||
}));
|
||||
|
||||
Template.pokerEndDate.events({
|
||||
'click .js-edit-date': Popup.open('editPokerEndDate'),
|
||||
});
|
||||
@@ -1,37 +1,29 @@
|
||||
const descriptionFormIsOpen = new ReactiveVar(false);
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onDestroyed() {
|
||||
descriptionFormIsOpen.set(false);
|
||||
$('.note-popover').hide();
|
||||
},
|
||||
Template.descriptionForm.onDestroyed(function () {
|
||||
descriptionFormIsOpen.set(false);
|
||||
$('.note-popover').hide();
|
||||
});
|
||||
|
||||
Template.descriptionForm.helpers({
|
||||
descriptionFormIsOpen() {
|
||||
return descriptionFormIsOpen.get();
|
||||
},
|
||||
});
|
||||
|
||||
getInput() {
|
||||
return this.$('.js-new-description-input');
|
||||
Template.descriptionForm.events({
|
||||
async 'submit .js-card-description'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const description = tpl.currentComponent ? tpl.currentComponent().getValue() : tpl.$('textarea').val();
|
||||
await this.setDescription(description);
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-description'(event) {
|
||||
event.preventDefault();
|
||||
const description = this.currentComponent().getValue();
|
||||
this.data().setDescription(description);
|
||||
},
|
||||
// Pressing Ctrl+Enter should submit the form
|
||||
'keydown form textarea'(evt) {
|
||||
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
|
||||
const submitButton = this.find('button[type=submit]');
|
||||
if (submitButton) {
|
||||
submitButton.click();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
// Pressing Ctrl+Enter should submit the form
|
||||
'keydown form textarea'(evt, tpl) {
|
||||
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
|
||||
const submitButton = tpl.find('button[type=submit]');
|
||||
if (submitButton) {
|
||||
submitButton.click();
|
||||
}
|
||||
}
|
||||
},
|
||||
}).register('descriptionForm');
|
||||
});
|
||||
@@ -1,16 +1,46 @@
|
||||
/* Date Format Selector */
|
||||
.card-details-item-date-format {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-details-item-date-format .card-details-item-title {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 6px;
|
||||
color: #333;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.card-details-item-date-format .js-date-format-selector {
|
||||
width: 100%;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.card-details-item-date-format .js-date-format-selector:focus {
|
||||
outline: none;
|
||||
border-color: #007cba;
|
||||
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2);
|
||||
}
|
||||
|
||||
.assignee {
|
||||
border-radius: 0.4vw;
|
||||
display: block;
|
||||
position: relative;
|
||||
float: left;
|
||||
height: 4vw;
|
||||
width: 4vw;
|
||||
margin: 0.4vh;
|
||||
height: clamp(24px, 3.5vw, 36px);
|
||||
width: clamp(24px, 3.5vw, 36px);
|
||||
margin: 0.3vh;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
z-index: 1;
|
||||
text-decoration: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.04);
|
||||
}
|
||||
.assignee .avatar {
|
||||
overflow: hidden;
|
||||
@@ -23,23 +53,28 @@
|
||||
background-color: #dbdbdb;
|
||||
color: #444;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
.assignee .avatar.avatar-image {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.assignee .assignee-presence-status {
|
||||
background-color: #b3b3b3;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 50%;
|
||||
height: 1vw;
|
||||
width: 1vw;
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
position: absolute;
|
||||
right: -0.1vw;
|
||||
bottom: -0.1vw;
|
||||
border: 1px solid #fff;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
z-index: 15;
|
||||
}
|
||||
.assignee .assignee-presence-status.active {
|
||||
@@ -63,6 +98,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 0 2px #bfbfbf inset;
|
||||
transition: box-shadow 0.12s;
|
||||
}
|
||||
.assignee.add-assignee:hover,
|
||||
.assignee.add-assignee.is-active {
|
||||
@@ -74,22 +110,83 @@
|
||||
background-color: rgba(0,0,0,0.875);
|
||||
color: #fff;
|
||||
border-radius: 0.7vw;
|
||||
font-size: 0.98em;
|
||||
}
|
||||
|
||||
.card-details {
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: min(600px, 80vw);
|
||||
will-change: flex-basis;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: #f7f7f7;
|
||||
border-radius: bottom 0.4vw;
|
||||
border-radius: 0 0 0.4vw 0.4vw;
|
||||
z-index: 30;
|
||||
animation: flexGrowIn 0.1s;
|
||||
box-shadow: 0 0 0.9vh 0 #b3b3b3;
|
||||
transition: flex-basis 0.1s;
|
||||
transition: flex-basis 0.1s, box-shadow 0.15s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Desktop mode: position card below board header */
|
||||
body.desktop-mode .card-details:not(.card-details-popup) {
|
||||
position: fixed;
|
||||
width: auto;
|
||||
max-width: 800px;
|
||||
flex-basis: auto;
|
||||
border-radius: 8px;
|
||||
z-index: 2001;
|
||||
}
|
||||
|
||||
/* Default position for first card or when dragged */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):not([style*="left"]):not([style*="top"]) {
|
||||
top: 8px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
/* Stagger positions for multiple cards using nth-of-type */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(1) {
|
||||
top: 8px;
|
||||
left: 20px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(2) {
|
||||
top: 38px;
|
||||
left: 50px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(3) {
|
||||
top: 68px;
|
||||
left: 80px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(4) {
|
||||
top: 98px;
|
||||
left: 110px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(5) {
|
||||
top: 128px;
|
||||
left: 140px;
|
||||
}
|
||||
|
||||
/* For expanded cards, set dimensions */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):not(.card-details-collapsed) {
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
/* Collapsed card state - hide content and set height to title row only */
|
||||
.card-details.card-details-collapsed .card-details-canvas > *:not(.card-details-header) {
|
||||
display: none !important;
|
||||
}
|
||||
.card-details.card-details-collapsed {
|
||||
height: auto !important;
|
||||
bottom: auto !important;
|
||||
overflow: visible;
|
||||
}
|
||||
body.desktop-mode .card-details.card-details-collapsed {
|
||||
bottom: auto !important;
|
||||
}
|
||||
.card-details .mCustomScrollBox {
|
||||
padding-left: 0;
|
||||
}
|
||||
@@ -99,18 +196,47 @@
|
||||
}
|
||||
.card-details .card-details-header {
|
||||
margin: 0 -20px 5px;
|
||||
padding: 7px 20px;
|
||||
padding: 8px 20px;
|
||||
background: #ededed;
|
||||
border-bottom: 1px solid #dbdbdb;
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
z-index: 500;
|
||||
display: flow-root;
|
||||
min-height: 44px;
|
||||
}
|
||||
.card-details .card-details-header .card-number {
|
||||
color: #b3b3b3;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* Collapse toggle triangle */
|
||||
.card-details .card-details-header .card-collapse-toggle {
|
||||
float: left;
|
||||
font-size: 20px;
|
||||
padding: 7px 10px;
|
||||
margin-left: -10px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: #000;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-details .card-details-header .card-drag-handle {
|
||||
font-size: 20px;
|
||||
padding: 8px 10px;
|
||||
margin-right: 10px;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-details .card-details-header .close-card-details,
|
||||
.card-details .card-details-header .maximize-card-details,
|
||||
.card-details .card-details-header .minimize-card-details,
|
||||
@@ -128,11 +254,19 @@
|
||||
font-size: 24px;
|
||||
padding: 5px 10px 5px 10px;
|
||||
margin-right: -8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
transition: color 0.13s;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details-mobile-web {
|
||||
.card-details .card-details-header .close-card-details-mobile-web,
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
font-size: 24px;
|
||||
padding: 5px;
|
||||
margin-right: 40px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.card-details .card-details-header .card-copy-button {
|
||||
font-size: 17px;
|
||||
@@ -147,12 +281,44 @@
|
||||
.card-details .card-details-header .card-details-menu {
|
||||
font-size: 17px;
|
||||
padding: 10px;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.card-details .card-details-header .card-details-menu-mobile-web {
|
||||
font-size: 17px;
|
||||
padding: 10px;
|
||||
margin-right: 30px;
|
||||
}
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle,
|
||||
.card-details .card-details-header .card-zoom-in,
|
||||
.card-details .card-details-header .card-zoom-out {
|
||||
font-size: 24px;
|
||||
padding: 5px 10px 5px 10px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* Unify all card text to match title size */
|
||||
.card-details {
|
||||
font-size: 1em;
|
||||
}
|
||||
.card-details p,
|
||||
.card-details span,
|
||||
.card-details div,
|
||||
.card-details a,
|
||||
.card-details label,
|
||||
.card-details input,
|
||||
.card-details textarea,
|
||||
.card-details select,
|
||||
.card-details button,
|
||||
.card-details .card-details-item-title,
|
||||
.card-details .card-label,
|
||||
.card-details .viewer {
|
||||
font-size: inherit;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.card-details .card-details-header .card-details-watch {
|
||||
font-size: 17px;
|
||||
padding-left: 7px;
|
||||
@@ -160,9 +326,13 @@
|
||||
}
|
||||
.card-details .card-details-header .card-details-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.33em;
|
||||
font-size: 1.35em;
|
||||
margin: 7px 0 0;
|
||||
padding: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.card-details .card-details-header .linked-card-location {
|
||||
font-style: italic;
|
||||
@@ -177,10 +347,10 @@
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.card-details .card-details-header form.inlined-form .copied-tooltip {
|
||||
padding: 0px 10px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.card-details .card-details-header .card-details-list {
|
||||
font-size: 0.85em;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.card-details .card-details-header .card-details-list a.card-details-list-title {
|
||||
@@ -190,7 +360,7 @@
|
||||
display: inline-block;
|
||||
background: #e6e6e6;
|
||||
border-radius: 3px;
|
||||
padding: 0px 5px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
.card-details .card-details-header .copied-tooltip {
|
||||
margin-right: 10px;
|
||||
@@ -201,11 +371,13 @@
|
||||
}
|
||||
.card-details .card-description textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
.card-details .card-details-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 15px 0;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.card-details .card-details-items .card-details-item {
|
||||
margin-right: 0.5em;
|
||||
@@ -256,15 +428,28 @@
|
||||
position: fixed;
|
||||
resize: both;
|
||||
}
|
||||
|
||||
/* Override for mobile mode even on larger screens */
|
||||
body.mobile-mode .card-details {
|
||||
width: 100vw !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
.card-details-maximized {
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: calc(100% - 20px);
|
||||
will-change: flex-basis;
|
||||
overflow-y: scroll;
|
||||
overflow-x: scroll;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
background: #f7f7f7;
|
||||
border-radius: bottom 3px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
z-index: 100;
|
||||
animation: flexGrowIn 0.1s;
|
||||
box-shadow: 0 0 7px 0 #b3b3b3;
|
||||
@@ -307,19 +492,52 @@ input[type="submit"].attachment-add-link-submit {
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
.card-details {
|
||||
width: calc(100% - 1px);
|
||||
padding: 0px 20px 0px 20px;
|
||||
margin: 0px;
|
||||
width: 100% !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
transition: none;
|
||||
overflow-y: revert;
|
||||
overflow-x: revert;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 100 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Ensure card details are above everything on mobile */
|
||||
body.mobile-mode .card-details {
|
||||
z-index: 100 !important;
|
||||
width: 100vw !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
.card-details .card-details-canvas {
|
||||
width: 100%;
|
||||
padding-left: 0px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details {
|
||||
margin-right: 0px;
|
||||
display: block !important;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details-mobile-web {
|
||||
display: block !important;
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
display: block !important;
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
display: block !important;
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
.card-details .card-details-header .card-details-menu {
|
||||
margin-right: 40px;
|
||||
@@ -345,6 +563,62 @@ input[type="submit"].attachment-add-link-submit {
|
||||
.pop-over > .content-wrapper > .popup-container-depth-0 .card-details-header {
|
||||
margin: 0;
|
||||
}
|
||||
/* iPhone mobile: enlarge header buttons and increase spacing */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header {
|
||||
padding-right: 16px;
|
||||
}
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .maximize-card-details,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .minimize-card-details,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-details-menu-mobile-web,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-copy-mobile-button,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-mobile-desktop-toggle,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-in,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-out {
|
||||
font-size: 2em !important; /* 2x bigger */
|
||||
padding: 0.3em !important;
|
||||
margin-right: 0.75em !important; /* 2x space compared to default */
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
/* Avoid clipping of the close button on the right edge */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details {
|
||||
margin-right: 0.75em !important;
|
||||
}
|
||||
/* Enlarge the header title too */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-details-title {
|
||||
font-size: 1.2em !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile mode styles - apply when body has mobile-mode class regardless of screen size */
|
||||
body.mobile-mode .card-details {
|
||||
width: 100vw !important;
|
||||
padding: 0px !important;
|
||||
margin: 0px !important;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 100 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body.mobile-mode .card-details .card-details-canvas {
|
||||
width: 100% !important;
|
||||
padding: 0 15px !important;
|
||||
}
|
||||
|
||||
body.mobile-mode .card-details .card-details-header .close-card-details,
|
||||
body.mobile-mode .card-details .card-details-header .close-card-details-mobile-web {
|
||||
display: block !important;
|
||||
}
|
||||
.card-details-white {
|
||||
background: #fff !important;
|
||||
@@ -453,13 +727,15 @@ input[type="submit"].attachment-add-link-submit {
|
||||
.vote-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.vote-title .js-edit-date {
|
||||
align-self: baseline;
|
||||
margin-left: 5px;
|
||||
align-self: flex-start;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.vote-result {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.js-show-positive-votes {
|
||||
cursor: pointer;
|
||||
@@ -470,29 +746,33 @@ input[type="submit"].attachment-add-link-submit {
|
||||
.poker-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.poker-title .js-edit-date {
|
||||
align-self: baseline;
|
||||
margin-left: 5px;
|
||||
align-self: flex-start;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.poker-result {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
}
|
||||
.js-show-positive-poker-votes {
|
||||
cursor: pointer;
|
||||
}
|
||||
.poker-deck {
|
||||
display: grid;
|
||||
flex-direction: column;
|
||||
grid-auto-flow: row;
|
||||
text-align: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.poker-card-result {
|
||||
width: 32px;
|
||||
width: 34px;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
padding: 4px 2px 4px 2px;
|
||||
padding: 4px 2px;
|
||||
cursor: default;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.winner {
|
||||
font-weight: bold;
|
||||
@@ -503,6 +783,7 @@ input[type="submit"].attachment-add-link-submit {
|
||||
}
|
||||
.responsive-table {
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
}
|
||||
.poker-table {
|
||||
display: table;
|
||||
@@ -565,11 +846,15 @@ input[type="submit"].attachment-add-link-submit {
|
||||
margin: auto;
|
||||
margin-right: 10px;
|
||||
width: 100px;
|
||||
border-radius: 2px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
.estimation-add button {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin: auto;
|
||||
border-radius: 2px;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
.poker-card {
|
||||
width: 48px;
|
||||
@@ -588,6 +873,7 @@ input[type="submit"].attachment-add-link-submit {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.12s;
|
||||
}
|
||||
.poker-card .inner {
|
||||
display: table-cell;
|
||||
|
||||
@@ -5,49 +5,72 @@ template(name="cardDetails")
|
||||
|
||||
+attachmentViewer
|
||||
|
||||
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}'): .card-details-canvas
|
||||
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}' class='{{#if cardCollapsed}}card-details-collapsed{{/if}}'): .card-details-canvas
|
||||
.card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
|
||||
+inlinedForm(classNames="js-card-details-title")
|
||||
+editCardTitleForm
|
||||
else
|
||||
unless isMiniScreen
|
||||
unless isPopup
|
||||
a.fa.fa-times-thin.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
if canModifyCard
|
||||
if cardMaximized
|
||||
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
else
|
||||
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
span.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
|
||||
if cardCollapsed
|
||||
i.fa.fa-caret-right
|
||||
else
|
||||
i.fa.fa-caret-down
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
i.fa.fa-times-thin
|
||||
if cardMaximized
|
||||
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
else
|
||||
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
a.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
a.card-copy-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
if canModifyCard
|
||||
a.fa.fa-navicon.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
a.fa.fa-link.card-copy-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
class="fa-link"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
span.card-drag-handle.js-card-drag-handle(title="Drag card")
|
||||
i.fa.fa-arrows
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
else
|
||||
unless isPopup
|
||||
a.fa.fa-times-thin.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
if canModifyCard
|
||||
a.fa.fa-navicon.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
a.fa.fa-link.card-copy-mobile-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
class="fa-link"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
i.fa.fa-times-thin
|
||||
a.card-zoom-out.js-card-zoom-out(title="{{_ 'zoom-out'}}")
|
||||
i.fa.fa-search-minus
|
||||
a.card-zoom-in.js-card-zoom-in(title="{{_ 'zoom-in'}}")
|
||||
i.fa.fa-search-plus
|
||||
a.card-mobile-desktop-toggle.js-card-mobile-desktop-toggle(title="{{_ 'mobile-desktop-toggle'}}")
|
||||
if mobileMode
|
||||
i.fa.fa-desktop
|
||||
else
|
||||
i.fa.fa-mobile
|
||||
if cardMaximized
|
||||
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
else
|
||||
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
a.card-copy-mobile-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
h2.card-details-title.js-card-title(
|
||||
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
|
||||
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{else}}js-card-title-drag-handle{{/if}}")
|
||||
+viewer
|
||||
if currentBoard.allowsCardNumber
|
||||
span.card-number
|
||||
| ##{getCardNumber}
|
||||
= getTitle
|
||||
if isWatching
|
||||
i.card-details-watch.fa.fa-eye
|
||||
i.card-details-watch
|
||||
i.fa.fa-eye
|
||||
.card-details-path
|
||||
each parentList
|
||||
| >
|
||||
@@ -106,6 +129,16 @@ template(name="cardDetails")
|
||||
if currentBoard.hasAnyAllowsDate
|
||||
hr
|
||||
|
||||
.card-details-item.card-details-item-date-format
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-calendar
|
||||
| {{_ 'date-format'}}
|
||||
.card-details-item-content
|
||||
select.js-date-format-selector
|
||||
option(value="YYYY-MM-DD" selected="{{#if isDateFormat 'YYYY-MM-DD'}}selected{{/if}}") {{_ 'date-format-yyyy-mm-dd'}}
|
||||
option(value="DD-MM-YYYY" selected="{{#if isDateFormat 'DD-MM-YYYY'}}selected{{/if}}") {{_ 'date-format-dd-mm-yyyy'}}
|
||||
option(value="MM-DD-YYYY" selected="{{#if isDateFormat 'MM-DD-YYYY'}}selected{{/if}}") {{_ 'date-format-mm-dd-yyyy'}}
|
||||
|
||||
if currentBoard.allowsReceivedDate
|
||||
.card-details-item.card-details-item-received
|
||||
h3.card-details-item-title
|
||||
@@ -135,7 +168,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsDueDate
|
||||
.card-details-item.card-details-item-due
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sign-in
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'card-due'}}
|
||||
if getDue
|
||||
+cardDueDate
|
||||
@@ -248,7 +281,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsCardSortingByNumber
|
||||
.card-details-item.card-details-sort-order
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sort
|
||||
i.fa.fa-sort-numeric-asc
|
||||
| {{_ 'sort'}}
|
||||
if canModifyCard
|
||||
+inlinedForm(classNames="js-card-details-sort")
|
||||
@@ -287,7 +320,7 @@ template(name="cardDetails")
|
||||
hr
|
||||
.card-details-item.card-details-item-customfield
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-list-alt
|
||||
i.fa.fa-list
|
||||
= definition.name
|
||||
+cardCustomField
|
||||
|
||||
@@ -298,7 +331,7 @@ template(name="cardDetails")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleCustomFieldsGridButton")
|
||||
label.toggle-label(for="toggleCustomFieldsGridButton")
|
||||
a.fa.fa-plus.js-custom-fields.card-details-item.custom-fields(title="{{_ 'custom-fields'}}")
|
||||
a.js-custom-fields.card-details-item.custom-fields(title="{{_ 'custom-fields'}}")
|
||||
|
||||
if getVoteQuestion
|
||||
hr
|
||||
@@ -317,7 +350,8 @@ template(name="cardDetails")
|
||||
.card-label.card-label-green {{ voteCountPositive }}
|
||||
.card-label.card-label-red {{ voteCountNegative }}
|
||||
unless ($and currentBoard.isPublic voteAllowNonBoardMembers )
|
||||
.card-label.card-label-gray {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
.card-label.card-label-gray
|
||||
| {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
+viewer
|
||||
= getVoteQuestion
|
||||
if showVotingButtons
|
||||
@@ -327,7 +361,7 @@ template(name="cardDetails")
|
||||
| {{_ 'vote-for-it'}}
|
||||
button.card-details-red.js-vote.js-vote-negative(class="{{#if $eq voteState false}}voted{{/if}}")
|
||||
if $eq voteState false
|
||||
i.fa.fa-thumbs-down
|
||||
| 👎
|
||||
| {{_ 'vote-against'}}
|
||||
|
||||
if getPokerQuestion
|
||||
@@ -343,7 +377,8 @@ template(name="cardDetails")
|
||||
.poker-result
|
||||
if expiredPoker
|
||||
unless ($and currentBoard.isPublic pokerAllowNonBoardMembers )
|
||||
.card-label.card-label-gray {{ pokerCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
.card-label.card-label-gray
|
||||
| {{ pokerCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
if showPlanningPokerButtons
|
||||
.poker-result
|
||||
.poker-deck
|
||||
@@ -535,14 +570,14 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsDescriptionTitle
|
||||
hr
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-align-left
|
||||
i.fa.fa-file-text-o
|
||||
| {{_ 'description'}}
|
||||
if currentBoard.allowsDescriptionText
|
||||
+inlinedCardDescription(classNames="card-description js-card-description")
|
||||
+descriptionForm
|
||||
.edit-controls.clearfix
|
||||
button.primary(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
else
|
||||
if currentBoard.allowsDescriptionText
|
||||
a.js-open-inlined-form(title="{{_ 'edit'}}" value=title)
|
||||
@@ -598,13 +633,15 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsComments
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isNoComments
|
||||
+commentForm
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
+commentForm
|
||||
+comments
|
||||
hr
|
||||
|
||||
.card-details-right
|
||||
|
||||
unless currentUser.isNoComments
|
||||
if currentUser.isBoardAdmin
|
||||
.activity-title
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-history
|
||||
@@ -617,7 +654,7 @@ template(name="cardDetails")
|
||||
input.toggle-switch(type="checkbox" id="toggleShowActivitiesCard")
|
||||
label.toggle-label(for="toggleShowActivitiesCard")
|
||||
|
||||
unless currentUser.isNoComments
|
||||
if currentUser.isBoardAdmin
|
||||
if isLoaded.get
|
||||
if isLinkedCard
|
||||
+activities(card=this mode="linkedcard")
|
||||
@@ -627,31 +664,31 @@ template(name="cardDetails")
|
||||
+activities(card=this mode="card")
|
||||
|
||||
template(name="editCardTitleForm")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
a(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
textarea.js-edit-card-title(rows='1' autofocus dir="auto")
|
||||
= getTitle
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-title-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
|
||||
template(name="editCardRequesterForm")
|
||||
input.js-edit-card-requester(type='text' autofocus value=getRequestedBy dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-requester-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
|
||||
template(name="editCardAssignerForm")
|
||||
input.js-edit-card-assigner(type='text' autofocus value=getAssignedBy dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-assigner-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
|
||||
template(name="editCardSortOrderForm")
|
||||
input.js-edit-card-sort(type='text' autofocus value=sort dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-sort-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
|
||||
template(name="cardDetailsActionsPopup")
|
||||
ul.pop-over-list
|
||||
@@ -661,7 +698,7 @@ template(name="cardDetailsActionsPopup")
|
||||
i.fa.fa-eye
|
||||
| {{_ 'unwatch'}}
|
||||
else
|
||||
i.fa.fa-eye-slash
|
||||
i.fa.fa-eye
|
||||
| {{_ 'watch'}}
|
||||
hr
|
||||
if canModifyCard
|
||||
@@ -681,7 +718,7 @@ template(name="cardDetailsActionsPopup")
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-custom-fields
|
||||
i.fa.fa-list-alt
|
||||
i.fa.fa-list
|
||||
| {{_ 'card-edit-custom-fields'}}
|
||||
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
|
||||
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
|
||||
@@ -701,102 +738,263 @@ template(name="cardDetailsActionsPopup")
|
||||
i.fa.fa-eye
|
||||
| {{_ 'hide-list-on-minicard'}}
|
||||
else
|
||||
i.fa.fa-eye-slash
|
||||
i.fa.fa-eye
|
||||
| {{_ 'show-list-on-minicard'}}
|
||||
hr
|
||||
if canModifyCard
|
||||
hr
|
||||
else
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-export-card
|
||||
i.fa.fa-share-alt
|
||||
i.fa.fa-upload
|
||||
| {{_ 'export-card'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-move-card-to-top
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-move-card
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
li
|
||||
a.js-copy-card
|
||||
i.fa.fa-copy
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-copy-checklist-cards
|
||||
i.fa.fa-copy
|
||||
i.fa.fa-copy
|
||||
| {{_ 'copyManyCardsPopup-title'}}
|
||||
unless archived
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-archive
|
||||
i.fa.fa-arrow-right
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archive-card'}}
|
||||
unless canModifyCard
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-move-card-to-top
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-move-card
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
li
|
||||
a.js-copy-card
|
||||
i.fa.fa-clipboard
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-copy-checklist-cards
|
||||
i.fa.fa-copy
|
||||
| {{_ 'copyManyCardsPopup-title'}}
|
||||
unless archived
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-archive
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archive-card'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
| {{_ 'cardMorePopup-title'}}
|
||||
if canModifyCard
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
i.fa.fa-link
|
||||
| {{_ 'cardMorePopup-title'}}
|
||||
a.js-move-card-to-top
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-move-card
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
li
|
||||
a.js-copy-card
|
||||
i.fa.fa-clipboard
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-copy-checklist-cards
|
||||
i.fa.fa-copy
|
||||
| {{_ 'copyManyCardsPopup-title'}}
|
||||
unless archived
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-archive
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archive-card'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
| {{_ 'cardMorePopup-title'}}
|
||||
|
||||
template(name="exportCardPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a(href="{{exportUrlCardPDF}}",, download="{{exportFilenameCardPDF}}")
|
||||
i.fa.fa-share-alt
|
||||
a(href="{{exportUrlCardPDF}}", download="{{exportFilenameCardPDF}}")
|
||||
i.fa.fa-file-pdf-o
|
||||
| {{_ 'export-card-pdf'}}
|
||||
hr
|
||||
p.quiet {{_ 'export-card-excel-fields'}}
|
||||
ul.pop-over-list
|
||||
each excelExportFields
|
||||
li(class="{{#if checked}}active{{/if}}")
|
||||
a.js-excel-field-toggle(data-field="{{field}}" href="#")
|
||||
i.fa.fa-check
|
||||
| {{_ label}}
|
||||
ul.pop-over-list
|
||||
li
|
||||
a(href="{{exportUrlCardExcel}}", download="{{exportFilenameCardExcel}}")
|
||||
i.fa.fa-file-excel-o
|
||||
| {{_ 'download'}}
|
||||
|
||||
template(name="moveCardPopup")
|
||||
+copyAndMoveCard
|
||||
label(for='move-card-title') {{_ 'title'}}:
|
||||
input#move-card-title.full-line(type="text" value="{{getTitle}}" autofocus)
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
||||
template(name="copyCardPopup")
|
||||
label(for='copy-card-title') {{_ 'title'}}:
|
||||
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
|
||||
= getTitle
|
||||
+copyAndMoveCard
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
||||
template(name="copyManyCardsPopup")
|
||||
label(for='copy-checklist-cards-title') {{_ 'copyManyCardsPopup-instructions'}}:
|
||||
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
|
||||
| {{_ 'copyManyCardsPopup-format'}}
|
||||
+copyAndMoveCard
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
||||
template(name="convertChecklistItemToCardPopup")
|
||||
label(for='convert-checklist-item-to-card-title') {{_ 'title'}}:
|
||||
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
|
||||
= item.title
|
||||
+copyAndMoveCard
|
||||
|
||||
template(name="copyAndMoveCard")
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
@@ -807,13 +1005,13 @@ template(name="cardMembersPopup")
|
||||
each members
|
||||
li.item(class="{{#if isCardMember}}active{{/if}}")
|
||||
a.name.js-select-member(href="#")
|
||||
+userAvatar(userId=user._id)
|
||||
+userAvatar(userId=userId)
|
||||
span.full-name
|
||||
= user.profile.fullname
|
||||
| (<span class="username">{{ user.username }}</span>)
|
||||
= userData.profile.fullname
|
||||
if userData.username
|
||||
| (#{userData.username})
|
||||
if isCardMember
|
||||
i.fa.fa-check
|
||||
|
||||
template(name="cardAssigneesPopup")
|
||||
input.card-assignees-filter(type="text" placeholder="{{_ 'search'}}")
|
||||
unless currentUser.isWorker
|
||||
@@ -821,10 +1019,11 @@ template(name="cardAssigneesPopup")
|
||||
each members
|
||||
li.item(class="{{#if isCardAssignee}}active{{/if}}")
|
||||
a.name.js-select-assignee(href="#")
|
||||
+userAvatar(userId=user._id)
|
||||
+userAvatar(userId=userId)
|
||||
span.full-name
|
||||
= user.profile.fullname
|
||||
| (<span class="username">{{ user.username }}</span>)
|
||||
= userData.profile.fullname
|
||||
if userData.username
|
||||
| (#{userData.username})
|
||||
if isCardAssignee
|
||||
i.fa.fa-check
|
||||
if currentUser.isWorker
|
||||
@@ -834,10 +1033,10 @@ template(name="cardAssigneesPopup")
|
||||
+userAvatar(userId=currentUser._id)
|
||||
span.full-name
|
||||
= currentUser.profile.fullname
|
||||
| (<span class="username">{{ currentUser.username }}</span>)
|
||||
if currentUser.username
|
||||
| (#{currentUser.username})
|
||||
if currentUser.isCardAssignee
|
||||
i.fa.fa-check
|
||||
|
||||
template(name="cardAssigneePopup")
|
||||
.board-assignee-menu
|
||||
.mini-profile-info
|
||||
@@ -860,7 +1059,7 @@ template(name="cardMorePopup")
|
||||
span.clearfix
|
||||
span {{_ 'link-card'}}
|
||||
= ' '
|
||||
i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
input.inline-input(type="text" id="cardURL" readonly value="{{ originRelativeUrl }}" autofocus="autofocus")
|
||||
button.js-copy-card-link-to-clipboard(class="btn" id="clipboard") {{_ 'copy-card-link-to-clipboard'}}
|
||||
.copied-tooltip {{_ 'copied'}}
|
||||
@@ -892,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)
|
||||
i.fa.fa-check
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
.palette-colors
|
||||
each colors
|
||||
unless $eq color 'white'
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
|
||||
template(name="cardDeletePopup")
|
||||
p {{_ "card-delete-pop"}}
|
||||
@@ -936,7 +1136,7 @@ template(name="cardStartVotingPopup")
|
||||
.materialCheckBox#vote-public(name="vote-public" class="{{#if votePublic}}is-checked{{/if}}")
|
||||
span {{_ 'vote-public'}}
|
||||
.check-div.flex
|
||||
i.fa.fa-hourglass-end
|
||||
i.fa.fa-clock-o
|
||||
a.js-end-date
|
||||
span
|
||||
| {{_ 'card-end'}}
|
||||
@@ -982,7 +1182,7 @@ template(name="cardStartPlanningPokerPopup")
|
||||
.materialCheckBox#poker-allow-non-members(name="poker-allow-non-members" class="{{#if pokerAllowNonBoardMembers}}is-checked{{/if}}")
|
||||
span {{_ 'allowNonBoardMembers'}}
|
||||
.check-div.flex
|
||||
i.fa.fa-hourglass-end
|
||||
i.fa.fa-clock-o
|
||||
a.js-end-date
|
||||
span
|
||||
| {{_ 'card-end'}}
|
||||
|
||||
+1505
-1215
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,10 +13,10 @@ template(name="editCardSpentTime")
|
||||
button.primary.wide.left.js-submit-time(type="submit") {{_ 'save'}}
|
||||
button.js-delete-time.negate.wide.right {{_ 'delete'}}
|
||||
|
||||
template(name="timeBadge")
|
||||
template(name="cardSpentTime")
|
||||
if canModifyCard
|
||||
a.js-edit-time.card-time(title="{{showTitle}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
|
||||
| {{showTime}}
|
||||
a.js-edit-time.card-time(title="{{_ 'time'}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
|
||||
| ⏱️ {{showTime}}
|
||||
else
|
||||
a.card-time(title="{{showTitle}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
|
||||
| {{showTime}}
|
||||
a.card-time(title="{{_ 'time'}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
|
||||
| ⏱️ {{showTime}}
|
||||
@@ -1,85 +1,91 @@
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import Cards from '/models/cards';
|
||||
import { getCurrentCardIdFromContext } from '/client/lib/currentCard';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'editCardSpentTime';
|
||||
function getCardId() {
|
||||
return getCurrentCardIdFromContext();
|
||||
}
|
||||
|
||||
Template.editCardSpentTimePopup.onCreated(function () {
|
||||
this.error = new ReactiveVar('');
|
||||
this.card = Cards.findOne(getCardId());
|
||||
});
|
||||
|
||||
Template.editCardSpentTimePopup.helpers({
|
||||
error() {
|
||||
return Template.instance().error;
|
||||
},
|
||||
onCreated() {
|
||||
this.error = new ReactiveVar('');
|
||||
this.card = this.data();
|
||||
card() {
|
||||
return Cards.findOne(getCardId());
|
||||
},
|
||||
toggleOvertime() {
|
||||
this.card.setIsOvertime(!this.card.getIsOvertime());
|
||||
getIsOvertime() {
|
||||
const card = Cards.findOne(getCardId());
|
||||
return card?.getIsOvertime ? card.getIsOvertime() : false;
|
||||
},
|
||||
});
|
||||
|
||||
Template.editCardSpentTimePopup.events({
|
||||
//TODO : need checking this portion
|
||||
'submit .edit-time'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const card = Cards.findOne(getCardId());
|
||||
if (!card) return;
|
||||
|
||||
const spentTime = parseFloat(evt.target.time.value);
|
||||
let isOvertime = false;
|
||||
if ($('#overtime').attr('class').indexOf('is-checked') >= 0) {
|
||||
isOvertime = true;
|
||||
}
|
||||
if (spentTime >= 0) {
|
||||
card.setSpentTime(spentTime);
|
||||
card.setIsOvertime(isOvertime);
|
||||
Popup.back();
|
||||
} else {
|
||||
tpl.error.set('invalid-time');
|
||||
evt.target.time.focus();
|
||||
}
|
||||
},
|
||||
'click .js-delete-time'(evt) {
|
||||
evt.preventDefault();
|
||||
const card = Cards.findOne(getCardId());
|
||||
if (!card) return;
|
||||
card.setSpentTime(null);
|
||||
card.setIsOvertime(false);
|
||||
Popup.back();
|
||||
},
|
||||
'click a.js-toggle-overtime'(evt) {
|
||||
const card = Cards.findOne(getCardId());
|
||||
if (!card) return;
|
||||
card.setIsOvertime(!card.getIsOvertime());
|
||||
$('#overtime .materialCheckBox').toggleClass('is-checked');
|
||||
$('#overtime').toggleClass('is-checked');
|
||||
},
|
||||
storeTime(spentTime, isOvertime) {
|
||||
this.card.setSpentTime(spentTime);
|
||||
this.card.setIsOvertime(isOvertime);
|
||||
},
|
||||
deleteTime() {
|
||||
this.card.setSpentTime(null);
|
||||
this.card.setIsOvertime(false);
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
//TODO : need checking this portion
|
||||
'submit .edit-time'(evt) {
|
||||
evt.preventDefault();
|
||||
});
|
||||
|
||||
const spentTime = parseFloat(evt.target.time.value);
|
||||
//const isOvertime = this.card.getIsOvertime();
|
||||
let isOvertime = false;
|
||||
if ($('#overtime').attr('class').indexOf('is-checked') >= 0) {
|
||||
isOvertime = true;
|
||||
}
|
||||
if (spentTime >= 0) {
|
||||
this.storeTime(spentTime, isOvertime);
|
||||
Popup.back();
|
||||
} else {
|
||||
this.error.set('invalid-time');
|
||||
evt.target.time.focus();
|
||||
}
|
||||
},
|
||||
'click .js-delete-time'(evt) {
|
||||
evt.preventDefault();
|
||||
this.deleteTime();
|
||||
Popup.back();
|
||||
},
|
||||
'click a.js-toggle-overtime': this.toggleOvertime,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('editCardSpentTimePopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'timeBadge';
|
||||
},
|
||||
onCreated() {
|
||||
const self = this;
|
||||
self.time = ReactiveVar();
|
||||
},
|
||||
Template.cardSpentTime.helpers({
|
||||
showTitle() {
|
||||
if (this.data().getIsOvertime()) {
|
||||
const card = Cards.findOne(this._id) || this;
|
||||
if (card.getIsOvertime && card.getIsOvertime()) {
|
||||
return `${TAPi18n.__(
|
||||
'overtime',
|
||||
)} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
|
||||
} else {
|
||||
)} ${card.getSpentTime()} ${TAPi18n.__('hours')}`;
|
||||
} else if (card.getSpentTime) {
|
||||
return `${TAPi18n.__(
|
||||
'card-spent',
|
||||
)} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
|
||||
)} ${card.getSpentTime()} ${TAPi18n.__('hours')}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
showTime() {
|
||||
return this.data().getSpentTime();
|
||||
const card = Cards.findOne(this._id) || this;
|
||||
return card.getSpentTime ? card.getSpentTime() : '';
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-edit-time': Popup.open('editCardSpentTime'),
|
||||
},
|
||||
];
|
||||
getIsOvertime() {
|
||||
const card = Cards.findOne(this._id) || this;
|
||||
return card.getIsOvertime ? card.getIsOvertime() : false;
|
||||
},
|
||||
}).register('cardSpentTime');
|
||||
});
|
||||
|
||||
Template.cardSpentTime.events({
|
||||
'click .js-edit-time': Popup.open('editCardSpentTime'),
|
||||
});
|
||||
@@ -37,10 +37,12 @@ textarea.js-edit-checklist-item {
|
||||
.checklist-progress-bar-container .checklist-progress-bar {
|
||||
width: 80%;
|
||||
height: 10px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
|
||||
color: #fff !important;
|
||||
background-color: #2196f3 !important;
|
||||
color: #fff;
|
||||
background-color: #666;
|
||||
padding: 0.01em 16px;
|
||||
border-radius: 16px;
|
||||
height: 100%;
|
||||
|
||||
@@ -4,7 +4,8 @@ template(name="checklists")
|
||||
i.fa.fa-check
|
||||
| {{_ 'checklists'}}
|
||||
if canModifyCard
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId position="top")
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId
|
||||
position="top")
|
||||
+addChecklistItemForm
|
||||
else
|
||||
a.add-checklist-top.js-open-inlined-form(title="{{_ 'add-checklist'}}")
|
||||
@@ -62,6 +63,10 @@ template(name="checklistDeletePopup")
|
||||
p {{_ 'confirm-checklist-delete-popup'}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="checklistItemDeletePopup")
|
||||
p {{_ 'confirm-checklist-item-delete-popup'}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="addChecklistItemForm")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
@@ -91,7 +96,7 @@ template(name="editChecklistItemForm")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
|
||||
span(title=createdAt) {{ moment createdAt }}
|
||||
span(title=createdAt) {{ displayDate createdAt }}
|
||||
if canModifyCard
|
||||
a.js-delete-checklist-item {{_ "delete"}}...
|
||||
a.js-convert-checklist-item-to-card
|
||||
|
||||
@@ -2,10 +2,15 @@ import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import Cards from '/models/cards';
|
||||
import Boards from '/models/boards';
|
||||
import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard';
|
||||
import ChecklistItems from '/models/checklistItems';
|
||||
import Checklists from '/models/checklists';
|
||||
import { BoardSwimlaneListCardDialog } from '/client/lib/dialogWithBoardSwimlaneListCard';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
import autosize from 'autosize';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
const { calculateIndexData, capitalize } = Utils;
|
||||
// SubsManager removed for Meteor 3 migration
|
||||
const { calculateIndexData } = Utils;
|
||||
|
||||
function initSorting(items) {
|
||||
items.sortable({
|
||||
@@ -45,55 +50,66 @@ function initSorting(items) {
|
||||
});
|
||||
}
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
const self = this;
|
||||
self.itemsDom = this.$('.js-checklist-items');
|
||||
initSorting(self.itemsDom);
|
||||
self.itemsDom.mousedown(function (evt) {
|
||||
evt.stopPropagation();
|
||||
});
|
||||
Template.checklistDetail.onRendered(function () {
|
||||
const tpl = this;
|
||||
tpl.itemsDom = this.$('.js-checklist-items');
|
||||
initSorting(tpl.itemsDom);
|
||||
tpl.itemsDom.mousedown(function (evt) {
|
||||
evt.stopPropagation();
|
||||
});
|
||||
|
||||
function userIsMember() {
|
||||
return ReactiveCache.getCurrentUser()?.isBoardMember();
|
||||
}
|
||||
function userIsMember() {
|
||||
return ReactiveCache.getCurrentUser()?.isBoardMember();
|
||||
}
|
||||
|
||||
// Disable sorting if the current user is not a board member
|
||||
self.autorun(() => {
|
||||
const $itemsDom = $(self.itemsDom);
|
||||
if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
|
||||
$(self.itemsDom).sortable('option', 'disabled', !userIsMember());
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$(self.itemsDom).sortable({
|
||||
handle: 'span.fa.checklistitem-handle',
|
||||
});
|
||||
}
|
||||
// Disable sorting if the current user is not a board member
|
||||
tpl.autorun(() => {
|
||||
const $itemsDom = $(tpl.itemsDom);
|
||||
if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
|
||||
$(tpl.itemsDom).sortable('option', 'disabled', !userIsMember());
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$(tpl.itemsDom).sortable({
|
||||
handle: 'span.fa.checklistitem-handle',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.checklistDetail.helpers({
|
||||
/** returns the finished percent of the checklist */
|
||||
finishedPercent() {
|
||||
const ret = this.data().checklist.finishedPercent();
|
||||
const ret = this.checklist.finishedPercent();
|
||||
return ret;
|
||||
},
|
||||
}).register('checklistDetail');
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
addChecklist(event) {
|
||||
Template.checklists.helpers({
|
||||
checklists() {
|
||||
const card = ReactiveCache.getCard(this.cardId);
|
||||
if (!card || typeof card.checklists !== 'function') {
|
||||
return [];
|
||||
}
|
||||
const ret = card.checklists();
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
|
||||
Template.checklists.events({
|
||||
'click .js-open-checklist-details-menu': Popup.open('checklistActions'),
|
||||
'submit .js-add-checklist'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-add-checklist-item');
|
||||
const textarea = tpl.find('textarea.js-add-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
let cardId = this.currentData().cardId;
|
||||
let cardId = Template.currentData().cardId;
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
//if (card.isLinked()) cardId = card.linkedId;
|
||||
if (card.isLinkedCard()) {
|
||||
cardId = card.linkedId;
|
||||
}
|
||||
|
||||
let sortIndex;
|
||||
let checklistItemIndex;
|
||||
if (this.currentData().position === 'top') {
|
||||
if (Template.currentData().position === 'top') {
|
||||
sortIndex = Utils.calculateIndexData(null, card.firstChecklist()).base;
|
||||
checklistItemIndex = 0;
|
||||
} else {
|
||||
@@ -107,35 +123,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,33 +197,33 @@ BlazeComponent.extendComponent({
|
||||
textarea.value = '';
|
||||
textarea.focus();
|
||||
},
|
||||
|
||||
deleteItem() {
|
||||
const checklist = this.currentData().checklist;
|
||||
const item = this.currentData().item;
|
||||
if (checklist && item && item._id) {
|
||||
ChecklistItems.remove(item._id);
|
||||
'submit .js-edit-checklist-item'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = tpl.find('textarea.js-edit-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
const formData = Blaze.getData(event.currentTarget) || Blaze.getData(event.target);
|
||||
const item = formData?.item;
|
||||
if (item) {
|
||||
item.setTitle(title);
|
||||
}
|
||||
},
|
||||
|
||||
editChecklist(event) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-edit-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
const checklist = this.currentData().checklist;
|
||||
checklist.setTitle(title);
|
||||
'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
|
||||
'click .js-delete-checklist-item': Popup.afterConfirm('checklistItemDelete', function () {
|
||||
Popup.back();
|
||||
const item = this?.item || this;
|
||||
if (item && item._id) {
|
||||
ChecklistItems.remove(item._id);
|
||||
}
|
||||
}),
|
||||
// add and delete checklist / checklist-item
|
||||
'click .js-open-inlined-form'(event, tpl) {
|
||||
tpl.$('.js-close-inlined-form').click();
|
||||
},
|
||||
|
||||
editChecklistItem(event) {
|
||||
'click #toggleHideFinishedChecklist'(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const textarea = this.find('textarea.js-edit-checklist-item');
|
||||
const title = textarea.value.trim();
|
||||
const item = this.currentData().item;
|
||||
item.setTitle(title);
|
||||
Template.currentData().card.toggleHideFinishedChecklist();
|
||||
},
|
||||
|
||||
pressKey(event) {
|
||||
'keydown textarea.js-add-checklist-item'(event) {
|
||||
//If user press enter key inside a form, submit it
|
||||
//Unless the user is also holding down the 'shift' key
|
||||
if (event.keyCode === 13 && !event.shiftKey) {
|
||||
@@ -191,201 +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': this.deleteItem,
|
||||
'focus .js-add-checklist-item': this.focusChecklistItem,
|
||||
// add and delete checklist / checklist-item
|
||||
'click .js-open-inlined-form': this.closeAllInlinedForms,
|
||||
'click #toggleHideFinishedChecklist'(event) {
|
||||
event.preventDefault();
|
||||
this.data().card.toggleHideFinishedChecklist();
|
||||
},
|
||||
keydown: this.pressKey,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('checklists');
|
||||
Template.addChecklistItemForm.events({
|
||||
'click a.fa.fa-copy'(event, tpl) {
|
||||
const $editor = tpl.$('textarea');
|
||||
const promise = Utils.copyTextToClipboard($editor[0].value);
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
subManager.subscribe('board', Session.get('currentBoard'), false);
|
||||
this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
|
||||
},
|
||||
|
||||
boards() {
|
||||
const ret = ReactiveCache.getBoards(
|
||||
{
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
_id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() },
|
||||
},
|
||||
{
|
||||
sort: { sort: 1 /* boards default sorting */ },
|
||||
},
|
||||
);
|
||||
return ret;
|
||||
},
|
||||
|
||||
swimlanes() {
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
return board.swimlanes();
|
||||
},
|
||||
|
||||
aBoardLists() {
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
return board.lists();
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'change .js-select-boards'(event) {
|
||||
this.selectedBoardId.set($(event.currentTarget).val());
|
||||
subManager.subscribe('board', this.selectedBoardId.get(), false);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('boardsSwimlanesAndLists');
|
||||
|
||||
Template.checklists.helpers({
|
||||
checklists() {
|
||||
const card = ReactiveCache.getCard(this.cardId);
|
||||
const ret = card.checklists();
|
||||
return ret;
|
||||
const $tooltip = tpl.$('.copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
autosize(this.$('textarea.js-add-checklist-item'));
|
||||
Template.checklistActionsPopup.events({
|
||||
'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () {
|
||||
Popup.back(2);
|
||||
const checklist = this.checklist;
|
||||
if (checklist && checklist._id) {
|
||||
Checklists.remove(checklist._id);
|
||||
}
|
||||
}),
|
||||
'click .js-move-checklist': Popup.open('moveChecklist'),
|
||||
'click .js-copy-checklist': Popup.open('copyChecklist'),
|
||||
'click .js-hide-checked-checklist-items'(event) {
|
||||
event.preventDefault();
|
||||
Template.currentData().checklist.toggleHideCheckedChecklistItems();
|
||||
Popup.back();
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click a.fa.fa-copy'(event) {
|
||||
const $editor = this.$('textarea');
|
||||
const promise = Utils.copyTextToClipboard($editor[0].value);
|
||||
|
||||
const $tooltip = this.$('.copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
}
|
||||
];
|
||||
}
|
||||
}).register('addChecklistItemForm');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () {
|
||||
Popup.back(2);
|
||||
const checklist = this.checklist;
|
||||
if (checklist && checklist._id) {
|
||||
Checklists.remove(checklist._id);
|
||||
}
|
||||
}),
|
||||
'click .js-move-checklist': Popup.open('moveChecklist'),
|
||||
'click .js-copy-checklist': Popup.open('copyChecklist'),
|
||||
'click .js-hide-checked-checklist-items'(event) {
|
||||
event.preventDefault();
|
||||
this.data().checklist.toggleHideCheckedChecklistItems();
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-hide-all-checklist-items'(event) {
|
||||
event.preventDefault();
|
||||
this.data().checklist.toggleHideAllChecklistItems();
|
||||
Popup.back();
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}).register('checklistActionsPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
autosize(this.$('textarea.js-edit-checklist-item'));
|
||||
'click .js-hide-all-checklist-items'(event) {
|
||||
event.preventDefault();
|
||||
Template.currentData().checklist.toggleHideAllChecklistItems();
|
||||
Popup.back();
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click a.fa.fa-copy'(event) {
|
||||
const $editor = this.$('textarea');
|
||||
const promise = Utils.copyTextToClipboard($editor[0].value);
|
||||
});
|
||||
|
||||
const $tooltip = this.$('.copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
}
|
||||
];
|
||||
}
|
||||
}).register('editChecklistItemForm');
|
||||
Template.editChecklistItemForm.onRendered(function () {
|
||||
autosize(this.$('textarea.js-edit-checklist-item'));
|
||||
});
|
||||
|
||||
Template.editChecklistItemForm.events({
|
||||
'click a.fa.fa-copy'(event, tpl) {
|
||||
const $editor = tpl.$('textarea');
|
||||
const promise = Utils.copyTextToClipboard($editor[0].value);
|
||||
|
||||
const $tooltip = tpl.$('.copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
});
|
||||
|
||||
Template.checklistItemDetail.helpers({
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
toggleItem() {
|
||||
const checklist = this.currentData().checklist;
|
||||
const item = this.currentData().item;
|
||||
Template.checklistItemDetail.events({
|
||||
'click .js-checklist-item .check-box-container'() {
|
||||
const checklist = Template.currentData().checklist;
|
||||
const item = Template.currentData().item;
|
||||
if (checklist && item && item._id) {
|
||||
item.toggleItem();
|
||||
}
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-checklist-item .check-box-container': this.toggleItem,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to find the dialog instance from a parent popup template.
|
||||
* copyAndMoveChecklist is included inside moveChecklistPopup / copyChecklistPopup,
|
||||
* so we traverse up the view hierarchy to find the parent template's dialog.
|
||||
*/
|
||||
function getParentDialog(tpl) {
|
||||
let view = tpl.view.parentView;
|
||||
while (view) {
|
||||
if (view.templateInstance && view.templateInstance() && view.templateInstance().dialog) {
|
||||
return view.templateInstance().dialog;
|
||||
}
|
||||
view = view.parentView;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Shared helpers for copyAndMoveChecklist sub-template */
|
||||
Template.copyAndMoveChecklist.helpers({
|
||||
boards() {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.boards() : [];
|
||||
},
|
||||
}).register('checklistItemDetail');
|
||||
swimlanes() {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.swimlanes() : [];
|
||||
},
|
||||
lists() {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.lists() : [];
|
||||
},
|
||||
cards() {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.cards() : [];
|
||||
},
|
||||
isDialogOptionBoardId(boardId) {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.isDialogOptionBoardId(boardId) : false;
|
||||
},
|
||||
isDialogOptionSwimlaneId(swimlaneId) {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.isDialogOptionSwimlaneId(swimlaneId) : false;
|
||||
},
|
||||
isDialogOptionListId(listId) {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.isDialogOptionListId(listId) : false;
|
||||
},
|
||||
isDialogOptionCardId(cardId) {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.isDialogOptionCardId(cardId) : false;
|
||||
},
|
||||
isTitleDefault(title) {
|
||||
const dialog = getParentDialog(Template.instance());
|
||||
return dialog ? dialog.isTitleDefault(title) : title;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper: register standard card dialog events on a checklist popup template.
|
||||
* Events bubble up from the copyAndMoveChecklist sub-template to the parent popup.
|
||||
*/
|
||||
function registerChecklistDialogEvents(templateName) {
|
||||
Template[templateName].events({
|
||||
async 'click .js-done'(event, tpl) {
|
||||
const dialog = tpl.dialog;
|
||||
const boardSelect = tpl.$('.js-select-boards')[0];
|
||||
const boardId = boardSelect.options[boardSelect.selectedIndex].value;
|
||||
|
||||
const listSelect = tpl.$('.js-select-lists')[0];
|
||||
const listId = listSelect.options[listSelect.selectedIndex].value;
|
||||
|
||||
const swimlaneSelect = tpl.$('.js-select-swimlanes')[0];
|
||||
const swimlaneId = swimlaneSelect.options[swimlaneSelect.selectedIndex].value;
|
||||
|
||||
const cardSelect = tpl.$('.js-select-cards')[0];
|
||||
const cardId = cardSelect.options.length > 0
|
||||
? cardSelect.options[cardSelect.selectedIndex].value
|
||||
: null;
|
||||
|
||||
const options = { boardId, swimlaneId, listId, cardId };
|
||||
try {
|
||||
await dialog.setDone(cardId, options);
|
||||
} catch (e) {
|
||||
console.error('Error in card dialog operation:', e);
|
||||
}
|
||||
Popup.back(2);
|
||||
},
|
||||
'change .js-select-boards'(event, tpl) {
|
||||
tpl.dialog.getBoardData($(event.currentTarget).val());
|
||||
},
|
||||
'change .js-select-swimlanes'(event, tpl) {
|
||||
tpl.dialog.selectedSwimlaneId.set($(event.currentTarget).val());
|
||||
tpl.dialog.setFirstListId();
|
||||
},
|
||||
'change .js-select-lists'(event, tpl) {
|
||||
tpl.dialog.selectedListId.set($(event.currentTarget).val());
|
||||
tpl.dialog.selectedCardId.set('');
|
||||
},
|
||||
'change .js-select-cards'(event, tpl) {
|
||||
tpl.dialog.selectedCardId.set($(event.currentTarget).val());
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Move Checklist Dialog */
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
|
||||
this.data().checklist.move(cardId);
|
||||
}
|
||||
}).register('moveChecklistPopup');
|
||||
Template.moveChecklistPopup.onCreated(function () {
|
||||
this.dialog = new BoardSwimlaneListCardDialog(this, {
|
||||
getDialogOptions() {
|
||||
return ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
|
||||
},
|
||||
async setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
|
||||
await Template.currentData().checklist.move(cardId);
|
||||
},
|
||||
});
|
||||
});
|
||||
registerChecklistDialogEvents('moveChecklistPopup');
|
||||
|
||||
/** Copy Checklist Dialog */
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
|
||||
this.data().checklist.copy(cardId);
|
||||
}
|
||||
}).register('copyChecklistPopup');
|
||||
Template.copyChecklistPopup.onCreated(function () {
|
||||
this.dialog = new BoardSwimlaneListCardDialog(this, {
|
||||
getDialogOptions() {
|
||||
return ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
|
||||
},
|
||||
async setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
|
||||
await Template.currentData().checklist.copy(cardId);
|
||||
},
|
||||
});
|
||||
});
|
||||
registerChecklistDialogEvents('copyChecklistPopup');
|
||||
@@ -0,0 +1,6 @@
|
||||
template(name='inlinedCardDescription')
|
||||
if isOpen.get
|
||||
form.inlined-form.js-inlined-form(id=id class=classNames)
|
||||
+Template.contentBlock
|
||||
else
|
||||
+Template.elseBlock
|
||||
@@ -223,9 +223,13 @@
|
||||
.card-label-edit-button:hover {
|
||||
background: #dbdbdb;
|
||||
}
|
||||
ul.edit-labels-pop-over span.fa.label-handle {
|
||||
ul.edit-labels-pop-over span.label-handle {
|
||||
padding-right: 10px;
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
ul.edit-labels-pop-over span.fa.label-handle + .card-label {
|
||||
ul.edit-labels-pop-over span.label-handle + .card-label {
|
||||
max-width: 180px;
|
||||
}
|
||||
@@ -1,119 +1,117 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { LABEL_COLORS } from '/models/metadata/colors';
|
||||
import { EscapeActions } from '/client/lib/escapeActions';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
let labelColors;
|
||||
Meteor.startup(() => {
|
||||
labelColors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
|
||||
labelColors = LABEL_COLORS;
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentColor = new ReactiveVar(this.data().color);
|
||||
},
|
||||
const getFallbackLabelColor = () => {
|
||||
if (Array.isArray(labelColors) && labelColors.length > 0) {
|
||||
return labelColors[0];
|
||||
}
|
||||
return 'green';
|
||||
};
|
||||
|
||||
Template.formLabel.onCreated(function () {
|
||||
const initialColor = this.data?.color || getFallbackLabelColor();
|
||||
this.currentColor = new ReactiveVar(initialColor);
|
||||
});
|
||||
|
||||
Template.formLabel.helpers({
|
||||
labels() {
|
||||
return labelColors.map(color => ({ color, name: '' }));
|
||||
const colors = Array.isArray(labelColors) ? labelColors : [getFallbackLabelColor()];
|
||||
return colors.map(color => ({ color, name: '' }));
|
||||
},
|
||||
|
||||
isSelected(color) {
|
||||
return this.currentColor.get() === color;
|
||||
return Template.instance().currentColor.get() === color;
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-palette-color'() {
|
||||
this.currentColor.set(this.currentData().color);
|
||||
},
|
||||
},
|
||||
];
|
||||
Template.formLabel.events({
|
||||
'click .js-palette-color'(event, tpl) {
|
||||
const paletteData = Blaze.getData(event.currentTarget);
|
||||
const selectedColor = paletteData?.color || Template.currentData()?.color || getFallbackLabelColor();
|
||||
tpl.currentColor.set(selectedColor);
|
||||
},
|
||||
}).register('formLabel');
|
||||
});
|
||||
|
||||
Template.createLabelPopup.helpers({
|
||||
// This is the default color for a new label. We search the first color that
|
||||
// is not already used in the board (although it's not a problem if two
|
||||
// labels have the same color).
|
||||
defaultColor() {
|
||||
const labels = Utils.getCurrentBoard().labels;
|
||||
const usedColors = _.pluck(labels, 'color');
|
||||
const availableColors = _.difference(labelColors, usedColors);
|
||||
return availableColors.length > 1 ? availableColors[0] : labelColors[0];
|
||||
const board = Utils.getCurrentBoard();
|
||||
const colors = Array.isArray(labelColors) ? labelColors : [getFallbackLabelColor()];
|
||||
const labels = Array.isArray(board?.labels) ? board.labels : [];
|
||||
const usedColors = labels.map(l => l.color);
|
||||
const availableColors = colors.filter(c => !usedColors.includes(c));
|
||||
return availableColors.length > 0 ? availableColors[0] : colors[0];
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
const itemsSelector = 'li.js-card-label-item:not(.placeholder)';
|
||||
const $labels = this.$('.edit-labels-pop-over');
|
||||
Template.cardLabelsPopup.onRendered(function () {
|
||||
const tpl = this;
|
||||
const itemsSelector = 'li.js-card-label-item:not(.placeholder)';
|
||||
const $labels = tpl.$('.edit-labels-pop-over');
|
||||
|
||||
$labels.sortable({
|
||||
connectWith: '.edit-labels-pop-over',
|
||||
tolerance: 'pointer',
|
||||
appendTo: '.edit-labels-pop-over',
|
||||
helper(element, currentItem) {
|
||||
let ret = currentItem.clone();
|
||||
if (currentItem.closest('.popup-container-depth-0').length == 0)
|
||||
{ // only set css transform at every sub-popup, not at the main popup
|
||||
const content = currentItem.closest('.content')[0]
|
||||
const offsetLeft = content.offsetLeft;
|
||||
const offsetTop = $('.pop-over > .header').height() * -1;
|
||||
ret.css("transform", `translate(${offsetLeft}px, ${offsetTop}px)`);
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
distance: 7,
|
||||
items: itemsSelector,
|
||||
placeholder: 'card-label-wrapper placeholder',
|
||||
start(evt, ui) {
|
||||
ui.helper.css('z-index', 1000);
|
||||
ui.placeholder.height(ui.helper.height());
|
||||
EscapeActions.clickExecute(evt.target, 'inlinedForm');
|
||||
},
|
||||
stop(evt, ui) {
|
||||
const newLabelOrderOnlyIds = ui.item.parent().children().toArray().map(_element => Blaze.getData(_element)._id)
|
||||
const card = Blaze.getData(this);
|
||||
card.board().setNewLabelOrder(newLabelOrderOnlyIds);
|
||||
},
|
||||
});
|
||||
|
||||
// Disable drag-dropping if the current user is not a board member or is comment only
|
||||
this.autorun(() => {
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$labels.sortable({
|
||||
handle: '.label-handle',
|
||||
});
|
||||
$labels.sortable({
|
||||
connectWith: '.edit-labels-pop-over',
|
||||
tolerance: 'pointer',
|
||||
appendTo: '.edit-labels-pop-over',
|
||||
helper(element, currentItem) {
|
||||
let ret = currentItem.clone();
|
||||
if (currentItem.closest('.popup-container-depth-0').length == 0)
|
||||
{ // only set css transform at every sub-popup, not at the main popup
|
||||
const content = currentItem.closest('.content')[0]
|
||||
const offsetLeft = content.offsetLeft;
|
||||
const offsetTop = $('.pop-over > .header').height() * -1;
|
||||
ret.css("transform", `translate(${offsetLeft}px, ${offsetTop}px)`);
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
distance: 7,
|
||||
items: itemsSelector,
|
||||
placeholder: 'card-label-wrapper placeholder',
|
||||
start(evt, ui) {
|
||||
ui.helper.css('z-index', 1000);
|
||||
ui.placeholder.height(ui.helper.height());
|
||||
EscapeActions.clickExecute(evt.target, 'inlinedForm');
|
||||
},
|
||||
stop(evt, ui) {
|
||||
const newLabelOrderOnlyIds = ui.item.parent().children().toArray().map(_element => Blaze.getData(_element)._id)
|
||||
const card = Blaze.getData(this);
|
||||
card.board().setNewLabelOrder(newLabelOrderOnlyIds);
|
||||
},
|
||||
});
|
||||
|
||||
// Disable drag-dropping if the current user is not a board member or is comment only
|
||||
tpl.autorun(() => {
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$labels.sortable({
|
||||
handle: '.label-handle',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.cardLabelsPopup.helpers({
|
||||
isLabelSelected(cardId) {
|
||||
return (ReactiveCache.getCard(cardId).labelIds || []).includes(this._id);
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-select-label'(event) {
|
||||
const card = this.data();
|
||||
const labelId = this.currentData()._id;
|
||||
card.toggleLabel(labelId);
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-edit-label': Popup.open('editLabel'),
|
||||
'click .js-add-label': Popup.open('createLabel'),
|
||||
}
|
||||
];
|
||||
}
|
||||
}).register('cardLabelsPopup');
|
||||
});
|
||||
|
||||
Template.cardLabelsPopup.events({
|
||||
});
|
||||
|
||||
Template.formLabel.events({
|
||||
'click .js-palette-color'(event) {
|
||||
const $this = $(event.currentTarget);
|
||||
|
||||
// hide selected ll colors
|
||||
$('.js-palette-select').addClass('hide');
|
||||
|
||||
// show select color
|
||||
$this.find('.js-palette-select').removeClass('hide');
|
||||
'click .js-select-label'(event) {
|
||||
const card = Template.currentData();
|
||||
const labelId = this._id;
|
||||
card.toggleLabel(labelId);
|
||||
event.preventDefault();
|
||||
},
|
||||
'click .js-edit-label': Popup.open('editLabel'),
|
||||
'click .js-add-label': Popup.open('createLabel'),
|
||||
});
|
||||
|
||||
Template.createLabelPopup.events({
|
||||
@@ -121,11 +119,19 @@ Template.createLabelPopup.events({
|
||||
'submit .create-label'(event, templateInstance) {
|
||||
event.preventDefault();
|
||||
const board = Utils.getCurrentBoard();
|
||||
if (!board) {
|
||||
return;
|
||||
}
|
||||
const name = templateInstance
|
||||
.$('#labelName')
|
||||
.val()
|
||||
.trim();
|
||||
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
|
||||
const selectedColorIcon = templateInstance.find('.js-palette-color .fa-check');
|
||||
const selectedPaletteNode = selectedColorIcon?.closest
|
||||
? selectedColorIcon.closest('.js-palette-color')
|
||||
: null;
|
||||
const selectedColorData = selectedPaletteNode && Blaze.getData(selectedPaletteNode);
|
||||
const color = selectedColorData?.color || getFallbackLabelColor();
|
||||
board.addLabel(name, color);
|
||||
Popup.back();
|
||||
},
|
||||
@@ -140,18 +146,20 @@ Template.editLabelPopup.events({
|
||||
'submit .edit-label'(event, templateInstance) {
|
||||
event.preventDefault();
|
||||
const board = Utils.getCurrentBoard();
|
||||
if (!board) {
|
||||
return;
|
||||
}
|
||||
const name = templateInstance
|
||||
.$('#labelName')
|
||||
.val()
|
||||
.trim();
|
||||
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
|
||||
const selectedColorIcon = templateInstance.find('.js-palette-color .fa-check');
|
||||
const selectedPaletteNode = selectedColorIcon?.closest
|
||||
? selectedColorIcon.closest('.js-palette-color')
|
||||
: null;
|
||||
const selectedColorData = selectedPaletteNode && Blaze.getData(selectedPaletteNode);
|
||||
const color = selectedColorData?.color || getFallbackLabelColor();
|
||||
board.editLabel(this._id, name, color);
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
Template.cardLabelsPopup.helpers({
|
||||
isLabelSelected(cardId) {
|
||||
return _.contains(ReactiveCache.getCard(cardId).labelIds, this._id);
|
||||
},
|
||||
});
|
||||
@@ -45,9 +45,10 @@
|
||||
}
|
||||
.minicard-details-menu-with-handle {
|
||||
float: right;
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
padding-right: 4vw;
|
||||
padding-left: 0.7vw;
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
padding: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.minicard-details-menu {
|
||||
float: right;
|
||||
@@ -97,10 +98,11 @@
|
||||
}
|
||||
.minicard .minicard-labels {
|
||||
float: none;
|
||||
margin-right: 6vw;
|
||||
}
|
||||
.minicard .minicard-labels .minicard-label {
|
||||
width: 1.5vw;
|
||||
height: 1.5vw;
|
||||
width: clamp(12px, 1.5vw, 16px);
|
||||
height: clamp(12px, 1.5vw, 16px);
|
||||
border-radius: 0.3vw;
|
||||
margin-right: 0.4vw;
|
||||
margin-bottom: 0.4vh;
|
||||
@@ -111,6 +113,7 @@
|
||||
}
|
||||
.minicard .minicard-custom-fields {
|
||||
display: block;
|
||||
margin-right: 6vw;
|
||||
}
|
||||
.minicard .minicard-custom-field {
|
||||
display: flex;
|
||||
@@ -130,21 +133,28 @@
|
||||
margin-right: 0.5vw;
|
||||
}
|
||||
.minicard .handle {
|
||||
width: 2.5vw;
|
||||
height: 2.5vw;
|
||||
width: clamp(20px, 2.5vw, 28px);
|
||||
height: clamp(20px, 2.5vw, 28px);
|
||||
position: absolute;
|
||||
right: 0.7vw;
|
||||
top: 0.7vh;
|
||||
right: 0vw;
|
||||
top: 4vh;
|
||||
display: none;
|
||||
z-index: 1;
|
||||
}
|
||||
@media only screen {
|
||||
.minicard .handle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.minicard .handle .fa-arrows {
|
||||
.minicard .handle .drag-handle {
|
||||
font-size: clamp(16px, 3vw, 20px);
|
||||
color: #ccc;
|
||||
display: inline-block;
|
||||
width: 1.4em;
|
||||
text-align: center;
|
||||
}
|
||||
.minicard .minicard-title {
|
||||
margin-right: 1.5vw;
|
||||
}
|
||||
.minicard .minicard-title .card-number {
|
||||
color: #b3b3b3;
|
||||
@@ -164,10 +174,143 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
margin-right: 6vw;
|
||||
clear: both;
|
||||
}
|
||||
.minicard .date {
|
||||
margin-right: 0.4vw;
|
||||
}
|
||||
|
||||
/* Unicode icons for minicard dates - matching cardDate.css */
|
||||
.minicard .card-date.end-date time::before {
|
||||
content: "🏁"; /* Finish flag - represents end/completion */
|
||||
}
|
||||
.minicard .card-date.due-date time::before {
|
||||
content: "⏰"; /* Alarm clock - represents due/deadline */
|
||||
}
|
||||
.minicard .card-date.start-date time::before {
|
||||
content: "🚀"; /* Rocket - represents start/launch */
|
||||
}
|
||||
.minicard .card-date.received-date time::before {
|
||||
content: "📥"; /* Inbox tray - represents received/incoming */
|
||||
}
|
||||
|
||||
.minicard .card-date time::before {
|
||||
font-size: inherit;
|
||||
margin-right: 0.3em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Date type specific colors for minicards - matching cardDate.css */
|
||||
.minicard .card-date.received-date {
|
||||
background-color: #dbdbdb; /* Grey for received - same as base card-date */
|
||||
}
|
||||
|
||||
.minicard .card-date.received-date:hover,
|
||||
.minicard .card-date.received-date.is-active {
|
||||
background-color: #b3b3b3;
|
||||
}
|
||||
|
||||
.minicard .card-date.start-date {
|
||||
background-color: #90ee90; /* Light green for start */
|
||||
color: #000; /* Black text for start */
|
||||
}
|
||||
|
||||
.minicard .card-date.start-date:hover,
|
||||
.minicard .card-date.start-date.is-active {
|
||||
background-color: #7dd87d;
|
||||
}
|
||||
|
||||
.minicard .card-date.due-date {
|
||||
background-color: #ffd700; /* Yellow for due */
|
||||
color: #000; /* Black text for due */
|
||||
}
|
||||
|
||||
.minicard .card-date.due-date:hover,
|
||||
.minicard .card-date.due-date.is-active {
|
||||
background-color: #e6c200;
|
||||
}
|
||||
|
||||
.minicard .card-date.end-date {
|
||||
background-color: #ffb3b3; /* Light red for end */
|
||||
color: #000; /* Black text for end */
|
||||
}
|
||||
|
||||
.minicard .card-date.end-date:hover,
|
||||
.minicard .card-date.end-date.is-active {
|
||||
background-color: #ff9999;
|
||||
}
|
||||
|
||||
/* Date status colors for minicards - matching cardDate.css */
|
||||
.minicard .card-date.overdue {
|
||||
background-color: #ff4444 !important; /* Red for overdue */
|
||||
color: #fff !important;
|
||||
}
|
||||
.minicard .card-date.overdue:hover,
|
||||
.minicard .card-date.overdue.is-active {
|
||||
background-color: #cc3333 !important;
|
||||
}
|
||||
|
||||
.minicard .card-date.due-soon {
|
||||
background-color: #ffaa00 !important; /* Amber for due soon */
|
||||
color: #000 !important;
|
||||
}
|
||||
.minicard .card-date.due-soon:hover,
|
||||
.minicard .card-date.due-soon.is-active {
|
||||
background-color: #e69900 !important;
|
||||
}
|
||||
|
||||
.minicard .card-date.not-due {
|
||||
/* No special background - uses default date type colors */
|
||||
}
|
||||
|
||||
.minicard .card-date.current {
|
||||
background-color: #5ba639 !important; /* Green for current/active */
|
||||
color: #fff !important;
|
||||
}
|
||||
.minicard .card-date.current:hover,
|
||||
.minicard .card-date.current.is-active {
|
||||
background-color: #46802c !important;
|
||||
}
|
||||
|
||||
.minicard .card-date.completed {
|
||||
background-color: #90ee90 !important; /* Light green for completed */
|
||||
color: #000 !important;
|
||||
}
|
||||
.minicard .card-date.completed:hover,
|
||||
.minicard .card-date.completed.is-active {
|
||||
background-color: #7dd87d !important;
|
||||
}
|
||||
|
||||
.minicard .card-date.completed-early {
|
||||
background-color: #4caf50 !important; /* Green for completed early */
|
||||
color: #fff !important;
|
||||
}
|
||||
.minicard .card-date.completed-early:hover,
|
||||
.minicard .card-date.completed-early.is-active {
|
||||
background-color: #45a049 !important;
|
||||
}
|
||||
|
||||
.minicard .card-date.completed-late {
|
||||
background-color: #ff9800 !important; /* Orange for completed late */
|
||||
color: #fff !important;
|
||||
}
|
||||
.minicard .card-date.completed-late:hover,
|
||||
.minicard .card-date.completed-late.is-active {
|
||||
background-color: #f57c00 !important;
|
||||
}
|
||||
|
||||
.minicard .card-date.completed-on-time {
|
||||
background-color: #2196f3 !important; /* Blue for completed on time */
|
||||
color: #fff !important;
|
||||
}
|
||||
.minicard .card-date.completed-on-time:hover,
|
||||
.minicard .card-date.completed-on-time.is-active {
|
||||
background-color: #1976d2 !important;
|
||||
}
|
||||
|
||||
.minicard .badges {
|
||||
float: left;
|
||||
margin-top: 1vh;
|
||||
@@ -220,8 +363,8 @@
|
||||
.minicard .minicard-creator .member {
|
||||
float: right;
|
||||
border-radius: 50%;
|
||||
height: 3.5vw;
|
||||
width: 3.5vw;
|
||||
height: clamp(24px, 3.5vw, 32px);
|
||||
width: clamp(24px, 3.5vw, 32px);
|
||||
margin-bottom: 0.5vh;
|
||||
}
|
||||
.minicard .minicard-members .assignee,
|
||||
@@ -229,8 +372,8 @@
|
||||
.minicard .minicard-creator .assignee {
|
||||
float: right;
|
||||
border-radius: 50%;
|
||||
height: 3.5vw;
|
||||
width: 3.5vw;
|
||||
height: clamp(24px, 3.5vw, 32px);
|
||||
width: clamp(24px, 3.5vw, 32px);
|
||||
}
|
||||
.minicard .minicard-members + .badges,
|
||||
.minicard .minicard-assignees + .badges,
|
||||
@@ -599,7 +742,80 @@
|
||||
gap: 0.3vw;
|
||||
}
|
||||
|
||||
.minicard-list-name i.fa {
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
/* Checklist display on minicard */
|
||||
.minicard-checklist {
|
||||
width: 100%;
|
||||
margin-top: 0.5vh;
|
||||
margin-bottom: 0.5vh;
|
||||
padding: 0.3vh 0.5vw;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 0.3vw;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.3vh;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-title {
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
color: #4d4d4d;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-menu {
|
||||
font-size: 1.2em;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
padding: 0 0.3vw;
|
||||
border-radius: 0.2vw;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-menu:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item {
|
||||
font-size: 0.75em;
|
||||
color: #666;
|
||||
margin-bottom: 0.2vh;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.3vw;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
padding: 0.2vh 0;
|
||||
border-radius: 0.2vw;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item.is-checked {
|
||||
text-decoration: line-through;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item .check-box-unicode {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.8em;
|
||||
margin-top: 0.1vh;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item:hover .check-box-unicode {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item .item-title {
|
||||
flex: 1;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
@@ -3,28 +3,29 @@ template(name="minicard")
|
||||
class="{{#if isLinkedCard}}linked-card{{/if}}"
|
||||
class="{{#if isLinkedBoard}}linked-board{{/if}}"
|
||||
class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}")
|
||||
if canModifyCard
|
||||
if canMoveCard
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
a.fa.fa-navicon.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
.handle
|
||||
.fa.fa-arrows
|
||||
else
|
||||
a.fa.fa-navicon.minicard-details-menu.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
i.fa.fa-arrows
|
||||
if canModifyCard
|
||||
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
.dates
|
||||
if getReceived
|
||||
unless getStart
|
||||
unless getDue
|
||||
unless getEnd
|
||||
.date
|
||||
+minicardReceivedDate
|
||||
if getStart
|
||||
.date
|
||||
+minicardStartDate
|
||||
if getDue
|
||||
.date
|
||||
+minicardDueDate
|
||||
if getEnd
|
||||
+minicardEndDate
|
||||
if showReceived
|
||||
if getReceived
|
||||
.date
|
||||
+minicardReceivedDate
|
||||
if showStart
|
||||
if getStart
|
||||
.date
|
||||
+minicardStartDate
|
||||
if showDue
|
||||
if getDue
|
||||
.date
|
||||
+minicardDueDate
|
||||
if showEnd
|
||||
if getEnd
|
||||
+minicardEndDate
|
||||
if getSpentTime
|
||||
.date
|
||||
+cardSpentTime
|
||||
@@ -45,7 +46,7 @@ template(name="minicard")
|
||||
.upload-progress-fill(style="width: {{progress}}%")
|
||||
if $eq status 'error'
|
||||
.upload-progress-error
|
||||
i.fa.fa-exclamation-triangle
|
||||
i.fa.fa-warning
|
||||
span {{_ 'upload-failed'}}
|
||||
else if $eq status 'completed'
|
||||
.upload-progress-success
|
||||
@@ -61,26 +62,30 @@ template(name="minicard")
|
||||
| {{ parentCardName }}
|
||||
if isLinkedBoard
|
||||
a.js-linked-link
|
||||
span.linked-icon.fa.fa-folder
|
||||
span.linked-icon
|
||||
i.fa.fa-folder
|
||||
else if isLinkedCard
|
||||
a.js-linked-link
|
||||
span.linked-icon.fa.fa-id-card
|
||||
span.linked-icon
|
||||
i.fa.fa-id-card
|
||||
if getArchived
|
||||
span.linked-icon.linked-archived.fa.fa-archive
|
||||
span.linked-icon.linked-archived
|
||||
i.fa.fa-archive
|
||||
+viewer
|
||||
if currentBoard.allowsCardNumber
|
||||
if showCardNumber
|
||||
span.card-number
|
||||
| ##{getCardNumber}
|
||||
= 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
|
||||
@@ -147,45 +152,54 @@ template(name="minicard")
|
||||
if canModifyCard
|
||||
if comments.length
|
||||
.badge(title="{{_ 'card-comments-title' comments.length }}")
|
||||
span.badge-icon.fa.fa-comment-o.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.fa.fa-align-left
|
||||
span.badge-icon
|
||||
i.fa.fa-file-text-o
|
||||
if getVoteQuestion
|
||||
.badge.badge-state-image-only(title=getVoteQuestion)
|
||||
span.badge-icon.fa.fa-thumbs-up(class="{{#if voteState}}text-green{{/if}}")
|
||||
span.badge-icon(class="{{#if voteState}}text-green{{/if}}")
|
||||
i.fa.fa-thumbs-up
|
||||
span.badge-text {{ voteCountPositive }}
|
||||
span.badge-icon.fa.fa-thumbs-down(class="{{#if $eq voteState false}}text-red{{/if}}")
|
||||
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}")
|
||||
i.fa.fa-thumbs-down
|
||||
span.badge-text {{ voteCountNegative }}
|
||||
if getPokerQuestion
|
||||
.badge.badge-state-image-only(title=getPokerQuestion)
|
||||
span.badge-icon.fa.fa-check(class="{{#if pokerState}}text-green{{/if}}")
|
||||
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}")
|
||||
i.fa.fa-check-square
|
||||
if expiredPoker
|
||||
span.badge-text {{ getPokerEstimation }}
|
||||
if attachments.length
|
||||
if currentBoard.allowsBadgeAttachmentOnMinicard
|
||||
.badge
|
||||
span.badge-icon.fa.fa-paperclip
|
||||
span.badge-icon
|
||||
i.fa.fa-paperclip
|
||||
span.badge-text= attachments.length
|
||||
if checklists.length
|
||||
.badge(class="{{#if checklistFinished}}is-finished{{/if}}")
|
||||
span.badge-icon.fa.fa-check-square-o
|
||||
span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
|
||||
if allSubtasks.count
|
||||
.badge
|
||||
span.badge-icon.fa.fa-sitemap
|
||||
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
|
||||
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
|
||||
if showSubtasks
|
||||
if allSubtasks.count
|
||||
.badge
|
||||
span.badge-icon
|
||||
i.fa.fa-globe
|
||||
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
|
||||
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
|
||||
if currentBoard.allowsCardSortingByNumber
|
||||
if currentBoard.allowsCardSortingByNumberOnMinicard
|
||||
.badge
|
||||
span.badge-icon.fa.fa-sort
|
||||
span.badge-icon
|
||||
i.fa.fa-sort-numeric-asc
|
||||
span.badge-text.check-list-sort {{ sort }}
|
||||
if shouldShowChecklistAtMinicard
|
||||
each shouldShowChecklistAtMinicard
|
||||
+minicardChecklist(checklist=. card=..)
|
||||
if currentBoard.allowsDescriptionTextOnMinicard
|
||||
if getDescription
|
||||
.minicard-description
|
||||
@@ -207,55 +221,13 @@ template(name="editCardSortOrderPopup")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-sort-popup(type="submit") {{_ 'save'}}
|
||||
|
||||
template(name="minicardDetailsActionsPopup")
|
||||
ul.pop-over-list
|
||||
if canModifyCard
|
||||
li
|
||||
a.js-move-card
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
li
|
||||
a.js-copy-card
|
||||
i.fa.fa-copy
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
hr
|
||||
li
|
||||
a.js-archive
|
||||
i.fa.fa-arrow-right
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archive-card'}}
|
||||
hr
|
||||
li
|
||||
a.js-move-card-to-top
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
li
|
||||
a.js-add-labels
|
||||
i.fa.fa-tags
|
||||
| {{_ 'card-edit-labels'}}
|
||||
li
|
||||
a.js-due-date
|
||||
i.fa.fa-sign-in
|
||||
| {{_ 'editCardDueDatePopup-title'}}
|
||||
li
|
||||
a.js-set-card-color
|
||||
i.fa.fa-paint-brush
|
||||
| {{_ 'setCardColorPopup-title'}}
|
||||
li
|
||||
a.js-link
|
||||
i.fa.fa-link
|
||||
| {{_ 'link-card'}}
|
||||
li
|
||||
a.js-toggle-watch-card
|
||||
if isWatching
|
||||
i.fa.fa-eye
|
||||
| {{_ 'unwatch'}}
|
||||
else
|
||||
i.fa.fa-eye-slash
|
||||
| {{_ 'watch'}}
|
||||
template(name="minicardChecklist")
|
||||
.minicard-checklist
|
||||
.checklist-header
|
||||
.checklist-title= checklist.title
|
||||
if canModifyCard
|
||||
a.checklist-menu.js-open-checklist-menu(title="{{_ 'checklistActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
each visibleItems
|
||||
+checklistItemDetail(item = . checklist = checklist card = card)
|
||||
|
||||
+195
-167
@@ -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,125 +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;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-linked-link'() {
|
||||
if (this.data().isLinkedCard()) Utils.goCardId(this.data().linkedId);
|
||||
else if (this.data().isLinkedBoard())
|
||||
Utils.goBoardId(this.data().linkedId);
|
||||
},
|
||||
'click .js-toggle-minicard-label-text'() {
|
||||
if (window.localStorage.getItem('hiddenMinicardLabelText')) {
|
||||
window.localStorage.removeItem('hiddenMinicardLabelText'); //true
|
||||
} else {
|
||||
window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true
|
||||
}
|
||||
},
|
||||
'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
|
||||
'click .minicard-labels' : this.cardLabelsPopup,
|
||||
'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
|
||||
// Drag and drop file upload handlers
|
||||
'dragover .minicard'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
'dragenter .minicard'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const card = this.data();
|
||||
const board = card.board();
|
||||
// Only allow drag-and-drop if user can modify card and board allows attachments
|
||||
if (card.canModifyCard() && board && board.allowsAttachments) {
|
||||
// Check if the drag contains files
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
$(event.currentTarget).addClass('is-dragging-over');
|
||||
}
|
||||
}
|
||||
},
|
||||
'dragleave .minicard'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
},
|
||||
'drop .minicard'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
|
||||
const card = this.data();
|
||||
const board = card.board();
|
||||
|
||||
// Check permissions
|
||||
if (!card.canModifyCard() || !board || !board.allowsAttachments) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a file drop (not a card reorder)
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the drop contains files (not just text/HTML)
|
||||
if (!dataTransfer.types.includes('Files')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileUpload(card, files);
|
||||
}
|
||||
},
|
||||
}
|
||||
];
|
||||
},
|
||||
}).register('minicard');
|
||||
|
||||
Template.minicard.helpers({
|
||||
hiddenMinicardLabelText() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (currentUser) {
|
||||
@@ -178,15 +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);
|
||||
@@ -206,68 +141,161 @@ Template.minicard.helpers({
|
||||
// Show list name if either:
|
||||
// 1. Board-wide setting is enabled, OR
|
||||
// 2. This specific card has the setting enabled
|
||||
const currentBoard = this.currentBoard;
|
||||
const currentBoard = this.board();
|
||||
if (!currentBoard) return false;
|
||||
return currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
|
||||
},
|
||||
|
||||
shouldShowChecklistAtMinicard() {
|
||||
// Return checklists that should be shown on minicard
|
||||
const currentBoard = this.board();
|
||||
if (!currentBoard) return [];
|
||||
|
||||
const checklists = this.checklists();
|
||||
const visibleChecklists = [];
|
||||
|
||||
checklists.forEach(checklist => {
|
||||
// Show checklist if either:
|
||||
// 1. Board-wide setting is enabled, OR
|
||||
// 2. This specific checklist has the setting enabled
|
||||
if (currentBoard.allowsChecklistAtMinicard || checklist.showChecklistAtMinicard) {
|
||||
visibleChecklists.push(checklist);
|
||||
}
|
||||
});
|
||||
|
||||
return visibleChecklists;
|
||||
}
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'keydown input.js-edit-card-sort-popup'(evt) {
|
||||
// enter = save
|
||||
if (evt.keyCode === 13) {
|
||||
this.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
'click button.js-submit-edit-card-sort-popup'(event) {
|
||||
// save button pressed
|
||||
event.preventDefault();
|
||||
const sort = this.$('.js-edit-card-sort-popup')[0]
|
||||
.value
|
||||
.trim();
|
||||
if (!Number.isNaN(sort)) {
|
||||
let card = this.data();
|
||||
card.move(card.boardId, card.swimlaneId, card.listId, sort);
|
||||
Popup.back();
|
||||
}
|
||||
},
|
||||
Template.minicard.events({
|
||||
'click .js-linked-link'() {
|
||||
if (this.isLinkedCard()) Utils.goCardId(this.linkedId);
|
||||
else if (this.isLinkedBoard())
|
||||
Utils.goBoardId(this.linkedId);
|
||||
},
|
||||
'click .js-toggle-minicard-label-text'() {
|
||||
if (window.localStorage.getItem('hiddenMinicardLabelText')) {
|
||||
window.localStorage.removeItem('hiddenMinicardLabelText'); //true
|
||||
} else {
|
||||
window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true
|
||||
}
|
||||
},
|
||||
'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
|
||||
'click .minicard-labels'(event, tpl) {
|
||||
if (tpl.find('.js-card-label:hover')) {
|
||||
Popup.open("cardLabels")(event, {dataContextIfCurrentDataIsUndefined: Template.currentData()});
|
||||
}
|
||||
},
|
||||
'click .js-open-minicard-details-menu'(event, tpl) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const card = Template.currentData();
|
||||
Popup.open('cardDetailsActions').call({currentData: () => card}, event);
|
||||
},
|
||||
// Drag and drop file upload handlers
|
||||
'dragover .minicard'(event) {
|
||||
// Only prevent default for file drags to avoid interfering with sortable
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
'dragenter .minicard'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const card = this;
|
||||
const board = card.board();
|
||||
// Only allow drag-and-drop if user can modify card and board allows attachments
|
||||
if (Utils.canModifyCard() && board && board.allowsAttachments) {
|
||||
$(event.currentTarget).addClass('is-dragging-over');
|
||||
}
|
||||
]
|
||||
}
|
||||
}).register('editCardSortOrderPopup');
|
||||
}
|
||||
},
|
||||
'dragleave .minicard'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
}
|
||||
},
|
||||
'drop .minicard'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
|
||||
Template.minicardDetailsActionsPopup.events({
|
||||
'click .js-due-date': Popup.open('editCardDueDate'),
|
||||
'click .js-move-card': Popup.open('moveCard'),
|
||||
'click .js-copy-card': Popup.open('copyCard'),
|
||||
'click .js-set-card-color': Popup.open('setCardColor'),
|
||||
'click .js-add-labels': Popup.open('cardLabels'),
|
||||
'click .js-link': Popup.open('linkCard'),
|
||||
'click .js-move-card-to-top'(event) {
|
||||
event.preventDefault();
|
||||
const minOrder = this.getMinSort();
|
||||
this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
|
||||
Popup.back();
|
||||
const card = this;
|
||||
const board = card.board();
|
||||
|
||||
// Check permissions
|
||||
if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a file drop (not a card reorder)
|
||||
if (!dataTransfer.files || dataTransfer.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileUpload(card, files);
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-move-card-to-bottom'(event) {
|
||||
event.preventDefault();
|
||||
const maxOrder = this.getMaxSort();
|
||||
this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1);
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-archive': Popup.afterConfirm('cardArchive', function () {
|
||||
Popup.close();
|
||||
this.archive();
|
||||
Utils.goBoardId(this.boardId);
|
||||
}),
|
||||
'click .js-toggle-watch-card'() {
|
||||
const currentCard = this;
|
||||
const level = currentCard.findWatcher(Meteor.userId()) ? null : 'watching';
|
||||
Meteor.call('watch', 'card', currentCard._id, level, (err, ret) => {
|
||||
if (!err && ret) Popup.back();
|
||||
});
|
||||
|
||||
Template.minicardChecklist.helpers({
|
||||
visibleItems() {
|
||||
const checklist = this.checklist || this;
|
||||
const items = checklist.items();
|
||||
|
||||
return items.filter(item => {
|
||||
// Hide finished items if hideCheckedChecklistItems is true
|
||||
if (item.isFinished && checklist.hideCheckedChecklistItems) {
|
||||
return false;
|
||||
}
|
||||
// Hide all items if hideAllChecklistItems is true
|
||||
if (checklist.hideAllChecklistItems) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Template.minicardChecklist.events({
|
||||
'click .js-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();
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -11,7 +11,7 @@ template(name="resultCard")
|
||||
= getBoard.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| NULL
|
||||
| {{_ 'no-name'}}
|
||||
if getBoard.archived
|
||||
i.fa.fa-archive
|
||||
li.result-card-context.result-card-context-separator
|
||||
@@ -25,7 +25,7 @@ template(name="resultCard")
|
||||
= getSwimlane.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| NULL
|
||||
| {{_ 'no-name'}}
|
||||
if getSwimlane.archived
|
||||
i.fa.fa-archive
|
||||
li.result-card-context.result-card-context-separator
|
||||
@@ -39,6 +39,6 @@ template(name="resultCard")
|
||||
= getList.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| NULL
|
||||
| {{_ 'no-name'}}
|
||||
if getList.archived
|
||||
i.fa.fa-archive
|
||||
@@ -1,35 +1,67 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
|
||||
Template.resultCard.helpers({
|
||||
userId() {
|
||||
return Meteor.userId();
|
||||
},
|
||||
getBoard() {
|
||||
const card = Template.currentData();
|
||||
try {
|
||||
if (card && card.boardId) {
|
||||
const board = ReactiveCache.getBoard(card.boardId);
|
||||
return board || null;
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently handle errors
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getSwimlane() {
|
||||
const card = Template.currentData();
|
||||
try {
|
||||
if (card && card.swimlaneId) {
|
||||
const swimlane = ReactiveCache.getSwimlane(card.swimlaneId);
|
||||
return swimlane || null;
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently handle errors
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getList() {
|
||||
const card = Template.currentData();
|
||||
try {
|
||||
if (card && card.listId) {
|
||||
const list = ReactiveCache.getList(card.listId);
|
||||
return list || null;
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently handle errors
|
||||
}
|
||||
return null;
|
||||
},
|
||||
originRelativeUrl() {
|
||||
const card = Template.currentData();
|
||||
if (card && card.boardId && card._id) {
|
||||
return `/board/${card.boardId}?cardId=${card._id}`;
|
||||
}
|
||||
return '#';
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
clickOnMiniCard(evt) {
|
||||
evt.preventDefault();
|
||||
const this_ = this;
|
||||
const cardId = this.currentData()._id;
|
||||
const boardId = this.currentData().boardId;
|
||||
Template.resultCard.events({
|
||||
'click .js-minicard'(event) {
|
||||
event.preventDefault();
|
||||
const cardId = Template.currentData()._id;
|
||||
const boardId = Template.currentData().boardId;
|
||||
Meteor.subscribe('popupCardData', cardId, {
|
||||
onReady() {
|
||||
Session.set('popupCardId', cardId);
|
||||
Session.set('popupCardBoardId', boardId);
|
||||
this_.cardDetailsPopup(evt);
|
||||
if (!Popup.isOpen()) {
|
||||
Popup.open("cardDetails")(event);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
cardDetailsPopup(event) {
|
||||
if (!Popup.isOpen()) {
|
||||
Popup.open("cardDetails")(event);
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-minicard': this.clickOnMiniCard,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('resultCard');
|
||||
});
|
||||
@@ -15,7 +15,7 @@ textarea.js-edit-subtask-item {
|
||||
color: #8c8c8c;
|
||||
text-decoration: underline;
|
||||
word-wrap: break-word;
|
||||
float: right;
|
||||
flex-shrink: 0;
|
||||
padding-top: 6px;
|
||||
}
|
||||
.delete-text:hover,
|
||||
@@ -24,8 +24,13 @@ textarea.js-edit-subtask-item {
|
||||
.js-delete-subtask-item:hover {
|
||||
color: inherit;
|
||||
}
|
||||
.subtask-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.subtask-title .checkbox {
|
||||
float: left;
|
||||
flex-shrink: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 18px;
|
||||
@@ -34,10 +39,12 @@ textarea.js-edit-subtask-item {
|
||||
.subtask-title .title {
|
||||
font-size: 18px;
|
||||
line-height: 25px;
|
||||
flex: 1;
|
||||
order: 1;
|
||||
}
|
||||
.subtask-title .subtasks-stat {
|
||||
margin: 0 0.5em;
|
||||
float: right;
|
||||
flex-shrink: 0;
|
||||
padding-top: 6px;
|
||||
}
|
||||
.subtask-title .subtasks-stat.is-finished {
|
||||
@@ -46,6 +53,9 @@ textarea.js-edit-subtask-item {
|
||||
.subtask-title .js-delete-subtask {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
.subtask-title .js-view-subtask {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
.js-confirm-subtask-delete {
|
||||
background-color: #f7f7f7;
|
||||
position: absolute;
|
||||
@@ -87,6 +97,15 @@ textarea.js-edit-subtask-item {
|
||||
top: 0;
|
||||
bottom: -600px;
|
||||
right: 0;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
/* Fix for mobile Safari: ensure this doesn't block card interaction */
|
||||
@media screen and (max-width: 800px) {
|
||||
#card-details-overlay {
|
||||
z-index: 15;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
.subtasks {
|
||||
background: #f7f7f7;
|
||||
@@ -127,6 +146,25 @@ textarea.js-edit-subtask-item {
|
||||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
}
|
||||
/* Unicode checkbox icons styling */
|
||||
.subtasks-item .check-box-unicode {
|
||||
font-size: 1.3em;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .subtasks-item .check-box.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
body.grey-icons-enabled .subtasks-item .check-box-unicode {
|
||||
filter: grayscale(100%);
|
||||
-webkit-filter: grayscale(100%);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.subtasks-item .item-title {
|
||||
flex: 1;
|
||||
padding-left: 10px;
|
||||
@@ -147,6 +185,7 @@ textarea.js-edit-subtask-item {
|
||||
display: inline-block;
|
||||
}
|
||||
.subtask-details-menu {
|
||||
float: right;
|
||||
flex-shrink: 0;
|
||||
padding: 6px 10px 6px 10px;
|
||||
order: 2;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
template(name="subtasks")
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sitemap
|
||||
i.fa.fa-globe
|
||||
| {{_ 'subtasks'}}
|
||||
if currentUser.isBoardAdmin
|
||||
if toggleDeleteDialog.get
|
||||
@@ -40,7 +40,7 @@ template(name="addSubtaskItemForm")
|
||||
textarea.js-add-subtask-item(rows='1' autofocus dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-add-subtask-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
|
||||
|
||||
template(name="editSubtaskItemForm")
|
||||
textarea.js-edit-subtask-item(rows='1' autofocus dir="auto")
|
||||
@@ -50,8 +50,8 @@ template(name="editSubtaskItemForm")
|
||||
= subtask.title
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-subtask-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
span(title=createdAt) {{ moment createdAt }}
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
|
||||
span(title=createdAt) {{ displayDate createdAt }}
|
||||
if canModifyCard
|
||||
if currentUser.isBoardAdmin
|
||||
a.js-delete-subtask-item {{_ "delete"}}...
|
||||
@@ -74,12 +74,14 @@ template(name="subtasksItems")
|
||||
template(name='subtaskItemDetail')
|
||||
.js-subtasks-item.subtasks-item
|
||||
if canModifyCard
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/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
|
||||
.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/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
|
||||
@@ -94,6 +96,10 @@ template(name="subtaskActionsPopup")
|
||||
a.js-view-subtask(title="{{ subtask.title }}")
|
||||
i.fa.fa-eye
|
||||
| {{_ "view-it"}}
|
||||
li
|
||||
a.js-go-to-subtask-board
|
||||
i.fa.fa-folder
|
||||
| {{_ "go-to-board"}}
|
||||
if currentUser.isBoardAdmin
|
||||
a.js-delete-subtask.delete-subtask
|
||||
i.fa.fa-trash
|
||||
|
||||
@@ -1,16 +1,35 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import Cards from '/models/cards';
|
||||
import { Filter } from '/client/lib/filter';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
addSubtask(event) {
|
||||
Template.subtasks.events({
|
||||
'click .js-open-subtask-details-menu'(event) {
|
||||
// Close any existing popup first to avoid accumulating content
|
||||
if (Popup.isOpen()) {
|
||||
Popup.close();
|
||||
}
|
||||
// Now open the popup for this specific subtask
|
||||
Popup.open('subtaskActions').call(this, event);
|
||||
},
|
||||
async 'submit .js-add-subtask'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-add-subtask-item');
|
||||
const textarea = tpl.find('textarea.js-add-subtask-item');
|
||||
const title = textarea.value.trim();
|
||||
const cardId = this.currentData().cardId;
|
||||
const cardId = Template.currentData().cardId;
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
const sortIndex = -1;
|
||||
const crtBoard = ReactiveCache.getBoard(card.boardId);
|
||||
const targetBoard = crtBoard.getDefaultSubtasksBoard();
|
||||
const listId = targetBoard.getDefaultSubtasksListId();
|
||||
const targetBoard = await crtBoard.getDefaultSubtasksBoardAsync();
|
||||
if (!targetBoard) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetList = await targetBoard.getDefaultSubtasksListAsync();
|
||||
if (!targetList) {
|
||||
return;
|
||||
}
|
||||
const listId = targetList._id;
|
||||
|
||||
//Get the full swimlane data for the parent task.
|
||||
const parentSwimlane = ReactiveCache.getSwimlane({
|
||||
@@ -18,20 +37,22 @@ BlazeComponent.extendComponent({
|
||||
_id: card.swimlaneId,
|
||||
});
|
||||
//find the swimlane of the same name in the target board.
|
||||
const targetSwimlane = ReactiveCache.getSwimlane({
|
||||
boardId: targetBoard._id,
|
||||
title: parentSwimlane.title,
|
||||
});
|
||||
const targetSwimlane = parentSwimlane
|
||||
? ReactiveCache.getSwimlane({
|
||||
boardId: targetBoard._id,
|
||||
title: parentSwimlane.title,
|
||||
})
|
||||
: undefined;
|
||||
//If no swimlane with a matching title exists in the target board, fall back to the default swimlane.
|
||||
const swimlaneId =
|
||||
targetSwimlane === undefined
|
||||
? targetBoard.getDefaultSwimline()._id
|
||||
: targetSwimlane._id;
|
||||
|
||||
const nextCardNumber = targetBoard.getNextCardNumber();
|
||||
const nextCardNumber = await targetBoard.getNextCardNumber();
|
||||
|
||||
if (title) {
|
||||
const _id = Cards.insert({
|
||||
const _id = await Cards.insertAsync({
|
||||
title,
|
||||
parentId: cardId,
|
||||
members: [],
|
||||
@@ -42,7 +63,7 @@ BlazeComponent.extendComponent({
|
||||
sort: sortIndex,
|
||||
swimlaneId,
|
||||
type: 'cardType-card',
|
||||
cardNumber: nextCardNumber
|
||||
cardNumber: nextCardNumber,
|
||||
});
|
||||
|
||||
// In case the filter is active we need to add the newly inserted card in
|
||||
@@ -52,7 +73,7 @@ BlazeComponent.extendComponent({
|
||||
Filter.addException(_id);
|
||||
|
||||
setTimeout(() => {
|
||||
this.$('.add-subtask-item')
|
||||
tpl.$('.add-subtask-item')
|
||||
.last()
|
||||
.click();
|
||||
}, 100);
|
||||
@@ -60,27 +81,20 @@ BlazeComponent.extendComponent({
|
||||
textarea.value = '';
|
||||
textarea.focus();
|
||||
},
|
||||
|
||||
deleteSubtask() {
|
||||
const subtask = this.currentData().subtask;
|
||||
if (subtask && subtask._id) {
|
||||
subtask.archive();
|
||||
}
|
||||
},
|
||||
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
|
||||
editSubtask(event) {
|
||||
'submit .js-edit-subtask-title'(event, tpl) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-edit-subtask-item');
|
||||
const textarea = tpl.find('textarea.js-edit-subtask-item');
|
||||
const title = textarea.value.trim();
|
||||
const subtask = this.currentData().subtask;
|
||||
const subtask = Template.currentData().subtask;
|
||||
subtask.setTitle(title);
|
||||
},
|
||||
|
||||
pressKey(event) {
|
||||
async 'click .js-delete-subtask-item'() {
|
||||
const subtask = Template.currentData().subtask;
|
||||
if (subtask && subtask._id) {
|
||||
await subtask.archive();
|
||||
}
|
||||
},
|
||||
keydown(event) {
|
||||
//If user press enter key inside a form, submit it
|
||||
//Unless the user is also holding down the 'shift' key
|
||||
if (event.keyCode === 13 && !event.shiftKey) {
|
||||
@@ -89,53 +103,66 @@ BlazeComponent.extendComponent({
|
||||
$form.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-open-subtask-details-menu': Popup.open('subtaskActions'),
|
||||
'submit .js-add-subtask': this.addSubtask,
|
||||
'submit .js-edit-subtask-title': this.editSubtask,
|
||||
'click .js-delete-subtask-item': this.deleteSubtask,
|
||||
keydown: this.pressKey,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('subtasks');
|
||||
Template.subtasks.onCreated(function () {
|
||||
this.toggleDeleteDialog = new ReactiveVar(false);
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
// ...
|
||||
}).register('subtaskItemDetail');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
Template.subtasks.helpers({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-view-subtask'(event) {
|
||||
if ($(event.target).hasClass('js-view-subtask')) {
|
||||
const subtask = this.currentData().subtask;
|
||||
const board = subtask.board();
|
||||
FlowRouter.go('card', {
|
||||
boardId: board._id,
|
||||
slug: board.slug,
|
||||
cardId: subtask._id,
|
||||
});
|
||||
}
|
||||
},
|
||||
'click .js-delete-subtask' : Popup.afterConfirm('subtaskDelete', function () {
|
||||
Popup.back(2);
|
||||
const subtask = this.subtask;
|
||||
if (subtask && subtask._id) {
|
||||
subtask.archive();
|
||||
}
|
||||
}),
|
||||
}
|
||||
]
|
||||
}
|
||||
}).register('subtaskActionsPopup');
|
||||
toggleDeleteDialog() {
|
||||
return Template.instance().toggleDeleteDialog;
|
||||
},
|
||||
});
|
||||
|
||||
Template.subtaskItemDetail.events({
|
||||
async 'click .js-subtasks-item .check-box-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() {
|
||||
@@ -145,5 +172,3 @@ Template.editSubtaskItemForm.helpers({
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/* Original Position Component Styles */
|
||||
.original-position-info {
|
||||
margin: 5px 0;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.original-position-loading {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.original-position-loading i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.original-position-details {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 3px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.original-position-moved {
|
||||
color: #856404;
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 3px;
|
||||
padding: 4px 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.original-position-moved i {
|
||||
color: #f39c12;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.original-position-unchanged {
|
||||
color: #155724;
|
||||
background-color: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
border-radius: 3px;
|
||||
padding: 4px 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.original-position-unchanged i {
|
||||
color: #28a745;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.original-position-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.original-title {
|
||||
color: #6c757d;
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.original-title strong {
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
/* Integration with existing Wekan styles */
|
||||
.swimlane .original-position-info,
|
||||
.list .original-position-info,
|
||||
.card .original-position-info {
|
||||
margin: 2px 0;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.original-position-info {
|
||||
font-size: 11px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.original-position-details {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.original-position-moved,
|
||||
.original-position-unchanged {
|
||||
padding: 3px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.original-position-details {
|
||||
background-color: #2d3748;
|
||||
border-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.original-position-moved {
|
||||
background-color: #744210;
|
||||
border-color: #b7791f;
|
||||
color: #fbd38d;
|
||||
}
|
||||
|
||||
.original-position-unchanged {
|
||||
background-color: #22543d;
|
||||
border-color: #38a169;
|
||||
color: #9ae6b4;
|
||||
}
|
||||
|
||||
.original-title {
|
||||
color: #a0aec0;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
.original-title strong {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
template(name="originalPosition")
|
||||
.original-position-info
|
||||
if isLoading
|
||||
.original-position-loading
|
||||
| ⏳ Loading original position...
|
||||
else
|
||||
if showOriginalPosition
|
||||
.original-position-details
|
||||
if hasMovedFromOriginal
|
||||
.original-position-moved
|
||||
span.original-position-text ℹ️ {{getOriginalPositionDescription}}
|
||||
else
|
||||
.original-position-unchanged
|
||||
span.original-position-text ✅ In original position
|
||||
|
||||
if getOriginalTitle
|
||||
.original-title
|
||||
strong Original title:
|
||||
| {{getOriginalTitle}}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
/**
|
||||
* Component to display original position information for swimlanes, lists, and cards
|
||||
*/
|
||||
|
||||
Template.originalPosition.onCreated(function () {
|
||||
this.originalPosition = new ReactiveVar(null);
|
||||
this.isLoading = new ReactiveVar(false);
|
||||
this.hasMoved = new ReactiveVar(false);
|
||||
|
||||
const tpl = this;
|
||||
|
||||
function loadOriginalPosition(entityId, entityType) {
|
||||
tpl.isLoading.set(true);
|
||||
|
||||
const methodName = `positionHistory.get${entityType.charAt(0).toUpperCase() + entityType.slice(1)}OriginalPosition`;
|
||||
|
||||
Meteor.call(methodName, entityId, (error, result) => {
|
||||
tpl.isLoading.set(false);
|
||||
if (error) {
|
||||
console.error('Error loading original position:', error);
|
||||
tpl.originalPosition.set(null);
|
||||
} else {
|
||||
tpl.originalPosition.set(result);
|
||||
|
||||
// Check if the entity has moved
|
||||
const movedMethodName = `positionHistory.has${entityType.charAt(0).toUpperCase() + entityType.slice(1)}Moved`;
|
||||
Meteor.call(movedMethodName, entityId, (movedError, movedResult) => {
|
||||
if (!movedError) {
|
||||
tpl.hasMoved.set(movedResult);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.autorun(() => {
|
||||
const data = Template.currentData();
|
||||
if (data && data.entityId && data.entityType) {
|
||||
loadOriginalPosition(data.entityId, data.entityType);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.originalPosition.helpers({
|
||||
getOriginalPosition() {
|
||||
return Template.instance().originalPosition.get();
|
||||
},
|
||||
|
||||
isLoading() {
|
||||
return Template.instance().isLoading.get();
|
||||
},
|
||||
|
||||
hasMovedFromOriginal() {
|
||||
return Template.instance().hasMoved.get();
|
||||
},
|
||||
|
||||
getOriginalPositionDescription() {
|
||||
const position = Template.instance().originalPosition.get();
|
||||
if (!position) return 'No original position data';
|
||||
|
||||
if (position.originalPosition) {
|
||||
const data = Template.currentData();
|
||||
const entityType = data.entityType;
|
||||
let description = `Original position: ${position.originalPosition.sort || 0}`;
|
||||
|
||||
if (entityType === 'list' && position.originalSwimlaneId) {
|
||||
description += ` in swimlane ${position.originalSwimlaneId}`;
|
||||
} else if (entityType === 'card') {
|
||||
if (position.originalSwimlaneId) {
|
||||
description += ` in swimlane ${position.originalSwimlaneId}`;
|
||||
}
|
||||
if (position.originalListId) {
|
||||
description += ` in list ${position.originalListId}`;
|
||||
}
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
return 'No original position data';
|
||||
},
|
||||
|
||||
getOriginalTitle() {
|
||||
const position = Template.instance().originalPosition.get();
|
||||
return position ? position.originalTitle : '';
|
||||
},
|
||||
|
||||
showOriginalPosition() {
|
||||
return Template.instance().originalPosition.get() !== null;
|
||||
},
|
||||
});
|
||||
@@ -4,11 +4,10 @@ template(name="datepicker")
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="text" name="date" value=showDate placeholder=dateFormat autofocus)
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="text" name="time" value=showTime placeholder=timeFormat)
|
||||
.js-datepicker
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
setupDatePicker,
|
||||
datePickerRendered,
|
||||
datePickerHelpers,
|
||||
datePickerEvents,
|
||||
} from '/client/lib/datepicker';
|
||||
|
||||
Template.datepicker.onCreated(function () {
|
||||
setupDatePicker(this);
|
||||
});
|
||||
|
||||
Template.datepicker.onRendered(function () {
|
||||
datePickerRendered(this);
|
||||
});
|
||||
|
||||
Template.datepicker.helpers(datePickerHelpers());
|
||||
@@ -130,8 +130,8 @@ textarea.editor {
|
||||
}
|
||||
input[type="submit"],
|
||||
button {
|
||||
background: #cfcfcf;
|
||||
background: linear-gradient(#cfcfcf, #c2c2c2);
|
||||
background: #000;
|
||||
background: linear-gradient(#000, #000);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
@@ -139,6 +139,7 @@ button {
|
||||
line-height: 1.3;
|
||||
padding: 1vh 2.5vw;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
input[type="submit"] .wide,
|
||||
button .wide {
|
||||
@@ -149,14 +150,16 @@ input[type="submit"]:hover,
|
||||
button:hover,
|
||||
input[type="submit"]:focus,
|
||||
button:focus {
|
||||
background: #c2c2c2;
|
||||
background: linear-gradient(#c2c2c2, #b5b5b5);
|
||||
background: #222;
|
||||
background: linear-gradient(#222, #222);
|
||||
color: #fff;
|
||||
}
|
||||
input[type="submit"]:active,
|
||||
button:active {
|
||||
background: #b5b5b5;
|
||||
background: linear-gradient(#b5b5b5, #a8a8a8);
|
||||
box-shadow: inset 0 3px 6px rgba(0,0,0,0.1);
|
||||
background: #111;
|
||||
background: linear-gradient(#111, #111);
|
||||
box-shadow: inset 0 3px 6px rgba(0,0,0,0.3);
|
||||
color: #fff;
|
||||
}
|
||||
input[type="submit"]:active:hover,
|
||||
button:active:hover,
|
||||
@@ -183,6 +186,12 @@ input[type="submit"].primary:active,
|
||||
button.primary:active {
|
||||
background: #01628c;
|
||||
}
|
||||
input[type="submit"].negate,
|
||||
button.negate {
|
||||
background: #eb5a46;
|
||||
box-shadow: 0 1px 0 #4d4d4d;
|
||||
color: #fff;
|
||||
}
|
||||
input[type="submit"].negate:hover,
|
||||
button.negate:hover,
|
||||
input[type="submit"].negate:focus,
|
||||
@@ -217,10 +226,10 @@ input[type="submit"]:disabled:active,
|
||||
input[type="button"].disabled:active,
|
||||
button.disabled:active,
|
||||
.button.disabled:active {
|
||||
background: #cfcfcf;
|
||||
background: #555;
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
color: #a8a8a8;
|
||||
color: #999;
|
||||
}
|
||||
fieldset {
|
||||
border: 1px solid #bfbfbf;
|
||||
@@ -315,11 +324,18 @@ textarea::-moz-placeholder {
|
||||
margin-right: 6px;
|
||||
border-top: 2px solid transparent;
|
||||
border-left: 2px solid transparent;
|
||||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
transform: rotate(40deg);
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
transform-origin: 100% 100%;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .materialCheckBox.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
.button-link {
|
||||
background: #fff;
|
||||
background: linear-gradient(#fff, #f5f5f5);
|
||||
@@ -393,12 +409,12 @@ textarea::-moz-placeholder {
|
||||
.button-link.setting.disabled.primary,
|
||||
.button-link.setting.disabled.primary:hover,
|
||||
.button-link.setting.disabled.primary:active {
|
||||
background: #cfcfcf;
|
||||
border-color: #c2c2c2;
|
||||
border-bottom-color: #b5b5b5;
|
||||
background: #555;
|
||||
border-color: #444;
|
||||
border-bottom-color: #333;
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
color: #a8a8a8;
|
||||
color: #999;
|
||||
}
|
||||
.button-link.setting .label {
|
||||
color: #222;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
template(name='inlinedForm')
|
||||
if isOpen.get
|
||||
form.inlined-form.js-inlined-form(id=id class=classNames)
|
||||
+Template.contentBlock
|
||||
else
|
||||
+Template.elseBlock
|
||||
.js-inlined-form-wrapper(style='display: contents;' data-position=position)
|
||||
if isOpen.get
|
||||
form.inlined-form.js-inlined-form(id=id class=classNames)
|
||||
+Template.contentBlock
|
||||
else
|
||||
+Template.elseBlock
|
||||
@@ -0,0 +1,203 @@
|
||||
/* Gantt chart cell background colors for Received, Start, Due, End (matching cardDetails) */
|
||||
.ganttview-received {
|
||||
background-color: #dbdbdb !important;
|
||||
color: #000 !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.ganttview-start {
|
||||
background-color: #90ee90 !important;
|
||||
color: #000 !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.ganttview-due {
|
||||
background-color: #ffd700 !important;
|
||||
color: #000 !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.ganttview-end {
|
||||
background-color: #ffb3b3 !important;
|
||||
color: #000 !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
/* Gantt View Styles */
|
||||
|
||||
.gantt-view {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.gantt-view.swimlane {
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.gantt-container {
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
background-color: #fff;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gantt-container table,
|
||||
.gantt-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
min-width: 800px;
|
||||
border: 2px solid #666;
|
||||
font-family: sans-serif;
|
||||
font-size: 13px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.gantt-container thead {
|
||||
background-color: #e8e8e8;
|
||||
border-bottom: 2px solid #666;
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.gantt-container thead th,
|
||||
.gantt-container thead tr > td:first-child {
|
||||
border-right: 2px solid #666;
|
||||
padding: 4px; /* half of 8px */
|
||||
width: 100px; /* half of 200px */
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
background-color: #e8e8e8;
|
||||
min-width: 100px; /* half of 200px */
|
||||
}
|
||||
|
||||
.gantt-container thead td {
|
||||
border-right: 1px solid #999;
|
||||
padding: 2px 1px; /* half */
|
||||
text-align: center;
|
||||
background-color: #f5f5f5;
|
||||
font-size: 11px;
|
||||
min-width: 15px; /* half of 30px */
|
||||
font-weight: bold;
|
||||
height: auto;
|
||||
line-height: 1.2;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.gantt-container tbody tr {
|
||||
border-bottom: 1px solid #999;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.gantt-container tbody tr:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.gantt-container tbody tr:hover td {
|
||||
background-color: #f9f9f9 !important;
|
||||
}
|
||||
|
||||
.gantt-container tbody td {
|
||||
border-right: 1px solid #ccc;
|
||||
padding: 1px; /* half */
|
||||
text-align: center;
|
||||
min-width: 15px; /* half of 30px */
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
line-height: 28px;
|
||||
background-color: #ffffff;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.gantt-container tbody td:nth-child(even) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.gantt-container tbody td:first-child {
|
||||
border-right: 2px solid #666;
|
||||
padding: 4px; /* half of 8px */
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background-color: #fafafa !important;
|
||||
text-align: left;
|
||||
width: 100px; /* half of 200px */
|
||||
min-width: 100px; /* half of 200px */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.gantt-container tbody td:first-child:hover {
|
||||
background-color: #f0f0f0 !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.js-gantt-task-cell {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.js-gantt-date-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gantt-container .ganttview-weekend {
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
.gantt-container .ganttview-today {
|
||||
background-color: #fcf8e3;
|
||||
border-right: 2px solid #ffb347;
|
||||
}
|
||||
|
||||
/* Task bar styling - VERY VISIBLE */
|
||||
.gantt-container tbody td.ganttview-block {
|
||||
background-color: #4CAF50 !important;
|
||||
color: #fff !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: bold !important;
|
||||
padding: 2px !important;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.gantt-container table {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.gantt-container thead td {
|
||||
min-width: 20px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.gantt-container tbody td {
|
||||
min-width: 20px;
|
||||
padding: 1px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.gantt-container tbody td:first-child {
|
||||
width: 100px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.gantt-container {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.gantt-container table {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//- Gantt Chart View Template
|
||||
template(name="ganttView")
|
||||
link(rel="stylesheet" href="/client/components/gantt/gantt.css")
|
||||
link(rel="stylesheet" href="/client/components/gantt/ganttCard.css")
|
||||
.gantt-view
|
||||
h2 {{_ 'board-view-gantt'}}
|
||||
if hasSelectedCard
|
||||
+ganttCard(selectedCard)
|
||||
each weeks
|
||||
table.gantt-table
|
||||
thead
|
||||
tr
|
||||
th {{_ 'task'}} {{_ 'predicate-week'}} {{week}}
|
||||
each weekDays this
|
||||
th
|
||||
| {{formattedDate .}} {{weekdayLabel .}}
|
||||
tbody
|
||||
each cardsInWeek this
|
||||
tr(data-card-id="{{cardId .}}")
|
||||
td.js-gantt-task-cell
|
||||
a.js-gantt-card-title(href="#")
|
||||
+viewer
|
||||
| {{cardTitle .}}
|
||||
each weekDays ..
|
||||
td(class="{{cellClasses .. .}}" data-card-id="{{cardId ..}}" data-date-type="{{cellContentClass .. .}}")
|
||||
| {{cellContent .. .}}
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import Cards from '/models/cards';
|
||||
|
||||
// Add click handler to ganttView for card titles
|
||||
Template.ganttView.events({
|
||||
'click .js-gantt-card-title'(event, template) {
|
||||
event.preventDefault();
|
||||
// Get card ID from the closest row's data attribute
|
||||
const $row = template.$(event.currentTarget).closest('tr');
|
||||
const cardId = $row.data('card-id');
|
||||
|
||||
if (cardId) {
|
||||
template.selectedCardId.set(cardId);
|
||||
}
|
||||
},
|
||||
});
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Utils } from '/client/lib/utils';
|
||||
|
||||
// Blaze template helpers for ganttView
|
||||
function getISOWeekInfo(d) {
|
||||
const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
||||
const dayNum = date.getUTCDay() || 7;
|
||||
date.setUTCDate(date.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
|
||||
const week = Math.ceil((((date - yearStart) / 86400000) + 1) / 7);
|
||||
return { year: date.getUTCFullYear(), week };
|
||||
}
|
||||
function startOfISOWeek(d) {
|
||||
const date = new Date(d);
|
||||
const day = date.getDay() || 7;
|
||||
if (day !== 1) date.setDate(date.getDate() - (day - 1));
|
||||
date.setHours(0,0,0,0);
|
||||
return date;
|
||||
}
|
||||
|
||||
Template.ganttView.helpers({
|
||||
weeks() {
|
||||
const board = Utils.getCurrentBoard();
|
||||
if (!board) return [];
|
||||
const cards = Cards.find({ boardId: board._id }, { sort: { startAt: 1, dueAt: 1 } }).fetch();
|
||||
const weeksMap = new Map();
|
||||
const relevantCards = cards.filter(c => c.receivedAt || c.startAt || c.dueAt || c.endAt);
|
||||
relevantCards.forEach(card => {
|
||||
['receivedAt','startAt','dueAt','endAt'].forEach(field => {
|
||||
if (card[field]) {
|
||||
const dt = new Date(card[field]);
|
||||
const info = getISOWeekInfo(dt);
|
||||
const key = `${info.year}-W${info.week}`;
|
||||
if (!weeksMap.has(key)) {
|
||||
weeksMap.set(key, { year: info.year, week: info.week, start: startOfISOWeek(dt) });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return Array.from(weeksMap.values()).sort((a,b) => a.start - b.start);
|
||||
},
|
||||
weekDays(week) {
|
||||
const weekStart = new Date(week.start);
|
||||
return Array.from({length:7}, (_,i) => {
|
||||
const d = new Date(weekStart);
|
||||
d.setDate(d.getDate() + i);
|
||||
d.setHours(0,0,0,0);
|
||||
return d;
|
||||
});
|
||||
},
|
||||
weekdayLabel(day) {
|
||||
const weekdayKeys = ['monday','tuesday','wednesday','thursday','friday','saturday','sunday'];
|
||||
return TAPi18n.__(weekdayKeys[day.getDay() === 0 ? 6 : day.getDay() - 1]);
|
||||
},
|
||||
formattedDate(day) {
|
||||
const currentUser = ReactiveCache.getCurrentUser && ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(day, dateFormat, false);
|
||||
},
|
||||
cardsInWeek(week) {
|
||||
const board = Utils.getCurrentBoard();
|
||||
if (!board) return [];
|
||||
const cards = Cards.find({ boardId: board._id }).fetch();
|
||||
return cards.filter(card => {
|
||||
return ['receivedAt','startAt','dueAt','endAt'].some(field => {
|
||||
if (card[field]) {
|
||||
const dt = new Date(card[field]);
|
||||
const info = getISOWeekInfo(dt);
|
||||
return info.week === week.week && info.year === week.year;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
},
|
||||
cardTitle(card) {
|
||||
return card.title;
|
||||
},
|
||||
cardId(card) {
|
||||
return card._id;
|
||||
},
|
||||
cardUrl(card) {
|
||||
if (!card) return '#';
|
||||
const board = ReactiveCache.getBoard(card.boardId);
|
||||
if (!board) return '#';
|
||||
return FlowRouter.path('card', {
|
||||
boardId: card.boardId,
|
||||
slug: board.slug,
|
||||
cardId: card._id,
|
||||
});
|
||||
},
|
||||
cellContentClass(card, day) {
|
||||
const cardDates = {
|
||||
receivedAt: card.receivedAt ? new Date(card.receivedAt) : null,
|
||||
startAt: card.startAt ? new Date(card.startAt) : null,
|
||||
dueAt: card.dueAt ? new Date(card.dueAt) : null,
|
||||
endAt: card.endAt ? new Date(card.endAt) : null,
|
||||
};
|
||||
if (cardDates.receivedAt && cardDates.receivedAt.toDateString() === day.toDateString()) return 'ganttview-received';
|
||||
if (cardDates.startAt && cardDates.startAt.toDateString() === day.toDateString()) return 'ganttview-start';
|
||||
if (cardDates.dueAt && cardDates.dueAt.toDateString() === day.toDateString()) return 'ganttview-due';
|
||||
if (cardDates.endAt && cardDates.endAt.toDateString() === day.toDateString()) return 'ganttview-end';
|
||||
return '';
|
||||
},
|
||||
cellContent(card, day) {
|
||||
const cardDates = {
|
||||
receivedAt: card.receivedAt ? new Date(card.receivedAt) : null,
|
||||
startAt: card.startAt ? new Date(card.startAt) : null,
|
||||
dueAt: card.dueAt ? new Date(card.dueAt) : null,
|
||||
endAt: card.endAt ? new Date(card.endAt) : null,
|
||||
};
|
||||
if (cardDates.receivedAt && cardDates.receivedAt.toDateString() === day.toDateString()) return '📥';
|
||||
if (cardDates.startAt && cardDates.startAt.toDateString() === day.toDateString()) return '🚀';
|
||||
if (cardDates.dueAt && cardDates.dueAt.toDateString() === day.toDateString()) return '⏰';
|
||||
if (cardDates.endAt && cardDates.endAt.toDateString() === day.toDateString()) return '🏁';
|
||||
return '';
|
||||
},
|
||||
isToday(day) {
|
||||
const today = new Date();
|
||||
return day.toDateString() === today.toDateString();
|
||||
},
|
||||
isWeekend(day) {
|
||||
const idx = day.getDay();
|
||||
return idx === 0 || idx === 6;
|
||||
},
|
||||
hasSelectedCard() {
|
||||
return Template.instance().selectedCardId.get() !== null;
|
||||
},
|
||||
selectedCard() {
|
||||
const cardId = Template.instance().selectedCardId.get();
|
||||
return cardId ? ReactiveCache.getCard(cardId) : null;
|
||||
},
|
||||
cellClasses(card, day) {
|
||||
// Get the base class from cellContentClass logic
|
||||
const cardDates = {
|
||||
receivedAt: card.receivedAt ? new Date(card.receivedAt) : null,
|
||||
startAt: card.startAt ? new Date(card.startAt) : null,
|
||||
dueAt: card.dueAt ? new Date(card.dueAt) : null,
|
||||
endAt: card.endAt ? new Date(card.endAt) : null,
|
||||
};
|
||||
let classes = '';
|
||||
if (cardDates.receivedAt && cardDates.receivedAt.toDateString() === day.toDateString()) classes = 'ganttview-received';
|
||||
else if (cardDates.startAt && cardDates.startAt.toDateString() === day.toDateString()) classes = 'ganttview-start';
|
||||
else if (cardDates.dueAt && cardDates.dueAt.toDateString() === day.toDateString()) classes = 'ganttview-due';
|
||||
else if (cardDates.endAt && cardDates.endAt.toDateString() === day.toDateString()) classes = 'ganttview-end';
|
||||
|
||||
// Add conditional classes
|
||||
const today = new Date();
|
||||
if (day.toDateString() === today.toDateString()) classes += ' ganttview-today';
|
||||
const idx = day.getDay();
|
||||
if (idx === 0 || idx === 6) classes += ' ganttview-weekend';
|
||||
if (classes.trim()) classes += ' js-gantt-date-icon';
|
||||
|
||||
return classes.trim();
|
||||
}
|
||||
});
|
||||
|
||||
Template.ganttView.onCreated(function() {
|
||||
this.selectedCardId = new ReactiveVar(null);
|
||||
// Provide properties expected by cardDetails component
|
||||
this.showOverlay = new ReactiveVar(false);
|
||||
this.mouseHasEnterCardDetails = false;
|
||||
});
|
||||
|
||||
// Blaze onRendered logic for ganttView
|
||||
Template.ganttView.onRendered(function() {
|
||||
const self = this;
|
||||
this.autorun(() => {
|
||||
// If you have legacy imperative rendering, keep it here
|
||||
if (typeof renderGanttChart === 'function') {
|
||||
renderGanttChart();
|
||||
}
|
||||
});
|
||||
// Add click handler for date cells (Received, Start, Due, End)
|
||||
this.$('.gantt-table').on('click', '.js-gantt-date-icon', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const $cell = self.$(this);
|
||||
const cardId = $cell.data('card-id');
|
||||
let dateType = $cell.data('date-type');
|
||||
// Remove 'ganttview-' prefix to match popup map
|
||||
if (typeof dateType === 'string' && dateType.startsWith('ganttview-')) {
|
||||
dateType = dateType.replace('ganttview-', '');
|
||||
}
|
||||
const popupMap = {
|
||||
received: 'editCardReceivedDate',
|
||||
start: 'editCardStartDate',
|
||||
due: 'editCardDueDate',
|
||||
end: 'editCardEndDate',
|
||||
};
|
||||
const popupName = popupMap[dateType];
|
||||
if (!popupName || typeof Popup === 'undefined' || typeof Popup.open !== 'function') return;
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
if (!card) return;
|
||||
const openFn = Popup.open(popupName);
|
||||
openFn.call({ currentData: () => card }, e, { dataContextIfCurrentDataIsUndefined: card });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
import markdownit from 'markdown-it';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
|
||||
const md = markdownit({ breaks: true, linkify: true });
|
||||
@@ -0,0 +1,47 @@
|
||||
.gantt-card-wrapper {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.gantt-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.gantt-card-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-color: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.gantt-card-content {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
template(name="ganttCard")
|
||||
+cardDetails(selectedCard)
|
||||
@@ -0,0 +1,41 @@
|
||||
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 Template.currentData();
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Add click handler to ganttView for card titles
|
||||
Template.ganttView.events({
|
||||
'click .js-gantt-card-title'(event, template) {
|
||||
event.preventDefault();
|
||||
// Get card ID from the closest row's data attribute
|
||||
const $row = template.$(event.currentTarget).closest('tr');
|
||||
const cardId = $row.data('card-id');
|
||||
|
||||
if (cardId) {
|
||||
template.selectedCardId.set(cardId);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
template(name="importHeaderBar")
|
||||
h1
|
||||
a.back-btn(href="{{pathFor 'home'}}")
|
||||
i.fa.fa-chevron-left
|
||||
i.fa-arrow-left
|
||||
| {{_ title}}
|
||||
|
||||
template(name="import")
|
||||
@@ -56,17 +56,17 @@ template(name="importMapMembersAddPopup")
|
||||
p
|
||||
| {{_ 'import-user-select'}}
|
||||
.js-map-member
|
||||
+EasySearch.Input(index=searchIndex)
|
||||
input.js-search-member-input(type="text" placeholder="{{_ 'search-users'}}")
|
||||
ul.pop-over-list
|
||||
+EasySearch.Each(index=searchIndex)
|
||||
each searchResults
|
||||
li.item.js-member-item
|
||||
a.name.js-select-import(title="{{profile.fullname}} ({{username}})" data-id="{{__originalId}}")
|
||||
+userAvatar(userId=__originalId)
|
||||
a.name.js-select-import(title="{{profile.fullname}} ({{username}})" data-id="{{_id}}")
|
||||
+userAvatar(userId=_id)
|
||||
span.full-name
|
||||
= profile.fullname
|
||||
| (<span class="username">{{username}}</span>)
|
||||
+EasySearch.IfSearching(index=searchIndex)
|
||||
if searching.get
|
||||
+spinner
|
||||
+EasySearch.IfNoResults(index=searchIndex)
|
||||
if noResults.get
|
||||
.manage-member-section
|
||||
p.quiet {{_ 'no-results'}}
|
||||
+234
-168
@@ -1,40 +1,71 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
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') {
|
||||
@@ -42,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 {
|
||||
@@ -50,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);
|
||||
@@ -59,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) {
|
||||
@@ -93,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') {
|
||||
@@ -139,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) {
|
||||
@@ -239,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) {
|
||||
@@ -258,59 +220,163 @@ 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();
|
||||
},
|
||||
// Global reactive variables for import member popup
|
||||
const importMemberPopupState = {
|
||||
searching: new ReactiveVar(false),
|
||||
searchResults: new ReactiveVar([]),
|
||||
noResults: new ReactiveVar(false),
|
||||
searchTimeout: null,
|
||||
};
|
||||
|
||||
onSelectUser() {
|
||||
Popup.getOpenerComponent(5).mapSelectedMember(this.currentData().__originalId);
|
||||
Template.importMapMembersAddPopup.onCreated(function () {
|
||||
this.searching = importMemberPopupState.searching;
|
||||
this.searchResults = importMemberPopupState.searchResults;
|
||||
this.noResults = importMemberPopupState.noResults;
|
||||
this.searchTimeout = null;
|
||||
|
||||
this.searching.set(false);
|
||||
this.searchResults.set([]);
|
||||
this.noResults.set(false);
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-select-import': this.onSelectUser,
|
||||
},
|
||||
];
|
||||
if (tpl.searchTimeout) {
|
||||
clearTimeout(tpl.searchTimeout);
|
||||
}
|
||||
|
||||
tpl.searchTimeout = setTimeout(() => {
|
||||
importPerformSearch(tpl, query);
|
||||
}, 300);
|
||||
},
|
||||
}).register('importMapMembersAddPopup');
|
||||
});
|
||||
|
||||
Template.importMapMembersAddPopup.helpers({
|
||||
searchIndex: () => UserSearchIndex,
|
||||
})
|
||||
searchResults() {
|
||||
return importMemberPopupState.searchResults.get();
|
||||
},
|
||||
searching() {
|
||||
return importMemberPopupState.searching;
|
||||
},
|
||||
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';
|
||||
},
|
||||
});
|
||||
+770
-111
File diff suppressed because it is too large.
Load diff
Loaded 100 of 1297 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user