Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aca661583d | ||
|
|
ca79fcb7e9 | ||
|
|
04bfa0e6ba | ||
|
|
3f8059baad | ||
|
|
b803d5336e | ||
|
|
7b72fcaee1 | ||
|
|
c56bdb84d2 | ||
|
|
ccc0d16b52 | ||
|
|
f38e3624cf | ||
|
|
8586fa8ce0 | ||
|
|
a5444e08ab | ||
|
|
f9e584e13c | ||
|
|
c795dfe96b | ||
|
|
a528a411da | ||
|
|
d96b8942e7 | ||
|
|
a31a615da6 | ||
|
|
e177bf54e2 | ||
|
|
ce55f0d8f4 | ||
|
|
320b76aec4 | ||
|
|
5d86bcce6d | ||
|
|
0b9f23bd4a | ||
|
|
526251397e | ||
|
|
997b9a7c76 | ||
|
|
bfebee5949 | ||
|
|
bd899b2bf2 | ||
|
|
ad511bd137 | ||
|
|
603a65ef17 | ||
|
|
2fa490d83d | ||
|
|
a787bcddf3 | ||
|
|
91a936e07d | ||
|
|
4ce181d172 | ||
|
|
0f5a9c3877 | ||
|
|
545566f566 | ||
|
|
8c0b4f79d8 | ||
|
|
c413a7e860 | ||
|
|
053bf1dfb7 | ||
|
|
146905a459 | ||
|
|
55576ec177 | ||
|
|
cc35dafef5 | ||
|
|
251d49eea9 | ||
|
|
cabfeed9a6 | ||
|
|
b4b90b1c41 | ||
|
|
d337afd5d3 | ||
|
|
4473ca09f6 | ||
|
|
d30192f7f9 | ||
|
|
7291617a77 | ||
|
|
95da8966fe | ||
|
|
db7ffc98fa | ||
|
|
cb43a41b0c | ||
|
|
a7e29b92bf | ||
|
|
4899330b6e | ||
|
|
c56bc0be7f | ||
|
|
fd8ce39990 | ||
|
|
f9ed63ac76 | ||
|
|
641d4c2f7f | ||
|
|
980510d71a | ||
|
|
07001a229d | ||
|
|
e2ec50730f | ||
|
|
20884cf6c7 | ||
|
|
f0118d52e9 | ||
|
|
d4e922eed4 | ||
|
|
b88b27689a | ||
|
|
a29733be9c | ||
|
|
33e0a82967 | ||
|
|
13b5b170b6 | ||
|
|
e2f2090da1 | ||
|
|
eabb6a239d | ||
|
|
2f59e42024 | ||
|
|
20b5e2ab8f | ||
|
|
0d5dd3082c | ||
|
|
5cb712bee4 | ||
|
|
00793c39f8 | ||
|
|
e37220cde6 | ||
|
|
dd20988b1c | ||
|
|
32785376d8 | ||
|
|
4a39bcc6ee | ||
|
|
984a2dcec1 | ||
|
|
c8bdb95b38 | ||
|
|
e89f4d260c | ||
|
|
eca42f32bb | ||
|
|
6ad9770bb6 | ||
|
|
5b47340b76 | ||
|
|
69f54dddaf | ||
|
|
b3cb47ac47 | ||
|
|
69e2ad1007 | ||
|
|
40784b2054 | ||
|
|
ab49e09da6 | ||
|
|
6ed8e54dac | ||
|
|
c80d1aae4c | ||
|
|
a62b3c6ce0 | ||
|
|
8a0568ce01 | ||
|
|
c740474145 | ||
|
|
4a373b45e1 | ||
|
|
32733023ca | ||
|
|
0ffb0fa24f | ||
|
|
b5177e3870 | ||
|
|
c326e58d5e | ||
|
|
4d4046986f | ||
|
|
36cbd3a606 | ||
|
|
715d47dd2c | ||
|
|
71d84f58a4 | ||
|
|
306305a95c | ||
|
|
2bc9fa1629 | ||
|
|
fd45ae2a62 | ||
|
|
46100cfd1d | ||
|
|
1d62322b3c | ||
|
|
d784d2b633 | ||
|
|
07186e12a9 | ||
|
|
0635a663f0 | ||
|
|
dcd8d80b9d | ||
|
|
58ae2b6c68 | ||
|
|
c76a569fb0 | ||
|
|
66e79d2df7 | ||
|
|
fc06ae1000 | ||
|
|
dac7e17500 | ||
|
|
8f51fcd65a | ||
|
|
a4f8faa48e | ||
|
|
b5ab917dfb | ||
|
|
376a30f8a9 | ||
|
|
9ebf4d2426 | ||
|
|
42c14faf41 | ||
|
|
4a658dc02a | ||
|
|
f250cba405 | ||
|
|
cf6e691498 | ||
|
|
5b2e6511b0 | ||
|
|
52a9514536 | ||
|
|
ffd37b9fd9 | ||
|
|
e2b80652a5 | ||
|
|
bd37b88e4d | ||
|
|
a0adc54a08 | ||
|
|
9431b2d530 | ||
|
|
594ef40f2e | ||
|
|
aacdcea37b | ||
|
|
309a456a0d | ||
|
|
f198421f10 | ||
|
|
fa0460b1f0 | ||
|
|
9fb1aeb827 | ||
|
|
59ad67c08c | ||
|
|
ec57618c98 | ||
|
|
3b378961b0 | ||
|
|
cbb1cd78de | ||
|
|
d6834d0287 | ||
|
|
a9a89b501a | ||
|
|
7de4385160 | ||
|
|
7ac5a2bba5 | ||
|
|
e0af5db1a9 | ||
|
|
87cd07dec7 | ||
|
|
e80f8e5121 | ||
|
|
2325a5c532 | ||
|
|
b1db262b37 | ||
|
|
2e564bd076 | ||
|
|
e09e9114aa | ||
|
|
efd91a8f72 | ||
|
|
fbfde81bc8 | ||
|
|
cf62807ad5 | ||
|
|
2d87ba18b3 | ||
|
|
74f1dfde72 | ||
|
|
db4b04d837 | ||
|
|
0b0e16c3ea | ||
|
|
1d16955b6d | ||
|
|
6dfa3beb2b | ||
|
|
5cd875813f | ||
|
|
08a6f084eb | ||
|
|
181f837d8c | ||
|
|
198509e760 | ||
|
|
67cb47173c | ||
|
|
7ed76c180e | ||
|
|
f244a43771 | ||
|
|
48e856fca2 | ||
|
|
7d83cb3d0b | ||
|
|
3af3c9a89d | ||
|
|
2d3bef9033 | ||
|
|
ce9afbcaca | ||
|
|
ba79d5389d | ||
|
|
2f4c40c1db | ||
|
|
6c9e8a4aa8 | ||
|
|
223c38c50d | ||
|
|
c1981dee9b | ||
|
|
45b3373145 | ||
|
|
54a566cfb1 | ||
|
|
5a51c2940a | ||
|
|
7232070bd1 | ||
|
|
33ab8920ca | ||
|
|
db4d47cc52 | ||
|
|
e210c9973b | ||
|
|
98d05ce545 | ||
|
|
9268d80974 | ||
|
|
1a221a6967 | ||
|
|
a039bb1066 | ||
|
|
90a7a61904 | ||
|
|
58e970d685 | ||
|
|
edf0f3c05b | ||
|
|
5bbf5e0dac | ||
|
|
414b8dbf41 | ||
|
|
2e0e1e56b5 | ||
|
|
546dcdf1de | ||
|
|
58f4884ad6 | ||
|
|
95d1625a9f | ||
|
|
05fbdd559f | ||
|
|
300b653ea3 | ||
|
|
1808ea7384 | ||
|
|
4408eae158 | ||
|
|
a68993d099 | ||
|
|
ecfb0f0fdf | ||
|
|
fc3bb962f7 | ||
|
|
23630a4c67 | ||
|
|
b0a7fea724 | ||
|
|
47bbd5b4a8 | ||
|
|
a7400dca45 | ||
|
|
4227377449 | ||
|
|
c1168d181b | ||
|
|
21fb8e9164 | ||
|
|
e569c2957e | ||
|
|
4292302c3c | ||
|
|
f34e4c0e36 | ||
|
|
1790918006 | ||
|
|
400eb81206 | ||
|
|
e07e461e48 | ||
|
|
451e9f7870 | ||
|
|
3c57840340 | ||
|
|
2cbf64da33 | ||
|
|
3ef0a3e685 | ||
|
|
9ebdc82d46 | ||
|
|
fe4b36b85d | ||
|
|
b7ff370561 | ||
|
|
5686c92e05 | ||
|
|
784c5c6b0c | ||
|
|
30541260f0 | ||
|
|
59fc756a0b | ||
|
|
4e346c0ab7 | ||
|
|
19fa12bb26 | ||
|
|
e669b1b9c7 | ||
|
|
f24e4a4ce3 | ||
|
|
8b5430acb8 | ||
|
|
614cb44b55 | ||
|
|
0ce8e8b74d | ||
|
|
4ea53af76e | ||
|
|
016f17d663 | ||
|
|
07f69950a7 | ||
|
|
cec625607d | ||
|
|
a290c7b34b | ||
|
|
5b77ac1b44 | ||
|
|
41c635afb5 | ||
|
|
adbf729cb2 | ||
|
|
88ea716d63 | ||
|
|
003a07ebce | ||
|
|
d3c237bc66 | ||
|
|
bac0fa81fc | ||
|
|
a42915614a | ||
|
|
5ff9bf331f | ||
|
|
36d7b0f8a7 | ||
|
|
67c8a98f20 | ||
|
|
a81a603031 | ||
|
|
e30ce78053 | ||
|
|
3d70de94c6 | ||
|
|
1b6e8797ec | ||
|
|
70975c2944 | ||
|
|
960e2126b4 | ||
|
|
3db1305e58 | ||
|
|
f16780b5e3 | ||
|
|
37a3065f3c | ||
|
|
7ff1649d89 | ||
|
|
a39ae31b45 | ||
|
|
6302a48221 | ||
|
|
c277bee9d2 | ||
|
|
c5f5ce126d | ||
|
|
0004ae716b | ||
|
|
7f53dfac3c | ||
|
|
18003900c2 | ||
|
|
fe104791b5 | ||
|
|
6244657ca5 | ||
|
|
46866dac85 | ||
|
|
c829c073cf | ||
|
|
0772ca4036 | ||
|
|
581733d605 | ||
|
|
b02af27ac3 | ||
|
|
20af0a2ef5 | ||
|
|
c58ab5b07d | ||
|
|
e5e711c938 | ||
|
|
42594abe4e | ||
|
|
0afbdc95b4 | ||
|
|
16a74bb748 | ||
|
|
8711b476be | ||
|
|
df9fba4765 | ||
|
|
7d27139aa9 | ||
|
|
e4638d5fbc | ||
|
|
bc5854dd29 | ||
|
|
ba49d4d140 | ||
|
|
71b7dcffb5 | ||
|
|
7713e613b4 | ||
|
|
91a0aa7387 | ||
|
|
fbd6b920ef | ||
|
|
1b25d1d572 | ||
|
|
e93e72234c | ||
|
|
15d9b0ae3a | ||
|
|
550d87ac6c | ||
|
|
f8e576e890 | ||
|
|
fb8ef4d978 | ||
|
|
5127e87898 | ||
|
|
3f2d4444e4 | ||
|
|
9c7badb0eb | ||
|
|
9d9f77a731 | ||
|
|
c400ce74b1 | ||
|
|
c2e20ee4a3 | ||
|
|
ccd9034339 | ||
|
|
0a1a075f31 | ||
|
|
4aaeec9515 | ||
|
|
ea310d7508 | ||
|
|
0a2e6a0c38 | ||
|
|
f26d582018 | ||
|
|
e9a727301d | ||
|
|
d64d2f9c42 | ||
|
|
5c0d122e84 | ||
|
|
5079c853a7 | ||
|
|
b039ba12a2 | ||
|
|
3323ac6ac1 | ||
|
|
3204311ac1 | ||
|
|
0fc2ad97cd | ||
|
|
30620d0ca4 | ||
|
|
bccc22c5fe | ||
|
|
ecf2418347 | ||
|
|
0c99cb3103 | ||
|
|
034dc08269 | ||
|
|
d1a51b42f6 | ||
|
|
92bfbb2d0c | ||
|
|
91b846e2cd | ||
|
|
7fe7fb4c15 | ||
|
|
0cebd8aa4d | ||
|
|
8662c96d1c | ||
|
|
0cbc9402f3 | ||
|
|
940df02456 | ||
|
|
b4b598f542 | ||
|
|
ef19c35b5a | ||
|
|
fc98120269 | ||
|
|
b8a3d6deaf | ||
|
|
45537ede87 | ||
|
|
29a9c5bc7b | ||
|
|
7ca81285b1 | ||
|
|
49a865cdbf | ||
|
|
a0c30c35ed | ||
|
|
de20424885 | ||
|
|
f7e09ae89c | ||
|
|
c6d4600683 | ||
|
|
bd1837ee36 | ||
|
|
544b24ceb1 | ||
|
|
0825374183 | ||
|
|
b053fb8e61 | ||
|
|
ae11e80bde | ||
|
|
8e296231ba | ||
|
|
49891eff36 | ||
|
|
58df525b49 | ||
|
|
1761f43afa | ||
|
|
37d7d938c5 | ||
|
|
b7ca2310b2 | ||
|
|
c562b3969a | ||
|
|
d1d553e8d7 | ||
|
|
b6e7b258e0 | ||
|
|
c7bbe47221 | ||
|
|
347fa9e5cd | ||
|
|
07ce151508 | ||
|
|
665c9b5e52 | ||
|
|
9399a0c545 | ||
|
|
a540b12895 | ||
|
|
e29d9dcd17 | ||
|
|
1aa0d84977 | ||
|
|
7f31d7c812 | ||
|
|
4987a95d8e | ||
|
|
ef7771febb | ||
|
|
12cba0e148 | ||
|
|
c3a4052227 | ||
|
|
82f048ccef | ||
|
|
7a585a3dfb | ||
|
|
8d3b53f51d | ||
|
|
d73e006935 | ||
|
|
9536e60bd1 | ||
|
|
678ca978a3 | ||
|
|
39420877fd | ||
|
|
6ea03cfba3 | ||
|
|
9214b56aea | ||
|
|
699b4c464f | ||
|
|
9fa54a3148 | ||
|
|
f2019b1059 | ||
|
|
714bbd0fb0 | ||
|
|
80777b4663 | ||
|
|
9473c1fe41 | ||
|
|
98f141d62f | ||
|
|
85dd213b14 | ||
|
|
3cf00911f7 | ||
|
|
bddaad8346 | ||
|
|
5df4efd7ba | ||
|
|
59df6aad05 | ||
|
|
c4af4d03ac | ||
|
|
62679819d9 | ||
|
|
46d46e313c | ||
|
|
27e9d3ce47 | ||
|
|
b6b0c5fe6d | ||
|
|
87b934a955 | ||
|
|
8cc6e9b812 | ||
|
|
b7da17ff31 | ||
|
|
2dd3916f7e | ||
|
|
516552cce6 | ||
|
|
2d44881619 | ||
|
|
0acbf30b03 | ||
|
|
e61f6b1c89 | ||
|
|
973a49526f | ||
|
|
e1902d58c1 | ||
|
|
1e53125499 | ||
|
|
91fb7d9e70 | ||
|
|
eb6b42c4c9 | ||
|
|
679d210667 | ||
|
|
1e6252de7f | ||
|
|
48b645ee1e | ||
|
|
951d2e4937 | ||
|
|
1658883b78 | ||
|
|
3514335247 | ||
|
|
55bec31a3f | ||
|
|
0d36abee4e | ||
|
|
caa6e615ff | ||
|
|
cd3576b995 | ||
|
|
324f3f7794 | ||
|
|
3257110673 | ||
|
|
66b444e2b0 | ||
|
|
23860b1ee8 | ||
|
|
101048339b | ||
|
|
dc78e3b7a0 | ||
|
|
d965faa317 | ||
|
|
5d2bfab0f5 | ||
|
|
841a6eaf8c | ||
|
|
db59bb4aa4 | ||
|
|
61f7099106 | ||
|
|
ef828bdd38 | ||
|
|
1134b45b05 | ||
|
|
b06daff4c7 | ||
|
|
cea414b589 | ||
|
|
b8942b728f | ||
|
|
8e6eabd9e8 | ||
|
|
290dd6c4d1 | ||
|
|
088bc16072 | ||
|
|
daad2fbd71 | ||
|
|
3af94c2a90 | ||
|
|
a3ca76d3c4 | ||
|
|
62ede48196 | ||
|
|
390a86a7a7 | ||
|
|
09631d6b0c | ||
|
|
2947238a02 | ||
|
|
a7af4b4809 | ||
|
|
cb6afe67a7 | ||
|
|
8c5b43295d | ||
|
|
79b94824ef | ||
|
|
33e4b046e8 | ||
|
|
386aea7c78 | ||
|
|
4a7bccd983 | ||
|
|
1f0cae9e76 | ||
|
|
87ae085e6d | ||
|
|
640ac2330f | ||
|
|
2543df9425 | ||
|
|
915ab47a72 | ||
|
|
09ff287da2 | ||
|
|
2896180f80 | ||
|
|
4283b5b0e3 | ||
|
|
bbbd3abf06 | ||
|
|
dd88483ec7 | ||
|
|
00ddec7575 | ||
|
|
ab0ebab240 | ||
|
|
79e83e33ec | ||
|
|
aa402d652d | ||
|
|
690481c138 | ||
|
|
881125aa98 | ||
|
|
77eea4d494 | ||
|
|
b26e16abb8 | ||
|
|
f08c7702ee | ||
|
|
a4399c7ef4 | ||
|
|
d6e50ed9a0 | ||
|
|
6b848b318d | ||
|
|
70ce70cf0e | ||
|
|
5a79bc5ee3 | ||
|
|
5792a86959 | ||
|
|
37c5436087 | ||
|
|
6592102e8f | ||
|
|
06a5a8f70d | ||
|
|
ef54ebada6 | ||
|
|
d4f13de1d9 | ||
|
|
4fcedde529 | ||
|
|
a4518bbefc | ||
|
|
95f771aa26 | ||
|
|
da98942cce | ||
|
|
f3efaf59e1 | ||
|
|
d64032f2a3 | ||
|
|
abad8cc4d5 | ||
|
|
0d9536e2f9 | ||
|
|
67b078b805 | ||
|
|
6f02eeae53 | ||
|
|
5bc03b23ea | ||
|
|
3d4acd8c8f | ||
|
|
448bec8181 | ||
|
|
0a34ee1b64 | ||
|
|
34e8e4d4c3 | ||
|
|
32627c03f4 | ||
|
|
63c314ca18 | ||
|
|
e8453783da | ||
|
|
96522ec3a3 | ||
|
|
17dedab391 | ||
|
|
283d2ee09c | ||
|
|
289ff0127e | ||
|
|
931d7217b1 | ||
|
|
3149a3927e | ||
|
|
c57bced7b1 | ||
|
|
8d794a59dc | ||
|
|
7bb1e24bda | ||
|
|
e0013b9b63 | ||
|
|
7d81aab900 | ||
|
|
cc99da5357 | ||
|
|
9bd21e1d1b | ||
|
|
2148aeea42 | ||
|
|
6a7a5505f9 | ||
|
|
5a6faafa30 | ||
|
|
e2f3dad779 | ||
|
|
ae2aa1f5cd | ||
|
|
0e3a17d922 | ||
|
|
033919a270 | ||
|
|
f5d40a0a12 | ||
|
|
0fd781e80a | ||
|
|
a8f6170fdf | ||
|
|
bd8c565415 | ||
|
|
ffb02fe0ec | ||
|
|
114520302c | ||
|
|
317138ab72 | ||
|
|
a990109f43 | ||
|
|
da68b01502 | ||
|
|
e90bc744d9 | ||
|
|
2b5c56484a | ||
|
|
ea30612013 | ||
|
|
4445612bb8 | ||
|
|
da56393078 | ||
|
|
a86ff1e8d0 | ||
|
|
7fce5650b8 | ||
|
|
b949357e66 | ||
|
|
8e71b68dbf | ||
|
|
b77eed221f | ||
|
|
1fa91474d0 | ||
|
|
fc32a89292 | ||
|
|
ea24010298 | ||
|
|
ae1f80a52c | ||
|
|
2f95431c9b | ||
|
|
d59683eff1 | ||
|
|
de77776cd0 | ||
|
|
74ccfea570 | ||
|
|
1f5a549589 | ||
|
|
3ccdc2e307 | ||
|
|
688b725807 | ||
|
|
ae01ea576c | ||
|
|
cd0cd64849 | ||
|
|
aab671398c | ||
|
|
d64aeb25d5 | ||
|
|
aa4fa83127 | ||
|
|
81c3dc1d95 | ||
|
|
746eecf3d8 | ||
|
|
cd948fb576 | ||
|
|
4ec4e19e63 | ||
|
|
d88d197de9 | ||
|
|
2a24918a9c | ||
|
|
d2a85b41e4 | ||
|
|
9bcd991a61 | ||
|
|
1a7bd65e59 | ||
|
|
f8ee929cf7 | ||
|
|
734165f3c7 | ||
|
|
8f9ef1bde8 | ||
|
|
2119c6ab0c | ||
|
|
a85479f73c | ||
|
|
21ba0a9606 | ||
|
|
82ba0f9593 | ||
|
|
7769124401 | ||
|
|
d9c978e7fc | ||
|
|
90899f0928 | ||
|
|
f0421da517 | ||
|
|
573d4bf2cb | ||
|
|
f1e1fd3593 | ||
|
|
c387c5bc34 | ||
|
|
0549bc0b0c | ||
|
|
c2a3e11324 | ||
|
|
107e2ac900 | ||
|
|
3b60bdea14 | ||
|
|
f6591d7820 | ||
|
|
6bbd622066 | ||
|
|
ee79cab7b2 | ||
|
|
a0b94065c5 | ||
|
|
e1fa607f87 | ||
|
|
2e91a359f5 | ||
|
|
9720e703fd | ||
|
|
f88898d5b8 | ||
|
|
30c1597b65 | ||
|
|
5bc5171220 | ||
|
|
d0f118e7af | ||
|
|
c481443667 | ||
|
|
b87cff1289 | ||
|
|
0c080830bc | ||
|
|
cdd7d69c66 | ||
|
|
c1cbcdcc72 | ||
|
|
1c84b19f24 | ||
|
|
2c73dae019 | ||
|
|
d285850a04 | ||
|
|
719ef87efc | ||
|
|
039b9abf5e | ||
|
|
39daf56811 | ||
|
|
a8de2f224f | ||
|
|
dda013844c | ||
|
|
3e9481c5bd | ||
|
|
85ac03a892 | ||
|
|
752699d1c2 | ||
|
|
339ca581ab | ||
|
|
20e9cf1144 | ||
|
|
9737884c4b | ||
|
|
2f5670e830 | ||
|
|
108b18cebf | ||
|
|
814ac22a0d | ||
|
|
3814a218c2 | ||
|
|
3fda90612d | ||
|
|
05762aa50c | ||
|
|
9a31371de0 | ||
|
|
b9b7143185 | ||
|
|
972721bdb5 | ||
|
|
3a48982916 | ||
|
|
6b3a6f9f7d | ||
|
|
48fddecb62 | ||
|
|
7f0d1cc5b3 | ||
|
|
70e82be0b4 | ||
|
|
5fc0809844 | ||
|
|
c79990bcd9 | ||
|
|
cd9e7412ea | ||
|
|
3e472606b5 | ||
|
|
2966bc3204 | ||
|
|
5dc6093eec | ||
|
|
86fc60f5ba | ||
|
|
8189a3f9ab | ||
|
|
76a65e2533 | ||
|
|
ad2914934e | ||
|
|
2769ba2874 | ||
|
|
294f1e178e | ||
|
|
c994d34bb8 | ||
|
|
42b5dd0751 | ||
|
|
f884d6d471 | ||
|
|
abe915a8d6 | ||
|
|
a2e8de2fd8 | ||
|
|
e34ca6a8a5 | ||
|
|
596c9de603 | ||
|
|
4170361a1c | ||
|
|
978a36ea77 | ||
|
|
8745ef7de8 | ||
|
|
2ce929b7b3 | ||
|
|
2dcdfa2862 | ||
|
|
b18daac9c9 | ||
|
|
606041a00d | ||
|
|
4ed5952a81 | ||
|
|
37bfce2eb7 | ||
|
|
59541699ce | ||
|
|
af73c16e37 | ||
|
|
673b09b84b | ||
|
|
09f8edfead | ||
|
|
e1ebdeca4f | ||
|
|
31769aa4af | ||
|
|
b32ad86ce7 | ||
|
|
7350473a65 | ||
|
|
1c87e337d6 | ||
|
|
dec038e7ed | ||
|
|
ebe6f7813e | ||
|
|
706dbfacac | ||
|
|
62ad588aa9 | ||
|
|
3c776e2ab6 | ||
|
|
ab4e2d1ed6 | ||
|
|
5e90c4a142 | ||
|
|
9a7fa25ca3 | ||
|
|
65cb0641f3 | ||
|
|
6f2b891214 | ||
|
|
44d7ab83ec | ||
|
|
b328cc3f6a | ||
|
|
836226a8d3 | ||
|
|
2083f6630a | ||
|
|
25e1e9028f | ||
|
|
a5651c686f | ||
|
|
93f0e0b125 | ||
|
|
730bb6d166 | ||
|
|
855bac59bf | ||
|
|
870900b92a | ||
|
|
a1d5802d36 | ||
|
|
6e16fa3992 | ||
|
|
ee3b49ce59 | ||
|
|
3253dd5010 | ||
|
|
a10d8e0438 | ||
|
|
5151814e98 | ||
|
|
2e2a0f01f4 | ||
|
|
dec9b82da0 | ||
|
|
33f4db5871 | ||
|
|
9a086c0397 | ||
|
|
013ddb8978 | ||
|
|
c4161c5ce6 | ||
|
|
392b8dd5b2 | ||
|
|
d19eb801f3 | ||
|
|
ed416ca555 | ||
|
|
106009316f | ||
|
|
cb10ad3a20 | ||
|
|
2fad0099e4 | ||
|
|
454416b0bd | ||
|
|
dd80b7308b | ||
|
|
fe5c5f1bdc | ||
|
|
79074deb34 | ||
|
|
db0294c95e | ||
|
|
8f0fa88eac | ||
|
|
946f7045e9 | ||
|
|
dda8c90d44 | ||
|
|
701485d76a | ||
|
|
b25198b87e | ||
|
|
ad7e3774c2 | ||
|
|
ee67dea96c | ||
|
|
b3d3bf1593 | ||
|
|
53726fed24 | ||
|
|
7d2ca2c811 | ||
|
|
c0d10a0ec3 | ||
|
|
8e01e3c762 | ||
|
|
04be599893 | ||
|
|
f6cb7fddf1 | ||
|
|
a662283b7d | ||
|
|
f34490fe28 | ||
|
|
1a98c86a37 | ||
|
|
97b5931a4f | ||
|
|
2a4dd9b693 | ||
|
|
ecf31c3675 | ||
|
|
9b0d0a2b7c | ||
|
|
e2ffca2e39 | ||
|
|
a8435879cf | ||
|
|
e16695585a | ||
|
|
a7bcfc6db7 | ||
|
|
21bed98d69 | ||
|
|
044793cc44 | ||
|
|
083a7050b9 | ||
|
|
cc78e19740 | ||
|
|
ee01a07b0a | ||
|
|
ce528b1826 | ||
|
|
92c0543614 | ||
|
|
b6638f4a29 | ||
|
|
96be56a620 | ||
|
|
2e555e420a | ||
|
|
a849951c53 | ||
|
|
435cb04717 | ||
|
|
25a375261e | ||
|
|
1a1a44616f | ||
|
|
e6347a305c | ||
|
|
b137108ae1 | ||
|
|
4ce7ff7cef | ||
|
|
184415a026 | ||
|
|
3977adcb8f | ||
|
|
1c47f98f59 | ||
|
|
9f0c37352e | ||
|
|
7a20fa4805 | ||
|
|
6a713d9e8d | ||
|
|
51b0544752 | ||
|
|
1022a7aa3a | ||
|
|
c048678844 | ||
|
|
f9fb02457c | ||
|
|
ce419ce64e | ||
|
|
ecb6ff57fc | ||
|
|
663866562f | ||
|
|
dfdd0b6cbb | ||
|
|
c81ca82822 | ||
|
|
66b067eb49 | ||
|
|
4e9f2dc46a | ||
|
|
f02d8b61b5 | ||
|
|
088de1877c | ||
|
|
4611abffed | ||
|
|
f206f2f45c | ||
|
|
9c5f5757be | ||
|
|
ec2e0f6fed | ||
|
|
ae0d059b6f | ||
|
|
0132b801b2 | ||
|
|
06ca564ec2 | ||
|
|
d83ce5e633 | ||
|
|
5287319fde | ||
|
|
c75a23e693 | ||
|
|
180bf930c1 | ||
|
|
c16e99ba1e | ||
|
|
b9887fc917 | ||
|
|
96b36bd8ef | ||
|
|
0e32f666eb | ||
|
|
1341fc6a98 | ||
|
|
81c9e96bf3 | ||
|
|
20f91ef893 | ||
|
|
1187254573 | ||
|
|
f22fb4f4a9 | ||
|
|
a14762e005 | ||
|
|
9a91214043 | ||
|
|
30795a46cf | ||
|
|
b0865c8d06 | ||
|
|
c2aabbc6db | ||
|
|
4aad3dafd4 | ||
|
|
62269a9902 | ||
|
|
4bc2fd4a7f | ||
|
|
0e43a08689 | ||
|
|
77b1416413 | ||
|
|
200545822c | ||
|
|
ac33f2164f | ||
|
|
e82ca8a1f4 | ||
|
|
f894f9ef6d | ||
|
|
ccfd8ef566 | ||
|
|
6c6b66c802 | ||
|
|
2cb2e7cb10 | ||
|
|
8744f47a0e | ||
|
|
b742b25f96 | ||
|
|
6d7c20f397 | ||
|
|
f3d384c1dd | ||
|
|
69ce79a7b9 | ||
|
|
1cd191f76b | ||
|
|
afc3c29341 | ||
|
|
fedb3040d1 | ||
|
|
0a969c59d3 | ||
|
|
d9fb9f49fd | ||
|
|
fc77d0d201 | ||
|
|
56700d1d06 | ||
|
|
84259c5116 | ||
|
|
6cc746df8b | ||
|
|
b3ad06692f | ||
|
|
bb0814bb11 | ||
|
|
722faf8aaa | ||
|
|
159324858b | ||
|
|
f73a370719 | ||
|
|
dfc658b910 | ||
|
|
0a52f470b7 | ||
|
|
e4822d8bc4 | ||
|
|
9d1cab3ac2 | ||
|
|
6d16ec238a | ||
|
|
e0284f225b | ||
|
|
a5acd0801a | ||
|
|
d59337ae47 | ||
|
|
2f27a3f839 | ||
|
|
450b33f98a | ||
|
|
9adf5027fa | ||
|
|
1610b84845 | ||
|
|
40b2cf88fc | ||
|
|
55ef6c0c56 | ||
|
|
a9c67ac200 | ||
|
|
3892be47ac | ||
|
|
a2654b2e29 | ||
|
|
a052cc8fa5 | ||
|
|
5d09100cb6 | ||
|
|
76769d1b85 | ||
|
|
2cf57250e8 | ||
|
|
5d0a0c931f | ||
|
|
fa1adfec76 | ||
|
|
9271b07383 | ||
|
|
4fe955650a | ||
|
|
6b401000b9 | ||
|
|
aa681cde88 | ||
|
|
0921c6ea53 | ||
|
|
2df73bedee | ||
|
|
938fe49b2c | ||
|
|
7e2e649a8e | ||
|
|
6e11aa27f8 | ||
|
|
57ee624398 | ||
|
|
3cba95b60a | ||
|
|
bd796ce642 | ||
|
|
3a8473e632 | ||
|
|
89480b61ce | ||
|
|
c5e5bb613e | ||
|
|
b52d1ca6c4 | ||
|
|
790bafbb09 | ||
|
|
c256ce6968 | ||
|
|
85465e0dfb | ||
|
|
b7409c1d0b | ||
|
|
4060d33bbc | ||
|
|
6526d99123 | ||
|
|
1b44c848ad | ||
|
|
e6980be061 | ||
|
|
81c77650b4 | ||
|
|
791e37dd9c | ||
|
|
f926448bbb | ||
|
|
7128f067d0 | ||
|
|
59ab6c1cd8 | ||
|
|
89105b6e72 | ||
|
|
51e43f1c25 | ||
|
|
9b24d644a4 | ||
|
|
2549e0e4c8 | ||
|
|
d974fa638c | ||
|
|
f399b5119f | ||
|
|
12d3fabdb7 | ||
|
|
d5727f5b15 | ||
|
|
1908d04fd4 | ||
|
|
c7f8c828cb | ||
|
|
054d0913fd | ||
|
|
ca7dc69c85 | ||
|
|
b2ff51d99c | ||
|
|
abd4b6bd87 | ||
|
|
b92d07c9a6 | ||
|
|
c6204417a3 | ||
|
|
3a23401443 | ||
|
|
12e6e46924 | ||
|
|
be1b959519 | ||
|
|
7841c9e801 | ||
|
|
bc486e2a7d | ||
|
|
d01885e2ad | ||
|
|
358ffe0601 | ||
|
|
8d0bf1b758 | ||
|
|
c7961cf2f4 | ||
|
|
2ac374e578 | ||
|
|
ad3306e19e | ||
|
|
f54cbac370 | ||
|
|
4bfc0140be | ||
|
|
cc7459b983 | ||
|
|
11415cd517 | ||
|
|
9aa028f32d | ||
|
|
2a415cbd26 | ||
|
|
40a52da2d3 | ||
|
|
31e043c1d7 | ||
|
|
985afafed0 | ||
|
|
3a4d20ecb2 | ||
|
|
35b524f179 | ||
|
|
d2f17ef140 | ||
|
|
d99864ca95 | ||
|
|
e6e1191f8a | ||
|
|
1f10c20874 | ||
|
|
c699f7a940 | ||
|
|
7115c292c7 | ||
|
|
7744b14b48 | ||
|
|
6fed9e73d8 | ||
|
|
e14a3f9c2d | ||
|
|
7bf75b1ff7 | ||
|
|
6905e24199 | ||
|
|
9eb25c09da | ||
|
|
c97f5ffca3 | ||
|
|
eb19168b5f | ||
|
|
e421b68766 | ||
|
|
6b86625c56 | ||
|
|
254b6dd5c7 | ||
|
|
7b545d12c7 | ||
|
|
01ed20b400 | ||
|
|
fad930833a | ||
|
|
9192fbee44 | ||
|
|
aaa5f9885d | ||
|
|
1979b1692d | ||
|
|
fc31eeca88 | ||
|
|
6d4de76a9a | ||
|
|
9245ba4a5b | ||
|
|
973a788c21 | ||
|
|
0d5df8d3cf | ||
|
|
b67a236657 | ||
|
|
170617d457 | ||
|
|
cc2f359b6c | ||
|
|
c0d44a8686 | ||
|
|
a1700f38b1 | ||
|
|
b82ba63532 | ||
|
|
dbd1324a90 | ||
|
|
e52158b729 | ||
|
|
68a717cf47 | ||
|
|
c0a9780f80 | ||
|
|
0520129a40 | ||
|
|
6b36e126ab | ||
|
|
3cb4662363 | ||
|
|
cef14f301f | ||
|
|
eca4a0c440 | ||
|
|
cea04b2ada | ||
|
|
b7d9af9ae7 | ||
|
|
0046b3c802 | ||
|
|
25384c8ac3 | ||
|
|
2eee72da8a | ||
|
|
d65c422e3f | ||
|
|
2ab9bd3172 | ||
|
|
a0b5c2e88c | ||
|
|
0bd4108661 | ||
|
|
7d1a1475ba | ||
|
|
6418263165 | ||
|
|
b32c835cf9 | ||
|
|
a927685bc6 | ||
|
|
6827d40e84 | ||
|
|
9aa12b0eb6 | ||
|
|
8ca1a36d78 | ||
|
|
394f2eae69 | ||
|
|
7588459c3e | ||
|
|
2c33f45a6a | ||
|
|
43cf0b66ac | ||
|
|
c05f6fcff2 | ||
|
|
a4eb32158b | ||
|
|
072ced9d0e | ||
|
|
c5fefc1414 | ||
|
|
c825895ceb | ||
|
|
9488972ed8 | ||
|
|
1f4bed38f7 | ||
|
|
747112957a | ||
|
|
18228096f9 | ||
|
|
d0ea5bf500 | ||
|
|
74fe91fa3a | ||
|
|
d4e3075fcc | ||
|
|
413ae5f20e | ||
|
|
e23f86f542 | ||
|
|
b3c47dbe84 | ||
|
|
b10c2b9a15 | ||
|
|
bb6910acd1 | ||
|
|
76b000820e | ||
|
|
50b874629c | ||
|
|
4d145ab4ad | ||
|
|
4d67a4641f | ||
|
|
bc2eab6f9d | ||
|
|
3ccce75863 | ||
|
|
b390b266e2 | ||
|
|
561af8926c | ||
|
|
5717dc2cf4 | ||
|
|
d0e64ad6d2 | ||
|
|
8e1a735ec7 | ||
|
|
1301d67295 | ||
|
|
7c239f5218 | ||
|
|
61235c953c | ||
|
|
ea65a0308d | ||
|
|
f840b07c02 | ||
|
|
8af719d39e | ||
|
|
c1a4250bd2 | ||
|
|
b4b442f8a8 | ||
|
|
b7e76fcfa1 | ||
|
|
01950cc796 | ||
|
|
14f2022e21 | ||
|
|
23bac73559 | ||
|
|
2de9b94b01 | ||
|
|
05d1736f5f | ||
|
|
db5346fc5c | ||
|
|
f09f5bd737 | ||
|
|
d96a1a5c9e | ||
|
|
ad3cc9c087 | ||
|
|
d0c7bf65a4 | ||
|
|
bb6ac70f63 | ||
|
|
c38b9da7d6 | ||
|
|
19153ca489 | ||
|
|
8e7a5e8cb5 | ||
|
|
1407059937 | ||
|
|
a2911bc9c3 | ||
|
|
f99b735746 | ||
|
|
4510ddda15 | ||
|
|
028ec46f46 | ||
|
|
c0e4e01deb | ||
|
|
cfcf682134 | ||
|
|
27bd9817d7 | ||
|
|
dc389a68ab | ||
|
|
6d982cdfd9 | ||
|
|
827ee615c1 | ||
|
|
f5f0ba721e | ||
|
|
1344f85986 | ||
|
|
e7462ada12 | ||
|
|
11199b6225 | ||
|
|
345aece0f2 | ||
|
|
912c84422b | ||
|
|
b402676079 | ||
|
|
908a5fc60d | ||
|
|
38e57d3635 | ||
|
|
14167b19b9 | ||
|
|
fc548b426d | ||
|
|
094f13934c | ||
|
|
71acd9603e | ||
|
|
341f655ab7 | ||
|
|
0c5323c106 | ||
|
|
7f871fdf30 | ||
|
|
6347b40a02 | ||
|
|
bce84a432a | ||
|
|
b6fc4deb63 | ||
|
|
6b48f9e259 | ||
|
|
44b7666426 | ||
|
|
ef70ed76a2 | ||
|
|
a1a1b3d1ee | ||
|
|
0326757399 | ||
|
|
edbc8ed92b | ||
|
|
07e9ec0617 | ||
|
|
18d0fa4327 | ||
|
|
666ee84033 | ||
|
|
120642f47d | ||
|
|
148b81262d | ||
|
|
c41467f76e | ||
|
|
c83e83b8b1 | ||
|
|
563a508e26 | ||
|
|
1d8347cc23 | ||
|
|
874abf2c41 | ||
|
|
3e18f820ea | ||
|
|
b571f1c953 | ||
|
|
fd69c7ceb8 | ||
|
|
d15faa1890 | ||
|
|
0c7e12c5e7 | ||
|
|
36a3077853 | ||
|
|
51bc23dbd9 | ||
|
|
0b1e0bd395 | ||
|
|
151aafd114 | ||
|
|
16c8a65a1c | ||
|
|
f3133e9cd8 | ||
|
|
de84aa7674 | ||
|
|
1bdbfe86bb | ||
|
|
397b9f439e | ||
|
|
261e0aee0f | ||
|
|
a6d9bde658 | ||
|
|
8d0a07e1a8 | ||
|
|
659615e6f2 | ||
|
|
9f0b82c0d5 | ||
|
|
9a23dcc991 | ||
|
|
980d345305 | ||
|
|
41f76ee974 | ||
|
|
83a60e6303 | ||
|
|
7b23c85117 | ||
|
|
ff3900d100 | ||
|
|
e5e4b8ebfb | ||
|
|
9516b75d65 |
No files matched your search
+1
-2
@@ -97,9 +97,8 @@
|
||||
"Avatar": true,
|
||||
"Avatars": true,
|
||||
"BlazeComponent": false,
|
||||
"BlazeLayout": false,
|
||||
|
||||
"CollectionHooks": false,
|
||||
"DocHead": false,
|
||||
"ESSearchResults": false,
|
||||
"FastRender": false,
|
||||
"FlowRouter": false,
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
name: wekan
|
||||
version: 0
|
||||
version-script: git describe --tags | cut -c 2-
|
||||
summary: The open-source kanban
|
||||
description: |
|
||||
Wekan is an open-source and collaborative kanban board application.
|
||||
|
||||
Whether you’re maintaining a personal todo list, planning your holidays with some friends, or working in a team on your next revolutionary idea, Kanban boards are an unbeatable tool to keep your things organized. They give you a visual overview of the current state of your project, and make you productive by allowing you to focus on the few items that matter the most.
|
||||
Depending on target environment, some configuration settings might need to be adjusted.
|
||||
For full list of configuration options call:
|
||||
$ wekan.help
|
||||
|
||||
confinement: strict
|
||||
grade: stable
|
||||
|
||||
architectures:
|
||||
- amd64
|
||||
|
||||
plugs:
|
||||
mongodb-plug:
|
||||
interface: content
|
||||
target: $SNAP_DATA/shared
|
||||
|
||||
hooks:
|
||||
configure:
|
||||
plugs:
|
||||
- network
|
||||
- network-bind
|
||||
|
||||
slots:
|
||||
mongodb-slot:
|
||||
interface: content
|
||||
write:
|
||||
- $SNAP_DATA/share
|
||||
|
||||
apps:
|
||||
wekan:
|
||||
command: wekan-control
|
||||
daemon: simple
|
||||
plugs: [network, network-bind]
|
||||
|
||||
mongodb:
|
||||
command: mongodb-control
|
||||
daemon: simple
|
||||
plugs: [network, network-bind]
|
||||
|
||||
caddy:
|
||||
command: caddy-control
|
||||
daemon: simple
|
||||
plugs: [network, network-bind]
|
||||
|
||||
help:
|
||||
command: wekan-help
|
||||
|
||||
database-backup:
|
||||
command: mongodb-backup
|
||||
plugs: [network, network-bind]
|
||||
|
||||
database-list-backups:
|
||||
command: ls -al $SNAP_COMMON/db-backups/
|
||||
|
||||
database-restore:
|
||||
command: mongodb-restore
|
||||
plugs: [network, network-bind]
|
||||
|
||||
parts:
|
||||
mongodb:
|
||||
source: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-4.2.6.tgz
|
||||
plugin: dump
|
||||
stage-packages: [libssl1.0.0, libcurl3]
|
||||
filesets:
|
||||
mongo:
|
||||
- usr
|
||||
- bin
|
||||
- lib
|
||||
stage:
|
||||
- $mongo
|
||||
prime:
|
||||
- $mongo
|
||||
|
||||
wekan:
|
||||
source: .
|
||||
plugin: nodejs
|
||||
node-engine: 14.21.3
|
||||
node-packages:
|
||||
- node-gyp
|
||||
- node-pre-gyp
|
||||
- fibers
|
||||
build-packages:
|
||||
- ca-certificates
|
||||
- apt-utils
|
||||
- python
|
||||
- python3
|
||||
- g++
|
||||
- capnproto
|
||||
- curl
|
||||
- libcurl3
|
||||
- execstack
|
||||
- nodejs
|
||||
- npm
|
||||
stage-packages:
|
||||
- libfontconfig1
|
||||
override-build: |
|
||||
echo "Cleaning environment first"
|
||||
rm -rf ~/.meteor ~/.npm /usr/local/lib/node_modules
|
||||
# Create the OpenAPI specification
|
||||
rm -rf .build
|
||||
## Use Meteor 1.8.x on Snap
|
||||
#rm -rf .meteor
|
||||
#mv .snap-meteor-1.8/.meteor .
|
||||
#mv .snap-meteor-1.8/package.json .
|
||||
#mv .snap-meteor-1.8/package-lock.json .
|
||||
## Meteor 1.9.x has changes to Buffer() => Buffer.alloc(), so reverting those
|
||||
#mv .snap-meteor-1.8/cfs_access-point.txt fix-download-unicode/
|
||||
#mv .snap-meteor-1.8/export.js models/
|
||||
#mv .snap-meteor-1.8/wekanCreator.js models/
|
||||
#mv .snap-meteor-1.8/ldap.js packages/wekan-ldap/server/ldap.js
|
||||
#mv .snap-meteor-1.8/oidc_server.js packages/wekan-oidc/oidc_server.js
|
||||
rm -rf .snap-meteor-1.8
|
||||
#mkdir -p .build/python
|
||||
#cd .build/python
|
||||
#git clone --depth 1 -b master https://github.com/Kronuz/esprima-python
|
||||
#cd esprima-python
|
||||
#python3 setup.py install
|
||||
#cd ../../..
|
||||
#mkdir -p ./public/api
|
||||
#python3 ./openapi/generate_openapi.py --release $(git describe --tags --abbrev=0) > ./public/api/wekan.yml
|
||||
# we temporary need api2html and mkdirp
|
||||
#npm install -g api2html@0.3.0
|
||||
#npm install -g mkdirp
|
||||
#api2html -c ./public/logo-header.png -o ./public/api/wekan.html ./public/api/wekan.yml
|
||||
#npm uninstall -g mkdirp
|
||||
#npm uninstall -g api2html
|
||||
# Node Fibers 100% CPU usage issue:
|
||||
# https://github.com/wekan/wekan-mongodb/issues/2#issuecomment-381453161
|
||||
# https://github.com/meteor/meteor/issues/9796#issuecomment-381676326
|
||||
# https://github.com/sandstorm-io/sandstorm/blob/0f1fec013fe7208ed0fd97eb88b31b77e3c61f42/shell/server/00-startup.js#L99-L129
|
||||
# Also see beginning of wekan/server/authentication.js
|
||||
# import Fiber from "fibers";
|
||||
# Fiber.poolSize = 1e9;
|
||||
# OLD: Download node version 8.12.0 prerelease build => Official node 8.12.0 has been released
|
||||
# Description at https://releases.wekan.team/node.txt
|
||||
##echo "375bd8db50b9c692c0bbba6e96d4114cd29bee3770f901c1ff2249d1038f1348 node" >> node-SHASUMS256.txt.asc
|
||||
##curl https://releases.wekan.team/node -o node
|
||||
# Verify Fibers patched node authenticity
|
||||
##echo "Fibers 100% CPU issue patched node authenticity:"
|
||||
##grep node node-SHASUMS256.txt.asc | shasum -a 256 -c -
|
||||
##rm -f node-SHASUMS256.txt.asc
|
||||
##chmod +x node
|
||||
##mv node `which node`
|
||||
# DOES NOT WORK: paxctl fix.
|
||||
# Removed from build-packages: - paxctl
|
||||
#echo "Applying paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303"
|
||||
#paxctl -mC `which node`
|
||||
#echo "Installing npm"
|
||||
#curl -L https://www.npmjs.com/install.sh | sh
|
||||
echo "Installing meteor"
|
||||
curl https://install.meteor.com/ -o install_meteor.sh
|
||||
#sed -i "s|RELEASE=.*|RELEASE=\"1.8.1-beta.0\"|g" install_meteor.sh
|
||||
chmod +x install_meteor.sh
|
||||
sh install_meteor.sh
|
||||
rm install_meteor.sh
|
||||
# REPOS BELOW ARE INCLUDED TO WEKAN REPO
|
||||
#if [ ! -d "packages" ]; then
|
||||
# mkdir packages
|
||||
#fi
|
||||
#if [ ! -d "packages/kadira-flow-router" ]; then
|
||||
# cd packages
|
||||
# git clone --depth 1 -b master https://github.com/wekan/flow-router.git kadira-flow-router
|
||||
# cd ..
|
||||
#fi
|
||||
#if [ ! -d "packages/meteor-useraccounts-core" ]; then
|
||||
# cd packages
|
||||
# git clone --depth 1 -b master https://github.com/meteor-useraccounts/core.git meteor-useraccounts-core
|
||||
# sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' meteor-useraccounts-core/package.js
|
||||
# cd ..
|
||||
#fi
|
||||
#if [ ! -d "packages/meteor-accounts-cas" ]; then
|
||||
# cd packages
|
||||
# git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-cas.git meteor-accounts-cas
|
||||
# cd ..
|
||||
#fi
|
||||
#if [ ! -d "packages/wekan-ldap" ]; then
|
||||
# cd packages
|
||||
# git clone --depth 1 -b master https://github.com/wekan/wekan-ldap.git
|
||||
# cd ..
|
||||
#fi
|
||||
#if [ ! -d "packages/wekan-scrollbar" ]; then
|
||||
# cd packages
|
||||
# git clone --depth 1 -b master https://github.com/wekan/wekan-scrollbar.git
|
||||
# cd ..
|
||||
#fi
|
||||
#if [ ! -d "packages/wekan_accounts-oidc" ]; then
|
||||
# cd packages
|
||||
# git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-oidc.git
|
||||
# mv meteor-accounts-oidc/packages/switch_accounts-oidc wekan-accounts-oidc
|
||||
# mv meteor-accounts-oidc/packages/switch_oidc wekan-oidc
|
||||
# rm -rf meteor-accounts-oidc
|
||||
# cd ..
|
||||
#fi
|
||||
#if [ ! -d "packages/markdown" ]; then
|
||||
# cd packages
|
||||
# git clone --depth 1 -b master --recurse-submodules https://github.com/wekan/markdown.git
|
||||
# cd ..
|
||||
#fi
|
||||
rm -rf .build
|
||||
meteor add standard-minifier-js --allow-superuser
|
||||
meteor npm install --allow-superuser
|
||||
meteor npm install --allow-superuser --save babel-runtime
|
||||
meteor build .build --directory --allow-superuser
|
||||
cp -f fix-download-unicode/cfs_access-point.txt .build/bundle/programs/server/packages/cfs_access-point.js
|
||||
#Removed binary version of bcrypt because of security vulnerability that is not fixed yet.
|
||||
#https://github.com/wekan/wekan/commit/4b2010213907c61b0e0482ab55abb06f6a668eac
|
||||
#https://github.com/wekan/wekan/commit/7eeabf14be3c63fae2226e561ef8a0c1390c8d3c
|
||||
#cd .build/bundle/programs/server/npm/node_modules/meteor/npm-bcrypt
|
||||
#rm -rf node_modules/bcrypt
|
||||
#meteor npm install --save bcrypt
|
||||
# Change from npm-bcrypt directory back to .build/bundle/programs/server directory.
|
||||
#cd ../../../../
|
||||
# Change to directory .build/bundle/programs/server
|
||||
cd .build/bundle/programs/server
|
||||
npm install
|
||||
npm install --allow-superuser --save babel-runtime
|
||||
#meteor npm install --save bcrypt
|
||||
# Change back to Wekan source directory
|
||||
cd ../../../..
|
||||
cp -r .build/bundle/* $SNAPCRAFT_PART_INSTALL/
|
||||
cp .build/bundle/.node_version.txt $SNAPCRAFT_PART_INSTALL/
|
||||
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/wekan
|
||||
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs
|
||||
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/tar/lib/.mkdir.js.swp
|
||||
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
|
||||
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-gyp/node_modules/tar/lib/.mkdir.js.swp
|
||||
# Meteor 1.8.x additional .swp remove
|
||||
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
|
||||
|
||||
organize:
|
||||
README: README.wekan
|
||||
prime:
|
||||
- -lib/node_modules/node-pre-gyp/node_modules/tar/lib/.unpack.js.swp
|
||||
|
||||
helpers:
|
||||
source: snap-src
|
||||
plugin: dump
|
||||
|
||||
caddy:
|
||||
plugin: dump
|
||||
source: https://caddyserver.com/download/linux/amd64?license=personal&telemetry=off
|
||||
source-type: tar
|
||||
organize:
|
||||
caddy: bin/caddy
|
||||
CHANGES.txt: CADDY_CHANGES.txt
|
||||
EULA.txt: CADDY_EULA.txt
|
||||
LICENSES.txt: CADDY_LICENSES.txt
|
||||
README.txt: CADDY_README.txt
|
||||
stage:
|
||||
- -init
|
||||
@@ -1,198 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Note: If you use other locale than en_US.UTF-8 , you need to additionally install en_US.UTF-8"
|
||||
echo " with 'sudo dpkg-reconfigure locales' , so that MongoDB works correctly."
|
||||
echo " You can still use any other locale as your main locale."
|
||||
|
||||
#Below script installs newest node 8.x for Debian/Ubuntu/Mint.
|
||||
#NODE_VERSION=12.21.0
|
||||
#X64NODE="https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz"
|
||||
|
||||
function pause(){
|
||||
read -p "$*"
|
||||
}
|
||||
|
||||
function cprec(){
|
||||
if [[ -d "$1" ]]; then
|
||||
if [[ ! -d "$2" ]]; then
|
||||
sudo mkdir -p "$2"
|
||||
fi
|
||||
|
||||
for i in $(ls -A "$1"); do
|
||||
cprec "$1/$i" "$2/$i"
|
||||
done
|
||||
else
|
||||
sudo cp "$1" "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
# sudo npm doesn't work right, so this is a workaround
|
||||
function npm_call(){
|
||||
TMPDIR="/tmp/tmp_npm_prefix"
|
||||
if [[ -d "$TMPDIR" ]]; then
|
||||
rm -rf $TMPDIR
|
||||
fi
|
||||
mkdir $TMPDIR
|
||||
NPM_PREFIX="$(npm config get prefix)"
|
||||
npm config set prefix $TMPDIR
|
||||
npm "$@"
|
||||
npm config set prefix "$NPM_PREFIX"
|
||||
|
||||
echo "Moving files to $NPM_PREFIX"
|
||||
for i in $(ls -A $TMPDIR); do
|
||||
cprec "$TMPDIR/$i" "$NPM_PREFIX/$i"
|
||||
done
|
||||
rm -rf $TMPDIR
|
||||
}
|
||||
|
||||
#function wekan_repo_check(){
|
||||
## UNCOMMENTING, IT'S NOT REQUIRED THAT /HOME/USERNAME IS /HOME/WEKAN
|
||||
# git_remotes="$(git remote show 2>/dev/null)"
|
||||
# res=""
|
||||
# for i in $git_remotes; do
|
||||
# res="$(git remote get-url $i | sed 's/.*wekan\/wekan.*/wekan\/wekan/')"
|
||||
# if [[ "$res" == "wekan/wekan" ]]; then
|
||||
# break
|
||||
# fi
|
||||
# done
|
||||
#
|
||||
# if [[ "$res" != "wekan/wekan" ]]; then
|
||||
# echo "$PWD is not a wekan repository"
|
||||
# exit;
|
||||
# fi
|
||||
#}
|
||||
|
||||
echo
|
||||
PS3='Please enter your choice: '
|
||||
options=("Install Wekan dependencies" "Build Wekan" "Run Meteor for dev on http://localhost:4000" "Run Meteor for dev on http://CURRENT-IP-ADDRESS:4000" "Run Meteor for dev on http://CUSTOM-IP-ADDRESS:PORT" "Quit")
|
||||
|
||||
select opt in "${options[@]}"
|
||||
do
|
||||
case $opt in
|
||||
"Install Wekan dependencies")
|
||||
|
||||
if [[ "$OSTYPE" == "linux-gnu" ]]; then
|
||||
echo "Linux";
|
||||
# Debian, Ubuntu, Mint
|
||||
sudo apt-get install -y build-essential gcc g++ make git curl wget
|
||||
# npm nodejs
|
||||
#sudo npm -g install npm
|
||||
curl -0 -L https://npmjs.org/install.sh | sudo sh
|
||||
sudo chown -R $(id -u):$(id -g) $HOME/.npm
|
||||
sudo npm -g install n
|
||||
sudo n 12.21.0
|
||||
#curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
|
||||
#sudo apt-get install -y nodejs
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
echo "macOS";
|
||||
pause '1) Install XCode 2) Install Node 8.x from https://nodejs.org/en/ 3) Press [Enter] key to continue.'
|
||||
elif [[ "$OSTYPE" == "cygwin" ]]; then
|
||||
# POSIX compatibility layer and Linux environment emulation for Windows
|
||||
echo "TODO: Add Cygwin";
|
||||
exit;
|
||||
elif [[ "$OSTYPE" == "msys" ]]; then
|
||||
# Lightweight shell and GNU utilities compiled for Windows (part of MinGW)
|
||||
echo "TODO: Add msys on Windows";
|
||||
exit;
|
||||
elif [[ "$OSTYPE" == "win32" ]]; then
|
||||
# I'm not sure this can happen.
|
||||
echo "TODO: Add Windows";
|
||||
exit;
|
||||
elif [[ "$OSTYPE" == "freebsd"* ]]; then
|
||||
echo "TODO: Add FreeBSD";
|
||||
exit;
|
||||
else
|
||||
echo "Unknown"
|
||||
echo ${OSTYPE}
|
||||
exit;
|
||||
fi
|
||||
|
||||
## Latest npm with Meteor 1.8.x
|
||||
npm_call -g install npm
|
||||
npm_call -g install node-gyp
|
||||
# Latest fibers for Meteor 1.8.x
|
||||
sudo mkdir -p /usr/local/lib/node_modules/fibers/.node-gyp
|
||||
npm_call -g install fibers
|
||||
# Install Meteor, if it's not yet installed
|
||||
curl https://install.meteor.com | bash
|
||||
sudo chown -R $(id -u):$(id -g) $HOME/.npm $HOME/.meteor
|
||||
break
|
||||
;;
|
||||
|
||||
"Build Wekan")
|
||||
echo "Building Wekan."
|
||||
#wekan_repo_check
|
||||
# REPOS BELOW ARE INCLUDED TO WEKAN REPO
|
||||
#rm -rf packages/kadira-flow-router packages/meteor-useraccounts-core packages/meteor-accounts-cas packages/wekan-ldap packages/wekan-ldap packages/wekan-scrfollbar packages/meteor-accounts-oidc packages/markdown
|
||||
#mkdir packages
|
||||
#cd packages
|
||||
#git clone --depth 1 -b master https://github.com/wekan/flow-router.git kadira-flow-router
|
||||
#git clone --depth 1 -b master https://github.com/meteor-useraccounts/core.git meteor-useraccounts-core
|
||||
#git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-cas.git
|
||||
#git clone --depth 1 -b master https://github.com/wekan/wekan-ldap.git
|
||||
#git clone --depth 1 -b master https://github.com/wekan/wekan-scrollbar.git
|
||||
#git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-oidc.git
|
||||
#git clone --depth 1 -b master --recurse-submodules https://github.com/wekan/markdown.git
|
||||
#mv meteor-accounts-oidc/packages/switch_accounts-oidc wekan_accounts-oidc
|
||||
#mv meteor-accounts-oidc/packages/switch_oidc wekan_oidc
|
||||
#rm -rf meteor-accounts-oidc
|
||||
#if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# echo "sed at macOS";
|
||||
# sed -i '' 's/api\.versionsFrom/\/\/api.versionsFrom/' ~/repos/wekan/packages/meteor-useraccounts-core/package.js
|
||||
#else
|
||||
# echo "sed at ${OSTYPE}"
|
||||
# sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' ~/repos/wekan/packages/meteor-useraccounts-core/package.js
|
||||
#fi
|
||||
#cd ..
|
||||
sudo chown -R $(id -u):$(id -g) $HOME/.npm $HOME/.meteor
|
||||
rm -rf node_modules .meteor/local
|
||||
npm install
|
||||
rm -rf .build
|
||||
meteor build .build --directory
|
||||
cp -f fix-download-unicode/cfs_access-point.txt .build/bundle/programs/server/packages/cfs_access-point.js
|
||||
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
||||
rm -rf .build/bundle/programs/web.browser.legacy
|
||||
#Removed binary version of bcrypt because of security vulnerability that is not fixed yet.
|
||||
#https://github.com/wekan/wekan/commit/4b2010213907c61b0e0482ab55abb06f6a668eac
|
||||
#https://github.com/wekan/wekan/commit/7eeabf14be3c63fae2226e561ef8a0c1390c8d3c
|
||||
#cd ~/repos/wekan/.build/bundle/programs/server/npm/node_modules/meteor/npm-bcrypt
|
||||
#rm -rf node_modules/bcrypt
|
||||
#meteor npm install bcrypt
|
||||
cd .build/bundle/programs/server
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
#meteor npm install bcrypt
|
||||
cd ../../../..
|
||||
echo Done.
|
||||
break
|
||||
;;
|
||||
|
||||
"Run Meteor for dev on http://localhost:4000")
|
||||
WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://localhost:4000 meteor run --exclude-archs web.browser.legacy,web.cordova --port 4000
|
||||
break
|
||||
;;
|
||||
|
||||
"Run Meteor for dev on http://CURRENT-IP-ADDRESS:4000")
|
||||
IPADDRESS=$(ip a | grep 'noprefixroute' | grep 'inet ' | cut -d: -f2 | awk '{ print $2}' | cut -d '/' -f 1)
|
||||
echo "Your IP address is $IPADDRESS"
|
||||
WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://$IPADDRESS:4000 meteor run --exclude-archs web.browser.legacy,web.cordova --port 4000
|
||||
break
|
||||
;;
|
||||
|
||||
"Run Meteor for dev on http://CUSTOM-IP-ADDRESS:PORT")
|
||||
ip address
|
||||
echo "From above list, what is your IP address?"
|
||||
read IPADDRESS
|
||||
echo "On what port you would like to run Wekan?"
|
||||
read PORT
|
||||
echo "ROOT_URL=http://$IPADDRESS:$PORT"
|
||||
WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://$IPADDRESS:$PORT meteor run --exclude-archs web.browser.legacy,web.cordova --port $PORT
|
||||
break
|
||||
;;
|
||||
|
||||
"Quit")
|
||||
break
|
||||
;;
|
||||
*) echo invalid option;;
|
||||
esac
|
||||
done
|
||||
@@ -1,155 +0,0 @@
|
||||
name: wekan
|
||||
version: git
|
||||
summary: The open-source kanban
|
||||
description: |
|
||||
Wekan is an open-source and collaborative kanban board application.
|
||||
|
||||
Whether you’re maintaining a personal todo list, planning your holidays with some friends, or working in a team on your next revolutionary idea, Kanban boards are an unbeatable tool to keep your things organized. They give you a visual overview of the current state of your project, and make you productive by allowing you to focus on the few items that matter the most.
|
||||
Depending on target environment, some configuration settings might need to be adjusted.
|
||||
For full list of configuration options call:
|
||||
$ wekan.help
|
||||
|
||||
confinement: strict
|
||||
grade: stable
|
||||
base: core18
|
||||
|
||||
architectures:
|
||||
- amd64
|
||||
|
||||
plugs:
|
||||
mongodb-plug:
|
||||
interface: content
|
||||
target: $SNAP_DATA/shared
|
||||
|
||||
hooks:
|
||||
configure:
|
||||
plugs:
|
||||
- network
|
||||
- network-bind
|
||||
|
||||
slots:
|
||||
mongodb-slot:
|
||||
interface: content
|
||||
write:
|
||||
- $SNAP_DATA/share
|
||||
|
||||
apps:
|
||||
wekan:
|
||||
command: wekan-control
|
||||
daemon: simple
|
||||
plugs: [network, network-bind]
|
||||
|
||||
mongodb:
|
||||
command: mongodb-control
|
||||
daemon: simple
|
||||
plugs: [network, network-bind]
|
||||
|
||||
caddy:
|
||||
command: caddy-control
|
||||
daemon: simple
|
||||
plugs: [network, network-bind]
|
||||
|
||||
help:
|
||||
command: wekan-help
|
||||
|
||||
database-backup:
|
||||
command: mongodb-backup
|
||||
plugs: [network, network-bind]
|
||||
|
||||
database-list-backups:
|
||||
command: ls -al $SNAP_COMMON/db-backups/
|
||||
|
||||
database-restore:
|
||||
command: mongodb-restore
|
||||
plugs: [network, network-bind]
|
||||
|
||||
parts:
|
||||
mongodb:
|
||||
source: https://repo.mongodb.org/apt/ubuntu/dists/xenial/mongodb-org/4.2/multiverse/binary-amd64/mongodb-org-server_4.2.2_amd64.deb
|
||||
#https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-4.0.14.tgz
|
||||
#https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.2.22.tgz
|
||||
plugin: dump
|
||||
stage-packages: [libssl1.0.0, libcurl3]
|
||||
filesets:
|
||||
mongo:
|
||||
- usr
|
||||
- bin
|
||||
- lib
|
||||
stage:
|
||||
- $mongo
|
||||
prime:
|
||||
- $mongo
|
||||
|
||||
wekan:
|
||||
source: .
|
||||
plugin: nodejs
|
||||
node-engine: 14.19.0
|
||||
node-packages:
|
||||
- node-gyp
|
||||
- node-pre-gyp
|
||||
- fibers
|
||||
build-packages:
|
||||
- ca-certificates
|
||||
- apt-utils
|
||||
- build-essential
|
||||
- python
|
||||
- python3
|
||||
- g++
|
||||
- capnproto
|
||||
- curl
|
||||
- libcurl3
|
||||
- execstack
|
||||
- nodejs
|
||||
- npm
|
||||
stage-packages:
|
||||
- libfontconfig1
|
||||
override-build: |
|
||||
echo "Cleaning environment first"
|
||||
rm -rf ~/.meteor ~/.npm /usr/local/lib/node_modules
|
||||
rm -rf .build
|
||||
echo "Installing meteor"
|
||||
curl https://install.meteor.com/ -o install_meteor.sh
|
||||
chmod +x install_meteor.sh
|
||||
sh install_meteor.sh
|
||||
rm install_meteor.sh
|
||||
rm -rf .build
|
||||
meteor add standard-minifier-js --allow-superuser
|
||||
meteor npm install --allow-superuser
|
||||
meteor npm install --allow-superuser --save babel-runtime
|
||||
meteor build .build --directory --allow-superuser
|
||||
cp -f fix-download-unicode/cfs_access-point.txt .build/bundle/programs/server/packages/cfs_access-point.js
|
||||
cd .build/bundle/programs/server
|
||||
npm install
|
||||
npm install --allow-superuser --save babel-runtime
|
||||
# Change back to Wekan source directory
|
||||
cd ../../../..
|
||||
cp -r .build/bundle/* $SNAPCRAFT_PART_INSTALL/
|
||||
cp .build/bundle/.node_version.txt $SNAPCRAFT_PART_INSTALL/
|
||||
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/wekan
|
||||
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs
|
||||
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/tar/lib/.mkdir.js.swp
|
||||
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
|
||||
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-gyp/node_modules/tar/lib/.mkdir.js.swp
|
||||
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
|
||||
|
||||
organize:
|
||||
README: README.wekan
|
||||
prime:
|
||||
- -lib/node_modules/node-pre-gyp/node_modules/tar/lib/.unpack.js.swp
|
||||
|
||||
helpers:
|
||||
source: snap-src
|
||||
plugin: dump
|
||||
|
||||
caddy:
|
||||
plugin: dump
|
||||
source: https://caddyserver.com/download/linux/amd64?license=personal&telemetry=off
|
||||
source-type: tar
|
||||
organize:
|
||||
caddy: bin/caddy
|
||||
CHANGES.txt: CADDY_CHANGES.txt
|
||||
EULA.txt: CADDY_EULA.txt
|
||||
LICENSES.txt: CADDY_LICENSES.txt
|
||||
README.txt: CADDY_README.txt
|
||||
stage:
|
||||
- -init
|
||||
@@ -1,183 +0,0 @@
|
||||
name: wekan
|
||||
version: '6.21'
|
||||
base: core20
|
||||
summary: Open Source kanban
|
||||
description: |
|
||||
WeKan ® is an Open Source and collaborative kanban board application.
|
||||
|
||||
Whether you’re maintaining a personal todo list, planning your holidays with some friends, or working in a team on your next revolutionary idea, Kanban boards are an unbeatable tool to keep your things organized. They give you a visual overview of the current state of your project, and make you productive by allowing you to focus on the few items that matter the most.
|
||||
Depending on target environment, some configuration settings might need to be adjusted.
|
||||
For full list of configuration options call:
|
||||
$ wekan.help
|
||||
|
||||
confinement: strict
|
||||
grade: stable
|
||||
|
||||
architectures:
|
||||
- build-on: amd64
|
||||
run-on: amd64
|
||||
|
||||
- build-on: arm64
|
||||
run-on: arm64
|
||||
|
||||
- build-on: ppc64el
|
||||
run-on: ppc64el
|
||||
|
||||
- build-on: s390x
|
||||
run-on: s390x
|
||||
|
||||
plugs:
|
||||
mongodb-plug:
|
||||
interface: content
|
||||
target: $SNAP_DATA/shared
|
||||
|
||||
hooks:
|
||||
configure:
|
||||
plugs:
|
||||
- network
|
||||
- network-bind
|
||||
|
||||
slots:
|
||||
mongodb-slot:
|
||||
interface: content
|
||||
write:
|
||||
- $SNAP_DATA/share
|
||||
|
||||
apps:
|
||||
wekan:
|
||||
command: wekan-control
|
||||
daemon: simple
|
||||
plugs: [network, network-bind, mount-observe, system-observe, bluetooth-control]
|
||||
restart-condition: on-failure
|
||||
|
||||
mongodb:
|
||||
command: mongodb-control
|
||||
daemon: simple
|
||||
plugs: [network, network-bind, mount-observe, system-observe, bluetooth-control]
|
||||
restart-condition: on-failure
|
||||
|
||||
caddy:
|
||||
command: caddy-control
|
||||
daemon: simple
|
||||
plugs: [network, network-bind]
|
||||
|
||||
help:
|
||||
command: wekan-help
|
||||
|
||||
database-backup:
|
||||
command: mongodb-backup
|
||||
plugs: [network, network-bind, mount-observe, system-observe, bluetooth-control]
|
||||
|
||||
database-list-backups:
|
||||
command: ls -al $SNAP_COMMON/db-backups/
|
||||
|
||||
database-restore:
|
||||
command: mongodb-restore
|
||||
plugs: [network, network-bind, mount-observe, system-observe, bluetooth-control]
|
||||
|
||||
parts:
|
||||
mongodb:
|
||||
plugin: dump
|
||||
source:
|
||||
- on amd64: https://repo.mongodb.org/apt/ubuntu/dists/focal/mongodb-org/4.4/multiverse/binary-amd64/mongodb-org-server_4.4.13_amd64.deb
|
||||
- on arm64: https://repo.mongodb.org/apt/ubuntu/dists/focal/mongodb-org/4.4/multiverse/binary-arm64/mongodb-org-server_4.4.13_arm64.deb
|
||||
- on ppc64el: https://repo.mongodb.org/apt/ubuntu/dists/focal/mongodb-org/4.4/multiverse/binary-ppc64el/mongodb-org-server_4.4.13_ppc64el.deb
|
||||
- on s390x: https://repo.mongodb.org/apt/ubuntu/dists/focal/mongodb-org/4.4/multiverse/binary-s390x/mongodb-org-server_4.4.13_s390x.deb
|
||||
stage-packages:
|
||||
- libssl1.1
|
||||
- libcurl3-dev
|
||||
- libcurl4-openssl-dev
|
||||
filesets:
|
||||
mongo:
|
||||
- usr
|
||||
- bin
|
||||
- lib
|
||||
stage:
|
||||
- $mongo
|
||||
prime:
|
||||
- $mongo
|
||||
|
||||
wekan:
|
||||
#plugin: npm
|
||||
plugin: dump
|
||||
source:
|
||||
# Fixed URLs to some allowed GitHub releases URL.
|
||||
# Non-GitHub build server file urls are not allowed at 2022-03-02 and later.
|
||||
- on amd64: https://github.com/wekan/wekan/releases/download/v6.20/wekan-6.20-amd64.zip
|
||||
- on arm64: https://github.com/wekan/wekan/releases/download/v6.20/wekan-6.20-arm64.zip
|
||||
- on ppc64el: https://github.com/wekan/wekan/releases/download/v6.20/wekan-6.20-ppc64el.zip
|
||||
- on s390x: https://github.com/wekan/wekan/releases/download/v6.20/wekan-6.20-s390x.zip
|
||||
# npm-node-version: 14.19.1
|
||||
# node-packages:
|
||||
# - node-gyp
|
||||
# - node-pre-gyp
|
||||
# - fibers
|
||||
# build-packages:
|
||||
# - npm
|
||||
# - build-essential
|
||||
# - ca-certificates
|
||||
# - apt-utils
|
||||
# - python
|
||||
# - python3
|
||||
# - g++
|
||||
# - capnproto
|
||||
# - curl
|
||||
# - execstack
|
||||
# - nodejs
|
||||
# - npm
|
||||
# - p7zip-full
|
||||
# stage-packages:
|
||||
# - libfontconfig1
|
||||
override-build: |
|
||||
cp -r bundle/* $SNAPCRAFT_PART_INSTALL/
|
||||
cp bundle/.node_version.txt $SNAPCRAFT_PART_INSTALL/
|
||||
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/wekan
|
||||
snapcraftctl build
|
||||
organize:
|
||||
README: README.wekan
|
||||
prime:
|
||||
- -lib/node_modules/node-pre-gyp/node_modules/tar/lib/.unpack.js.swp
|
||||
- -lib/node_modules/weka*
|
||||
|
||||
helpers:
|
||||
source: snap-src
|
||||
plugin: dump
|
||||
|
||||
caddy:
|
||||
plugin: dump
|
||||
## Caddy v1 is not developed anymore. TODO: Sometime migrate to Caddy v2.
|
||||
## https://caddy.community/t/caddyfile-v1-adapter/9129
|
||||
## https://github.com/caddyserver/caddy/tree/v1
|
||||
#source: https://caddyserver.com/download/linux/amd64?license=personal&telemetry=off
|
||||
#source-type: tar
|
||||
# Using last working binary that was downloaded from above URL to Wekan Snap,
|
||||
# and .txt files from https://github.com/caddyserver/caddy/tree/v1/dist
|
||||
source: https://wekan.github.io/caddy-v1-linux-amd64.7z
|
||||
source-type: 7z
|
||||
organize:
|
||||
caddy: bin/caddy
|
||||
CHANGES.txt: CADDY_CHANGES.txt
|
||||
EULA.txt: CADDY_EULA.txt
|
||||
LICENSES.txt: CADDY_LICENSES.txt
|
||||
README.txt: CADDY_README.txt
|
||||
stage:
|
||||
- -init
|
||||
|
||||
caddy2:
|
||||
plugin: dump
|
||||
source:
|
||||
# Fixed URLs to some allowed GitHub releases URL.
|
||||
# Non-GitHub build server file urls are not allowed at 2022-03-02 and later.
|
||||
- on amd64: https://github.com/wekan/wekan/releases/download/v6.20/caddy-v2-amd64.zip
|
||||
- on arm64: https://github.com/wekan/wekan/releases/download/v6.20/caddy-v2-arm64.zip
|
||||
- on ppc64el: https://github.com/wekan/wekan/releases/download/v6.20/caddy-v2-ppc64el.zip
|
||||
- on s390x: https://github.com/wekan/wekan/releases/download/v6.20/caddy-v2-s390x.zip
|
||||
source-type: zip
|
||||
organize:
|
||||
caddy: bin/caddy
|
||||
CHANGES.txt: CADDY_CHANGES.txt
|
||||
EULA.txt: CADDY_EULA.txt
|
||||
LICENSES.txt: CADDY_LICENSES.txt
|
||||
README.txt: CADDY_README.txt
|
||||
stage:
|
||||
- -init
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
custom: ['https://wekan.team/commercial-support/']
|
||||
github: wekan
|
||||
custom: ['https://wekan.fi/commercial-support/']
|
||||
@@ -1,55 +0,0 @@
|
||||
## Issue
|
||||
|
||||
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 UPGRADE](https://github.com/wekan/wekan/wiki/Backup)** to the newest
|
||||
WeKan ® before reporting an issue, if possible.
|
||||
|
||||
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,69 @@
|
||||
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: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform / Installation Method
|
||||
options:
|
||||
- Snap Stable
|
||||
- Snap Candidate
|
||||
- Docker
|
||||
- Sandstorm
|
||||
- Source (Meteor)
|
||||
- Windows
|
||||
- Mac
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: CPU
|
||||
attributes:
|
||||
label: CPU
|
||||
options:
|
||||
- amd64
|
||||
- arm64
|
||||
- s390x
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
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@v4
|
||||
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@v4
|
||||
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@9780b0c442fbb1117ed29e0efdff1e18412f7567
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
|
||||
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@369eb591f429131d6889c46b94e711f089e6ca96
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
|
||||
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@48aba3b46d1b1fec4febb7c5d0c644b249a11355
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
@@ -15,6 +15,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Build the Docker image
|
||||
run: docker build . --file Dockerfile --tag wekan:$(date +%s)
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -25,6 +25,6 @@ jobs:
|
||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||
|
||||
- name: Run chart-releaser
|
||||
uses: helm/chart-releaser-action@v1.6.0
|
||||
uses: helm/chart-releaser-action@v1.7.0
|
||||
env:
|
||||
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v4
|
||||
# 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@v4
|
||||
# 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@v4
|
||||
# uses: actions/checkout@v6
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
@@ -90,12 +90,12 @@ jobs:
|
||||
|
||||
# CHECKOUTS
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
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
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
run: sh ./test-wekan.sh -cv
|
||||
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: coverage-folder
|
||||
path: .coverage/
|
||||
@@ -147,10 +147,10 @@ jobs:
|
||||
needs: [tests]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download coverage
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: coverage-folder
|
||||
path: .coverage/
|
||||
|
||||
+8
-15
@@ -16,17 +16,15 @@ es5-shim@4.8.0
|
||||
|
||||
# Collections
|
||||
aldeed:collection2
|
||||
cottz:publish-relations
|
||||
reywood:publish-composite
|
||||
dburles:collection-helpers
|
||||
idmontie:migrations
|
||||
easy:search
|
||||
mongo@1.16.8
|
||||
mquandalle:collection-mutations
|
||||
|
||||
# Account system
|
||||
accounts-password@2.4.0
|
||||
useraccounts:core
|
||||
useraccounts:flow-routing
|
||||
useraccounts:unstyled
|
||||
simple:rest-accounts-password
|
||||
wekan-ldap
|
||||
@@ -44,23 +42,18 @@ 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
|
||||
# Datepicker (disabled - using native HTML inputs)
|
||||
# wekan-bootstrap-datepicker
|
||||
|
||||
# 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
|
||||
@@ -72,25 +65,25 @@ meteorhacks:subs-manager
|
||||
meteorhacks:aggregate@1.3.0
|
||||
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
|
||||
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
|
||||
wekan-fullcalendar
|
||||
momentjs:moment@2.29.3
|
||||
wekan-fontawesome
|
||||
# wekan-fontawesome
|
||||
|
||||
useraccounts:flow-routing-extra
|
||||
ostrio:flow-router-extra
|
||||
+1
-1
@@ -1 +1 @@
|
||||
METEOR@2.14
|
||||
METEOR@2.16
|
||||
+19
-32
@@ -1,5 +1,5 @@
|
||||
accounts-base@2.2.10
|
||||
accounts-oauth@1.4.3
|
||||
accounts-base@2.2.11
|
||||
accounts-oauth@1.4.4
|
||||
accounts-password@2.4.0
|
||||
aldeed:collection2@2.10.0
|
||||
aldeed:collection2-core@1.2.0
|
||||
@@ -7,7 +7,6 @@ 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
|
||||
@@ -20,29 +19,25 @@ 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
|
||||
check@1.4.1
|
||||
coffeescript@2.7.0
|
||||
coffeescript-compiler@2.4.1
|
||||
communitypackages:picker@1.1.1
|
||||
cottz:publish-relations@2.0.8
|
||||
dburles:collection-helpers@1.1.0
|
||||
ddp@1.4.1
|
||||
ddp-client@2.6.1
|
||||
ddp-common@1.4.0
|
||||
ddp-client@2.6.2
|
||||
ddp-common@1.4.1
|
||||
ddp-rate-limiter@1.2.1
|
||||
ddp-server@2.7.0
|
||||
ddp-server@2.7.1
|
||||
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
|
||||
email@2.2.6
|
||||
es5-shim@4.8.0
|
||||
fetch@0.1.4
|
||||
geojson-utils@1.0.11
|
||||
@@ -54,13 +49,10 @@ 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
|
||||
logging@1.3.4
|
||||
matb33:collection-hooks@1.3.0
|
||||
mdg:validation-error@0.5.1
|
||||
meteor@1.11.5
|
||||
@@ -74,25 +66,22 @@ 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
|
||||
minifier-js@2.8.0
|
||||
minifiers@1.1.8-faster-rebuild.0
|
||||
minimongo@1.9.3
|
||||
minimongo@1.9.4
|
||||
modern-browsers@0.1.10
|
||||
modules@0.20.0
|
||||
modules-runtime@0.13.1
|
||||
momentjs:moment@2.29.3
|
||||
mongo@1.16.8
|
||||
mongo@1.16.10
|
||||
mongo-decimal@0.1.3
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.8
|
||||
mongo-livedata@1.0.12
|
||||
mousetrap:mousetrap@1.4.6_1
|
||||
mquandalle:autofocus@1.0.0
|
||||
mquandalle:collection-mutations@0.1.0
|
||||
mquandalle:jade@0.4.9
|
||||
mquandalle:jade-compiler@0.4.5
|
||||
mquandalle:jquery-textcomplete@0.8.0_1
|
||||
mquandalle:mousetrap-bindglobal@0.0.1
|
||||
msavin:usercache@1.8.0
|
||||
npm-mongo@4.17.2
|
||||
oauth@2.2.1
|
||||
@@ -103,16 +92,16 @@ ordered-dict@1.1.0
|
||||
ostrio:cookies@2.7.2
|
||||
ostrio:cstorage@4.0.1
|
||||
ostrio:files@2.3.3
|
||||
ostrio:flow-router-extra@3.10.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
|
||||
quave:synced-cron@2.2.1
|
||||
raix:eventemitter@0.1.3
|
||||
raix:handlebar-helpers@0.2.5
|
||||
random@1.2.1
|
||||
@@ -122,8 +111,9 @@ reactive-dict@1.3.1
|
||||
reactive-var@1.0.12
|
||||
reload@1.3.1
|
||||
retry@1.1.0
|
||||
reywood:publish-composite@1.9.0
|
||||
routepolicy@1.1.1
|
||||
service-configuration@1.3.3
|
||||
service-configuration@1.3.4
|
||||
session@1.2.1
|
||||
sha@1.0.9
|
||||
shell-server@0.5.0
|
||||
@@ -144,23 +134,20 @@ templating-tools@1.2.2
|
||||
tracker@1.3.3
|
||||
typescript@4.9.5
|
||||
ui@1.0.13
|
||||
underscore@1.0.13
|
||||
underscore@1.6.1
|
||||
url@1.3.2
|
||||
useraccounts:core@1.16.2
|
||||
useraccounts:flow-routing@1.15.0
|
||||
useraccounts:flow-routing-extra@1.1.0
|
||||
useraccounts:unstyled@1.14.2
|
||||
webapp@1.13.6
|
||||
webapp@1.13.8
|
||||
webapp-hashing@1.1.1
|
||||
wekan-accounts-cas@0.1.0
|
||||
wekan-accounts-lockout@1.0.0
|
||||
wekan-accounts-oidc@1.0.10
|
||||
wekan-accounts-sandstorm@0.8.0
|
||||
wekan-bootstrap-datepicker@1.10.0
|
||||
wekan-fontawesome@6.4.2
|
||||
wekan-fullcalendar@3.10.5
|
||||
wekan-ldap@0.0.2
|
||||
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
|
||||
zodern:types@1.0.13
|
||||
+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
+12
-11
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
"editor.rulers": [80],
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"files.insertFinalNewline": true,
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.DS_Store": true,
|
||||
"node_modules": true,
|
||||
".meteor/local": true
|
||||
}
|
||||
}
|
||||
"terminal.integrated.env.linux": {
|
||||
"TERM": "xterm-256color"
|
||||
},
|
||||
"terminal.integrated.env.osx": {
|
||||
"TERM": "xterm-256color"
|
||||
},
|
||||
"terminal.integrated.env.windows": {
|
||||
"TERM": "xterm-256color"
|
||||
},
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"terminal.integrated.shellArgs.linux": ["-l"]
|
||||
}
|
||||
+1263
-10
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -11,7 +11,7 @@ For all code at WeKan GitHub Organization https://github.com/wekan
|
||||
|
||||
## Private reports
|
||||
|
||||
- Email support@wekan.team
|
||||
- Email support@wekan.fi
|
||||
- Security issues: [SECURITY.md](SECURITY.md)
|
||||
- License violations
|
||||
- Anything private, sensitive or negative
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ Not paid:
|
||||
|
||||
Paid by customers of WeKan Team:
|
||||
|
||||
- Commercial Support at https://wekan.team/commercial-support/
|
||||
- Commercial Support at https://wekan.fi/commercial-support/
|
||||
- Support
|
||||
- Private Chat
|
||||
- Features
|
||||
|
||||
+154
-96
@@ -1,76 +1,31 @@
|
||||
FROM scratch AS downloads
|
||||
|
||||
# Download NodeJS bundle
|
||||
ADD https://static.meteor.com/dev-bundle-node-os/v14.21.4/node-v14.21.4-linux-x64.tar.gz node.tar.gz
|
||||
|
||||
# Download Meteor bundle
|
||||
ADD https://static.meteor.com/packages-bootstrap/2.16/meteor-bootstrap-os.linux.x86_64.tar.gz meteor.tar.gz
|
||||
|
||||
FROM ubuntu:24.04 AS build
|
||||
|
||||
# Install node from official archive
|
||||
RUN --mount=type=bind,from=downloads,source=node.tar.gz,target=node.tar.gz \
|
||||
tar xzf node.tar.gz --strip-components=1 --keep-old-files --no-same-owner
|
||||
|
||||
# Install meteor from official archive
|
||||
RUN --mount=type=bind,from=downloads,source=meteor.tar.gz,target=meteor.tar.gz \
|
||||
tar xzf meteor.tar.gz -C $HOME --no-same-owner
|
||||
|
||||
# Update the npm version
|
||||
RUN npm install -g npm@6.14.17
|
||||
|
||||
# Install several build dependencies
|
||||
RUN apt update && \
|
||||
apt install --yes --no-install-suggests --no-install-recommends ssh git g++ curl ca-certificates && \
|
||||
rm -r /var/lib/apt/lists
|
||||
|
||||
# Change to temporary working directory
|
||||
WORKDIR /workdir
|
||||
|
||||
# Copy package.json and package-lock.json to install application dependencies
|
||||
COPY package.json .
|
||||
COPY package-lock.json .
|
||||
|
||||
# Install build dependencies
|
||||
RUN $HOME/.meteor/meteor npm install --production
|
||||
|
||||
# Copy meteor application configurations
|
||||
COPY .meteor .meteor
|
||||
|
||||
# Copy application sources
|
||||
COPY packages packages
|
||||
COPY imports imports
|
||||
COPY config config
|
||||
COPY models models
|
||||
COPY public public
|
||||
COPY server server
|
||||
COPY client client
|
||||
|
||||
# Build the application
|
||||
RUN $HOME/.meteor/meteor build --directory /build --allow-superuser
|
||||
|
||||
# Enter server bundle directory
|
||||
WORKDIR /build/bundle/programs/server
|
||||
|
||||
# Install server dependencies
|
||||
RUN $HOME/.meteor/meteor npm install --production
|
||||
|
||||
# Enter installation directory of fibers
|
||||
WORKDIR /build/bundle/programs/server/node_modules/fibers
|
||||
|
||||
# Build fibers integrations
|
||||
RUN node build.js
|
||||
|
||||
FROM ubuntu:24.04 AS wekan
|
||||
|
||||
# Add metadata to image
|
||||
FROM ubuntu:24.04
|
||||
LABEL maintainer="wekan"
|
||||
LABEL org.opencontainers.image.ref.name="ubuntu"
|
||||
LABEL org.opencontainers.image.version="24.04"
|
||||
LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
|
||||
|
||||
# Set default environment variables
|
||||
ENV WITH_API=true \
|
||||
# 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.
|
||||
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 \
|
||||
DEBUG=false \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
METEOR_RELEASE=METEOR@2.14 \
|
||||
USE_EDGE=false \
|
||||
METEOR_EDGE=1.5-beta.17 \
|
||||
NPM_VERSION=6.14.17 \
|
||||
FIBERS_VERSION=4.0.1 \
|
||||
ARCHITECTURE=linux-x64 \
|
||||
SRC_PATH=./ \
|
||||
WITH_API=true \
|
||||
RESULTS_PER_PAGE="" \
|
||||
DEFAULT_BOARD_ID="" \
|
||||
ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
|
||||
@@ -111,6 +66,7 @@ ENV WITH_API=true \
|
||||
OAUTH2_LOGIN_STYLE=redirect \
|
||||
OAUTH2_CLIENT_ID="" \
|
||||
OAUTH2_SECRET="" \
|
||||
OAUTH2_SECRET_FILE="" \
|
||||
OAUTH2_SERVER_URL="" \
|
||||
OAUTH2_AUTH_ENDPOINT="" \
|
||||
OAUTH2_USERINFO_ENDPOINT="" \
|
||||
@@ -136,6 +92,7 @@ ENV WITH_API=true \
|
||||
LDAP_AUTHENTIFICATION=false \
|
||||
LDAP_AUTHENTIFICATION_USERDN="" \
|
||||
LDAP_AUTHENTIFICATION_PASSWORD="" \
|
||||
LDAP_AUTHENTIFICATION_PASSWORD_FILE="" \
|
||||
LDAP_LOG_ENABLED=false \
|
||||
LDAP_BACKGROUND_SYNC=false \
|
||||
LDAP_BACKGROUND_SYNC_INTERVAL="" \
|
||||
@@ -201,38 +158,139 @@ ENV WITH_API=true \
|
||||
ORACLE_OIM_ENABLED=false \
|
||||
WAIT_SPINNER="" \
|
||||
WRITABLE_PATH=/data \
|
||||
S3=""
|
||||
S3="" \
|
||||
MAIL_SERVICE_PASSWORD_FILE="" \
|
||||
MONGO_PASSWORD_FILE="" \
|
||||
S3_SECRET_FILE=""
|
||||
|
||||
# Install node and application sources from build stage
|
||||
RUN --mount=type=bind,from=downloads,source=node.tar.gz,target=node.tar.gz \
|
||||
--mount=type=bind,from=build,source=/build,target=/build \
|
||||
# Install NodeJS
|
||||
tar xzf node.tar.gz --strip-components=1 --keep-old-files --no-same-owner && \
|
||||
# Add the wekan user
|
||||
useradd --uid 999 --user-group --system wekan && \
|
||||
# Copy wekan bundle
|
||||
cp -r /build/bundle /wekan && \
|
||||
# Remove the web.browser.legacy platform
|
||||
rm -r /wekan/programs/web.browser.legacy && \
|
||||
# Change ownership of wekan directory
|
||||
chown wekan:wekan -R /wekan && \
|
||||
# Create the data directory
|
||||
mkdir /data && \
|
||||
# Change ownership of data directory
|
||||
chown wekan:root -R /data
|
||||
# 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
|
||||
useradd --user-group --system --home-dir /home/wekan wekan
|
||||
# OS dependencies
|
||||
apt-get update --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
|
||||
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
|
||||
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.23/wekan-8.23-amd64.zip"
|
||||
unzip wekan-8.23-amd64.zip
|
||||
rm wekan-8.23-amd64.zip
|
||||
mv /home/wekan/app/bundle /build
|
||||
|
||||
# Put back the 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
|
||||
EOR
|
||||
|
||||
# Change to the wekan user
|
||||
USER wekan
|
||||
|
||||
# Set the port to listen on
|
||||
ENV PORT=8080
|
||||
EXPOSE ${PORT}
|
||||
EXPOSE $PORT
|
||||
|
||||
# Define the stop signal for node
|
||||
STOPSIGNAL SIGINT
|
||||
STOPSIGNAL SIGKILL
|
||||
WORKDIR /home/wekan/app
|
||||
|
||||
# Declare a volume on /data
|
||||
VOLUME /data
|
||||
|
||||
# Set startup command
|
||||
CMD ["node", "/wekan/main.js"]
|
||||
#---------------------------------------------------------------------
|
||||
# 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"]
|
||||
+6
-1
@@ -53,7 +53,12 @@ ENV QEMU_ARCHITECTURE=aarch64 \
|
||||
WITH_API=true \
|
||||
PORT=8080 \
|
||||
ROOT_URL=http://localhost \
|
||||
MONGO_URL=mongodb://127.0.0.1:27017/wekan
|
||||
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
|
||||
|
||||
+6
-1
@@ -56,7 +56,12 @@ ENV QEMU_ARCHITECTURE=s390x \
|
||||
WITH_API=true \
|
||||
PORT=8080 \
|
||||
ROOT_URL=http://localhost \
|
||||
MONGO_URL=mongodb://127.0.0.1:27017/wekan
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2024 The Wekan Team
|
||||
Copyright (c) Lauri Ojansivu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
[Gitpod Ready-to-Code](https://gitpod.io/#https://github.com/wekan/wekan)
|
||||
|
||||
# WeKan ® - Open Source kanban
|
||||
|
||||
## Downloads
|
||||
|
||||
https://wekan.github.io / Install WeKan ® Server
|
||||
https://wekan.fi/install/
|
||||
|
||||
## Docker Containers
|
||||
|
||||
@@ -16,7 +14,7 @@ docker-compose.yml at https://github.com/wekan/wekan/blob/main/docker-compose.ym
|
||||
|
||||
## Standards
|
||||
|
||||
- [WeKan and Standard for Public Code](https://wekan.github.io/standard-for-public-code/) assessment was made at 2023-11.
|
||||
- [WeKan and Standard for Public Code](https://wekan.fi/standard-for-public-code/) assessment was made at 2023-11.
|
||||
Currently Wekan meets 8 out of 16 criteria out of the box.
|
||||
Some others could be met with small changes.
|
||||
|
||||
@@ -27,21 +25,19 @@ docker-compose.yml at https://github.com/wekan/wekan/blob/main/docker-compose.ym
|
||||
- [Open Hub](https://www.openhub.net/p/wekan)
|
||||
- [OSS Insight](https://ossinsight.io/analyze/wekan/wekan)
|
||||
|
||||
## [Translate WeKan ® at Transifex](https://app.transifex.com/wekan/)
|
||||
## Translate WeKan ® at Transifex
|
||||
|
||||
Translations to non-English languages are accepted only at [Transifex](https://app.transifex.com/wekan/wekan) using webbrowser.
|
||||
New English strings of new features can be added as PRs to master branch file wekan/imports/i18n/data/en.i18n.json .
|
||||
|
||||
## [WeKan ® feature requests and bugs](https://github.com/wekan/wekan/issues)
|
||||
## WeKan ® feature requests and bugs
|
||||
|
||||
Please add most of your questions as GitHub issue: [WeKan ® Feature Requests and Bugs](https://github.com/wekan/wekan/issues).
|
||||
It's better than at chat where details get lost when chat scrolls up.
|
||||
|
||||
## Chat
|
||||
## Discussions
|
||||
|
||||
[Discussions][discussions] - WeKan Community GitHub Discussions, that are not [Feature Requests and Bugs](https://github.com/wekan/wekan/issues).
|
||||
|
||||
[WeKan IRC FAQ](https://github.com/wekan/wekan/wiki/IRC-FAQ)
|
||||
[IRC](https://github.com/wekan/wekan/blob/main/docs/FAQ/IRC-FAQ.md)
|
||||
|
||||
## Docker: Latest tag has newest release
|
||||
|
||||
@@ -51,8 +47,8 @@ See bottom of https://github.com/wekan/wekan/issues/3874
|
||||
## FAQ
|
||||
|
||||
**NOTE**:
|
||||
- Please read the [FAQ](https://github.com/wekan/wekan/wiki/FAQ) first
|
||||
- Please don't feed the [trolls](https://github.com/wekan/wekan/wiki/FAQ#why-am-i-called-a-troll) and [spammers](https://github.com/wekan/wekan/wiki/FAQ#why-am-i-called-a-spammer) that are mentioned in the FAQ :)
|
||||
- Please read the [FAQ](https://github.com/wekan/wekan/blob/main/docs/FAQ/FAQ.md) first
|
||||
- Please don't feed the [trolls](https://github.com/wekan/wekan/blob/main/docs/FAQ/FAQ.md#why-am-i-called-a-troll) and [spammers](https://github.com/wekan/wekan/blob/main/docs/FAQ/FAQ.md#why-am-i-called-a-spammer) that are mentioned in the FAQ :)
|
||||
|
||||
## About WeKan ®
|
||||
|
||||
@@ -72,14 +68,11 @@ that by providing one-click installation on various platforms.
|
||||
- WeKan ® largest user has 30k users using WeKan ® in their company.
|
||||
- WeKan ® has been [translated](https://app.transifex.com/wekan/) to about 105 languages.
|
||||
- [Features][features]: WeKan ® has real-time user interface.
|
||||
- [Platforms][platforms]: WeKan ® supports many platforms.
|
||||
- [Platforms](https://wekan.fi/install/): WeKan ® supports many platforms.
|
||||
WeKan ® is critical part of new platforms Wekan is currently being integrated to.
|
||||
|
||||
## Requirements
|
||||
|
||||
- 64bit: Linux [Snap](https://github.com/wekan/wekan-snap/wiki/Install) or [Sandstorm](https://sandstorm.io) /
|
||||
[Mac](https://github.com/wekan/wekan/wiki/Mac) / [Windows](https://github.com/wekan/wekan/wiki/Install-Wekan-from-source-on-Windows).
|
||||
[More Platforms](https://github.com/wekan/wekan/wiki/Platforms), bundle for RasPi3 ARM and other CPUs where Node.js and MongoDB exists.
|
||||
- 1 GB RAM minimum free for WeKan ®. Production server should have minimum total 4 GB RAM.
|
||||
For thousands of users, for example with [Docker](https://github.com/wekan/wekan/blob/main/docker-compose.yml): 3 frontend servers,
|
||||
each having 2 CPU and 2 wekan-app containers. One backend wekan-db server with many CPUs.
|
||||
@@ -89,7 +82,7 @@ that by providing one-click installation on various platforms.
|
||||
WeKan ® on Sandstorm is not usually affected by any Standalone WeKan ® (Snap/Docker/Source) security issues.
|
||||
- [Reporting all new bugs immediately](https://github.com/wekan/wekan/issues).
|
||||
New features and fixes are added to WeKan ® [many times a day](https://github.com/wekan/wekan/blob/main/CHANGELOG.md).
|
||||
- [Backups](https://github.com/wekan/wekan/wiki/Backup) of WeKan ® database once a day miminum.
|
||||
- [Backups](https://github.com/wekan/wekan/blob/main/docs/Backup/Backup.md) of WeKan ® database once a day miminum.
|
||||
Bugs, updates, users deleting list or card, harddrive full, harddrive crash etc can eat your data. There is no undo yet.
|
||||
Some bug can cause WeKan ® board to not load at all, requiring manual fixing of database content.
|
||||
|
||||
@@ -102,14 +95,22 @@ that by providing one-click installation on various platforms.
|
||||
- There is many companies and individuals contributing code to WeKan ®, to add features and bugfixes
|
||||
[many times a day](https://github.com/wekan/wekan/blob/main/CHANGELOG.md).
|
||||
- [Please add Add new Feature Requests and Bug Reports immediately](https://github.com/wekan/wekan/issues).
|
||||
- [Commercial Support](https://wekan.team/commercial-support/).
|
||||
- [Commercial Support](https://wekan.fi/commercial-support/).
|
||||
|
||||
We also welcome sponsors for features and bugfixes.
|
||||
By working directly with WeKan ® you get the benefit of active maintenance and new features added by growing WeKan ® developer community.
|
||||
|
||||
## Getting Started with Development
|
||||
|
||||
The default branch uses [Meteor 2 with Node.js 14](https://wekan.fi/install/).
|
||||
|
||||
To contribute, [create a fork](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#2-create-fork-of-httpsgithubcomwekanwekan-at-github-web-page) and run `./rebuild-wekan.sh` (or `./rebuild-wekan.bat` on Windows) as detailed [here](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#3-select-option-1-to-install-dependencies-and-then-enter). Once you're ready, please test your code and [submit a pull request (PR)](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Build-and-Create-Pull-Request.md#7-test).
|
||||
|
||||
Please refer to the [developer documentation](https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Developer-Documentation.md) for more information.
|
||||
|
||||
## Screenshot
|
||||
|
||||
[More screenshots at Features page](https://github.com/wekan/wekan/wiki/Features)
|
||||
[More screenshots at Features page](https://github.com/wekan/wekan/tree/main/docs/Features)
|
||||
|
||||
[![Screenshot of WeKan ®][screenshot_wekan]][roadmap_wekan]
|
||||
|
||||
@@ -118,16 +119,12 @@ By working directly with WeKan ® you get the benefit of active maintenance and
|
||||
WeKan ® is released under the very permissive [MIT license](LICENSE), and made
|
||||
with [Meteor](https://www.meteor.com).
|
||||
|
||||
[platforms]: https://github.com/wekan/wekan/wiki/Platforms
|
||||
[dev_docs]: https://github.com/wekan/wekan/wiki/Developer-Documentation
|
||||
[screenshot_wekan]: https://wekan.github.io/wekan-dark-mode.png
|
||||
[dev_docs]: https://github.com/wekan/wekan/blob/main/docs/DeveloperDocs/Developer-Documentation.md
|
||||
[screenshot_wekan]: https://wekan.fi/wekan-dark-mode.png
|
||||
[features]: https://github.com/wekan/wekan/wiki/Features
|
||||
[roadmap_wekan]: https://boards.wekan.team/b/D2SzJKZDS4Z48yeQH/wekan-open-source-kanban-board-with-mit-license
|
||||
[wekan_issues]: https://github.com/wekan/wekan/issues
|
||||
[wekan_issues]: https://github.com/wekan/wekan/issues
|
||||
[docker_image]: https://hub.docker.com/r/wekanteam/wekan/
|
||||
[wekan_wiki]: https://github.com/wekan/wekan/wiki
|
||||
[translate_wekan]: https://app.transifex.com/wekan/
|
||||
[translate_wekan]: https://app.transifex.com/wekan/wekan/
|
||||
[open_source]: https://en.wikipedia.org/wiki/Open-source_software
|
||||
[free_software]: https://en.wikipedia.org/wiki/Free_software
|
||||
[discussions]: https://github.com/wekan/wekan/discussions
|
||||
@@ -0,0 +1,113 @@
|
||||
# 🔐 WeKan — Login System Overview
|
||||
|
||||
This document provides a detailed overview of WeKan’s **login and authentication system**, covering client-side UI, server-side logic, external authentication methods, and potential upgrade paths.
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Login Web UI
|
||||
|
||||
WeKan's login interface is implemented using a combination of:
|
||||
|
||||
- `layouts.jade` – Login HTML structure
|
||||
- `layouts.js` – Login logic and interactivity
|
||||
- `layouts.css` – Styling and layout
|
||||
|
||||
📁 Source: [`client/components/main`](https://github.com/wekan/wekan/tree/main/client/components/main)
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Server-Side Authentication
|
||||
|
||||
Server-side login functionality is handled in:
|
||||
|
||||
- [`server/authentication.js`](https://github.com/wekan/wekan/blob/main/server/authentication.js)
|
||||
|
||||
Other related configurations:
|
||||
|
||||
- 🔧 Account config: [`config/accounts.js`](https://github.com/wekan/wekan/blob/main/config/accounts.js)
|
||||
- 📨 Sign-up invitations: [`models/settings.js#L275`](https://github.com/wekan/wekan/blob/main/models/settings.js#L275)
|
||||
- 👤 User creation logic: [`models/users.js#L1339`](https://github.com/wekan/wekan/blob/main/models/users.js#L1339)
|
||||
|
||||
---
|
||||
|
||||
## 👥 Meteor User Accounts
|
||||
|
||||
WeKan utilizes Meteor’s `accounts` system. Relevant resources:
|
||||
|
||||
- 📚 Meteor 2.x Accounts Docs: [v2-docs.meteor.com/api/accounts](https://v2-docs.meteor.com/api/accounts)
|
||||
- 🔍 Meteor Packages:
|
||||
- [`packages`](https://github.com/wekan/wekan/blob/main/.meteor/packages)
|
||||
- [`versions`](https://github.com/wekan/wekan/blob/main/.meteor/versions)
|
||||
- 📦 Meteor 2.14 core packages: [Meteor 2.14 packages](https://github.com/meteor/meteor/tree/release/METEOR%402.14/packages)
|
||||
|
||||
---
|
||||
|
||||
## 🔐 External Authentication (OIDC, LDAP, etc.)
|
||||
|
||||
WeKan supports external authentication methods via internal packages.
|
||||
|
||||
📁 See [`packages/`](https://github.com/wekan/wekan/tree/main/packages) for:
|
||||
- OpenID Connect (OIDC)
|
||||
- LDAP
|
||||
- OAuth and other integrations
|
||||
|
||||
---
|
||||
|
||||
## 📦 NPM & AtmosphereJS Dependencies
|
||||
|
||||
- 🔗 `package.json`: [Dependencies list](https://github.com/wekan/wekan/blob/main/package.json)
|
||||
- 🧩 WekanTeam scoped NPM packages: [@wekanteam on npm](https://www.npmjs.com/search?q=%40wekanteam)
|
||||
- ☁️ AtmosphereJS Meteor packages: [atmospherejs.com](https://atmospherejs.com)
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Meteor Version & Upgrade Notes
|
||||
|
||||
- 📌 Current Version: **Meteor 2.14**
|
||||
- [`.meteor/release`](https://github.com/wekan/wekan/blob/main/.meteor/release)
|
||||
- 🔧 Maintained with only **critical fixes** until ~Summer 2025
|
||||
- 🚀 Migration to **Meteor 3** or a new framework is under consideration
|
||||
|
||||
📘 Meteor 3 API: [docs.meteor.com/api/accounts](https://docs.meteor.com/api/accounts)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Prototypes & Examples
|
||||
|
||||
### 🐘 PHP Prototype Sign-Up
|
||||
|
||||
Used in experimental versions:
|
||||
|
||||
- Step 1: [`sign-up1.php`](https://github.com/wekan/php/blob/main/page/sign-up1.php)
|
||||
- Step 2: [`sign-up2.php`](https://github.com/wekan/php/blob/main/page/sign-up2.php)
|
||||
- Main entry: [`index.php#L72-L83`](https://github.com/wekan/php/blob/main/public/index.php#L72-L83)
|
||||
|
||||
---
|
||||
|
||||
### 🎨 WeKan Studio Prototype
|
||||
|
||||
Sign-up logic in the **WeKan Studio** version:
|
||||
|
||||
- [`signUp.fmt`](https://github.com/wekan/wekanstudio/blob/main/srv/templates/login/signUp.fmt)
|
||||
|
||||
---
|
||||
|
||||
## 📎 Future Considerations
|
||||
|
||||
- Upgrading to **Meteor 3.x**
|
||||
- Refactoring frontend logic to fix translation rendering order
|
||||
- Exploring **simplified authentication systems** in future prototypes
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Project Links
|
||||
|
||||
- 🔧 Main Repo: [github.com/wekan/wekan](https://github.com/wekan/wekan)
|
||||
- 🌐 Website: [wekan.github.io](https://wekan.github.io)
|
||||
- 📚 Documentation: [Wekan Wiki](https://github.com/wekan/wekan/wiki)
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
+68
-18
@@ -1,11 +1,20 @@
|
||||
About money, see [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
Security is very important to us. If you discover any issue regarding security, please disclose
|
||||
the information responsibly by sending an email to security@wekan.team and not by
|
||||
creating a GitHub issue. We will respond swiftly to fix verifiable security issues.
|
||||
## Responsible Security Disclosure
|
||||
|
||||
We thank you with a place at our hall of fame page, that is
|
||||
at https://wekan.github.io/hall-of-fame
|
||||
- To send email, use [ProtonMail](https://proton.me) email address or use PGP key [security-at-wekan.fi.asc](security-at-wekan.fi.asc)
|
||||
- Send info about security issue ONLY to security@wekan.fi (that is Protomail email address). NOT TO ANYWHERE ELSE. NO CC, NO BCC.
|
||||
- Wait for new WeKan release that fixes security issue
|
||||
- If you approve, we thank you by adding you to Hall of Fame: https://wekan.fi/hall-of-fame/
|
||||
|
||||
## 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?
|
||||
|
||||
@@ -25,7 +34,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?
|
||||
|
||||
@@ -62,11 +71,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
|
||||
|
||||
@@ -171,6 +175,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
|
||||
@@ -217,9 +272,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: "v7.79.0"
|
||||
appVersion: "v8.23.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -39,6 +39,12 @@ If *nix: chmod +x api.py => ./api.py users
|
||||
python3 api.py addcustomfieldtoboard AUTHORID BOARDID NAME TYPE SETTINGS SHOWONCARD AUTOMATICALLYONCARD SHOWLABELONMINICARD SHOWSUMATTOPOFLIST # Add Custom Field to Board
|
||||
python3 api.py editcustomfield BOARDID LISTID CARDID CUSTOMFIELDID NEWCUSTOMFIELDVALUE # Edit Custom Field
|
||||
python3 api.py listattachments BOARDID # List attachments
|
||||
python3 api.py uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND] # Upload attachment to card
|
||||
python3 api.py downloadattachment ATTACHMENTID OUTPUTPATH # Download attachment to local file
|
||||
python3 api.py attachmentinfo ATTACHMENTID # Get attachment information
|
||||
python3 api.py listcardattachments BOARDID SWIMLANEID LISTID CARDID # List attachments for specific card
|
||||
python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move] # Copy or move attachment
|
||||
python3 api.py deleteattachment ATTACHMENTID # Delete attachment
|
||||
python3 api.py cardsbyswimlane SWIMLANEID LISTID # Retrieve cards list on a swimlane
|
||||
python3 api.py getcard BOARDID LISTID CARDID # Get card info
|
||||
python3 api.py addlabel BOARDID LISTID CARDID LABELID # Add label to a card
|
||||
@@ -750,3 +756,273 @@ if arguments == 1:
|
||||
data2 = body.text.replace('}',"}\n")
|
||||
print(data2)
|
||||
# ------- LIST OF PUBLIC BOARDS END -----------
|
||||
|
||||
# ------- NEW ATTACHMENT API ENDPOINTS START -----------
|
||||
|
||||
if sys.argv[1] == 'uploadattachment':
|
||||
# ------- UPLOAD ATTACHMENT START -----------
|
||||
if arguments < 5:
|
||||
print("Usage: python3 api.py uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND]")
|
||||
print("Storage backends: fs, gridfs, s3")
|
||||
exit(1)
|
||||
|
||||
boardid = sys.argv[2]
|
||||
swimlaneid = sys.argv[3]
|
||||
listid = sys.argv[4]
|
||||
cardid = sys.argv[5]
|
||||
filepath = sys.argv[6]
|
||||
storage_backend = sys.argv[7] if arguments > 6 else None
|
||||
|
||||
# Read file and convert to base64
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
file_data = f.read()
|
||||
import base64
|
||||
base64_data = base64.b64encode(file_data).decode('utf-8')
|
||||
except FileNotFoundError:
|
||||
print(f"Error: File '{filepath}' not found")
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error reading file: {e}")
|
||||
exit(1)
|
||||
|
||||
# Get file info
|
||||
import os
|
||||
filename = os.path.basename(filepath)
|
||||
import mimetypes
|
||||
file_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
|
||||
|
||||
# Prepare request data
|
||||
upload_data = {
|
||||
'boardId': boardid,
|
||||
'swimlaneId': swimlaneid,
|
||||
'listId': listid,
|
||||
'cardId': cardid,
|
||||
'fileData': base64_data,
|
||||
'fileName': filename,
|
||||
'fileType': file_type
|
||||
}
|
||||
|
||||
if storage_backend:
|
||||
upload_data['storageBackend'] = storage_backend
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
upload_url = wekanurl + 'api/attachment/upload'
|
||||
|
||||
try:
|
||||
response = requests.post(upload_url, headers=headers, json=upload_data)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
print(f"Upload successful!")
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"File: {result.get('fileName')}")
|
||||
print(f"Size: {result.get('fileSize')} bytes")
|
||||
print(f"Storage: {result.get('storageBackend')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Upload failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- UPLOAD ATTACHMENT END -----------
|
||||
|
||||
if sys.argv[1] == 'downloadattachment':
|
||||
# ------- DOWNLOAD ATTACHMENT START -----------
|
||||
if arguments < 3:
|
||||
print("Usage: python3 api.py downloadattachment ATTACHMENTID OUTPUTPATH")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
outputpath = sys.argv[3]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
download_url = wekanurl + f'api/attachment/download/{attachmentid}'
|
||||
|
||||
try:
|
||||
response = requests.get(download_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
# Decode base64 data and save to file
|
||||
import base64
|
||||
file_data = base64.b64decode(result.get('base64Data'))
|
||||
|
||||
with open(outputpath, 'wb') as f:
|
||||
f.write(file_data)
|
||||
|
||||
print(f"Download successful!")
|
||||
print(f"File saved to: {outputpath}")
|
||||
print(f"Original filename: {result.get('fileName')}")
|
||||
print(f"Size: {result.get('fileSize')} bytes")
|
||||
print(f"Storage: {result.get('storageBackend')}")
|
||||
else:
|
||||
print(f"Download failed: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Download failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- DOWNLOAD ATTACHMENT END -----------
|
||||
|
||||
if sys.argv[1] == 'attachmentinfo':
|
||||
# ------- ATTACHMENT INFO START -----------
|
||||
if arguments < 2:
|
||||
print("Usage: python3 api.py attachmentinfo ATTACHMENTID")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
info_url = wekanurl + f'api/attachment/info/{attachmentid}'
|
||||
|
||||
try:
|
||||
response = requests.get(info_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
print("=== ATTACHMENT INFO ===")
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"File Name: {result.get('fileName')}")
|
||||
print(f"File Size: {result.get('fileSize')} bytes")
|
||||
print(f"File Type: {result.get('fileType')}")
|
||||
print(f"Storage Backend: {result.get('storageBackend')}")
|
||||
print(f"Board ID: {result.get('boardId')}")
|
||||
print(f"Swimlane ID: {result.get('swimlaneId')}")
|
||||
print(f"List ID: {result.get('listId')}")
|
||||
print(f"Card ID: {result.get('cardId')}")
|
||||
print(f"Created At: {result.get('createdAt')}")
|
||||
print(f"Is Image: {result.get('isImage')}")
|
||||
print(f"Versions: {len(result.get('versions', []))}")
|
||||
else:
|
||||
print(f"Failed to get attachment info: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- ATTACHMENT INFO END -----------
|
||||
|
||||
if sys.argv[1] == 'listcardattachments':
|
||||
# ------- LIST CARD ATTACHMENTS START -----------
|
||||
if arguments < 5:
|
||||
print("Usage: python3 api.py listcardattachments BOARDID SWIMLANEID LISTID CARDID")
|
||||
exit(1)
|
||||
|
||||
boardid = sys.argv[2]
|
||||
swimlaneid = sys.argv[3]
|
||||
listid = sys.argv[4]
|
||||
cardid = sys.argv[5]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
list_url = wekanurl + f'api/attachment/list/{boardid}/{swimlaneid}/{listid}/{cardid}'
|
||||
|
||||
try:
|
||||
response = requests.get(list_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
attachments = result.get('attachments', [])
|
||||
print(f"=== CARD ATTACHMENTS ({len(attachments)}) ===")
|
||||
for attachment in attachments:
|
||||
print(f"ID: {attachment.get('attachmentId')}")
|
||||
print(f"Name: {attachment.get('fileName')}")
|
||||
print(f"Size: {attachment.get('fileSize')} bytes")
|
||||
print(f"Type: {attachment.get('fileType')}")
|
||||
print(f"Storage: {attachment.get('storageBackend')}")
|
||||
print(f"Created: {attachment.get('createdAt')}")
|
||||
print("---")
|
||||
else:
|
||||
print(f"Failed to list attachments: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- LIST CARD ATTACHMENTS END -----------
|
||||
|
||||
if sys.argv[1] == 'copymoveattachment':
|
||||
# ------- COPY/MOVE ATTACHMENT START -----------
|
||||
if arguments < 6:
|
||||
print("Usage: python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move]")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
targetboardid = sys.argv[3]
|
||||
targetswimlaneid = sys.argv[4]
|
||||
targetlistid = sys.argv[5]
|
||||
targetcardid = sys.argv[6]
|
||||
operation = sys.argv[7] if arguments > 6 else 'copy'
|
||||
|
||||
if operation not in ['copy', 'move']:
|
||||
print("Operation must be 'copy' or 'move'")
|
||||
exit(1)
|
||||
|
||||
# Prepare request data
|
||||
request_data = {
|
||||
'attachmentId': attachmentid,
|
||||
'targetBoardId': targetboardid,
|
||||
'targetSwimlaneId': targetswimlaneid,
|
||||
'targetListId': targetlistid,
|
||||
'targetCardId': targetcardid
|
||||
}
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
|
||||
api_url = wekanurl + f'api/attachment/{operation}'
|
||||
|
||||
try:
|
||||
response = requests.post(api_url, headers=headers, json=request_data)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
print(f"{operation.capitalize()} successful!")
|
||||
if operation == 'copy':
|
||||
print(f"Source Attachment ID: {result.get('sourceAttachmentId')}")
|
||||
print(f"New Attachment ID: {result.get('newAttachmentId')}")
|
||||
else:
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"Source Board: {result.get('sourceBoardId')}")
|
||||
print(f"Target Board: {result.get('targetBoardId')}")
|
||||
print(f"File: {result.get('fileName')}")
|
||||
print(f"Size: {result.get('fileSize')} bytes")
|
||||
else:
|
||||
print(f"{operation.capitalize()} failed: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"{operation.capitalize()} failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- COPY/MOVE ATTACHMENT END -----------
|
||||
|
||||
if sys.argv[1] == 'deleteattachment':
|
||||
# ------- DELETE ATTACHMENT START -----------
|
||||
if arguments < 2:
|
||||
print("Usage: python3 api.py deleteattachment ATTACHMENTID")
|
||||
exit(1)
|
||||
|
||||
attachmentid = sys.argv[2]
|
||||
|
||||
# Make API call
|
||||
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
|
||||
delete_url = wekanurl + f'api/attachment/delete/{attachmentid}'
|
||||
|
||||
try:
|
||||
response = requests.delete(delete_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result.get('success'):
|
||||
print("Delete successful!")
|
||||
print(f"Attachment ID: {result.get('attachmentId')}")
|
||||
print(f"File: {result.get('fileName')}")
|
||||
else:
|
||||
print(f"Delete failed: {result.get('message', 'Unknown error')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Delete failed: {e}")
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Response: {e.response.text}")
|
||||
# ------- DELETE ATTACHMENT END -----------
|
||||
|
||||
# ------- NEW ATTACHMENT API ENDPOINTS END -----------
|
||||
@@ -4,3 +4,79 @@ if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/pwa-service-worker.js');
|
||||
});
|
||||
}
|
||||
|
||||
// Import board converter for on-demand conversion
|
||||
import '/client/lib/boardConverter';
|
||||
import '/client/components/boardConversionProgress';
|
||||
|
||||
// Import migration manager and progress UI
|
||||
import '/client/lib/attachmentMigrationManager';
|
||||
import '/client/components/settings/migrationProgress';
|
||||
|
||||
// Import cron settings
|
||||
import '/client/components/settings/cronSettings';
|
||||
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,16 @@
|
||||
.activity-title {
|
||||
margin: 0 0.5em 0.8em;
|
||||
margin: 0 0.7vw 1vh;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.reactions-popup .add-comment-reaction {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
font-size: 22px;
|
||||
border-radius: 0.7vw;
|
||||
font-size: clamp(18px, 4vw, 22px);
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
width: 40px;
|
||||
line-height: 1.3;
|
||||
width: 5vw;
|
||||
}
|
||||
.reactions-popup .add-comment-reaction:hover {
|
||||
background-color: #b0c4de;
|
||||
@@ -19,13 +19,13 @@
|
||||
clear: both;
|
||||
}
|
||||
.activities .activity {
|
||||
margin: 0.5px 0;
|
||||
padding: 6px 0;
|
||||
margin: 0.1vh 0;
|
||||
padding: 0.8vh 0;
|
||||
display: flex;
|
||||
}
|
||||
.activities .activity .member {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 4vw;
|
||||
height: 4vw;
|
||||
}
|
||||
.activities .activity .activity-member {
|
||||
font-weight: 700;
|
||||
@@ -36,18 +36,18 @@
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
margin-left: 3px;
|
||||
margin-left: 0.4vw;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
.activities .activity .activity-desc .activity-comment {
|
||||
display: block;
|
||||
border-radius: 3px;
|
||||
border-radius: 0.4vw;
|
||||
background: #fff;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
margin-top: 5px;
|
||||
padding: 5px;
|
||||
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.2);
|
||||
margin-top: 0.7vh;
|
||||
padding: 0.7vh 0.7vw;
|
||||
}
|
||||
.activities .activity .activity-desc .activity-checklist {
|
||||
display: block;
|
||||
|
||||
@@ -199,4 +199,5 @@ template(name="activity")
|
||||
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 {{ moment activity.createdAt }}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
|
||||
const activitiesPerPage = 500;
|
||||
@@ -216,15 +217,11 @@ BlazeComponent.extendComponent({
|
||||
{
|
||||
href: source.url,
|
||||
},
|
||||
DOMPurify.sanitize(source.system, {
|
||||
ALLOW_UNKNOWN_PROTOCOLS: true,
|
||||
}),
|
||||
sanitizeHTML(source.system),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return DOMPurify.sanitize(source.system, {
|
||||
ALLOW_UNKNOWN_PROTOCOLS: true,
|
||||
});
|
||||
return sanitizeHTML(source.system);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -248,10 +245,10 @@ BlazeComponent.extendComponent({
|
||||
href: `${attachment.link()}?download=true`,
|
||||
target: '_blank',
|
||||
},
|
||||
DOMPurify.sanitize(attachment.name),
|
||||
sanitizeText(attachment.name),
|
||||
),
|
||||
)) ||
|
||||
DOMPurify.sanitize(this.currentData().activity.attachmentName)
|
||||
sanitizeText(this.currentData().activity.attachmentName)
|
||||
);
|
||||
},
|
||||
|
||||
@@ -265,7 +262,7 @@ BlazeComponent.extendComponent({
|
||||
|
||||
Template.activity.helpers({
|
||||
sanitize(value) {
|
||||
return DOMPurify.sanitize(value, { ALLOW_UNKNOWN_PROTOCOLS: true });
|
||||
return sanitizeHTML(value);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -336,7 +333,7 @@ function createCardLink(card, board) {
|
||||
href: card.originRelativeUrl(),
|
||||
class: 'action-card',
|
||||
},
|
||||
DOMPurify.sanitize(text, { ALLOW_UNKNOWN_PROTOCOLS: true }),
|
||||
sanitizeHTML(text),
|
||||
),
|
||||
)
|
||||
);
|
||||
@@ -353,7 +350,7 @@ function createBoardLink(board, list) {
|
||||
href: board.originRelativeUrl(),
|
||||
class: 'action-board',
|
||||
},
|
||||
DOMPurify.sanitize(text, { ALLOW_UNKNOWN_PROTOCOLS: true }),
|
||||
sanitizeHTML(text),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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,7 +25,7 @@ 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' }}") ❌
|
||||
else
|
||||
.comment-text
|
||||
+viewer
|
||||
@@ -54,9 +54,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
|
||||
|
||||
@@ -57,8 +57,9 @@ BlazeComponent.extendComponent({
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
getComments() {
|
||||
const ret = this.data().comments();
|
||||
return ret;
|
||||
const data = this.data();
|
||||
if (!data || typeof data.comments !== 'function') return [];
|
||||
return data.comments();
|
||||
},
|
||||
}).register("comments");
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
/* Board Conversion Progress Styles */
|
||||
.board-conversion-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.board-conversion-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.board-conversion-modal {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.board-conversion-header {
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.board-conversion-header h3 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.board-conversion-header h3 i {
|
||||
margin-right: 8px;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.board-conversion-header p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.board-conversion-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.conversion-progress {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #2196F3, #21CBF3);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.3),
|
||||
transparent
|
||||
);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #2196F3;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.conversion-status {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.conversion-status i {
|
||||
margin-right: 8px;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.conversion-time {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
background-color: #f5f5f5;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.conversion-time i {
|
||||
margin-right: 6px;
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
.board-conversion-footer {
|
||||
padding: 16px 24px 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.conversion-info {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.conversion-info i {
|
||||
margin-right: 6px;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 600px) {
|
||||
.board-conversion-modal {
|
||||
width: 95%;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.board-conversion-header,
|
||||
.board-conversion-content,
|
||||
.board-conversion-footer {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.board-conversion-header h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
template(name="boardConversionProgress")
|
||||
.board-conversion-overlay(class="{{#if isConverting}}active{{/if}}")
|
||||
.board-conversion-modal
|
||||
.board-conversion-header
|
||||
h3
|
||||
| ⚙️
|
||||
| {{_ 'converting-board'}}
|
||||
p {{_ 'converting-board-description'}}
|
||||
|
||||
.board-conversion-content
|
||||
.conversion-progress
|
||||
.progress-bar
|
||||
.progress-fill(style="width: {{conversionProgress}}%")
|
||||
.progress-text {{conversionProgress}}%
|
||||
|
||||
.conversion-status
|
||||
| ⚙️
|
||||
| {{conversionStatus}}
|
||||
|
||||
.conversion-time(style="{{#unless conversionEstimatedTime}}display: none;{{/unless}}")
|
||||
| ⏰
|
||||
| {{_ 'estimated-time-remaining'}}: {{conversionEstimatedTime}}
|
||||
|
||||
.board-conversion-footer
|
||||
.conversion-info
|
||||
| ℹ️
|
||||
| {{_ 'conversion-info-text'}}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import {
|
||||
boardConverter,
|
||||
isConverting,
|
||||
conversionProgress,
|
||||
conversionStatus,
|
||||
conversionEstimatedTime
|
||||
} from '/client/lib/boardConverter';
|
||||
|
||||
Template.boardConversionProgress.helpers({
|
||||
isConverting() {
|
||||
return isConverting.get();
|
||||
},
|
||||
|
||||
conversionProgress() {
|
||||
return conversionProgress.get();
|
||||
},
|
||||
|
||||
conversionStatus() {
|
||||
return conversionStatus.get();
|
||||
},
|
||||
|
||||
conversionEstimatedTime() {
|
||||
return conversionEstimatedTime.get();
|
||||
}
|
||||
});
|
||||
|
||||
Template.boardConversionProgress.onCreated(function() {
|
||||
// Subscribe to conversion state changes
|
||||
this.autorun(() => {
|
||||
isConverting.get();
|
||||
conversionProgress.get();
|
||||
conversionStatus.get();
|
||||
conversionEstimatedTime.get();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
template(name="archivedBoards")
|
||||
h2
|
||||
i.fa.fa-archive
|
||||
span(title="{{_ 'archived-boards'}}") 📦
|
||||
| {{_ 'archived-boards'}}
|
||||
|
||||
ul.archived-lists
|
||||
@@ -8,10 +8,10 @@ template(name="archivedBoards")
|
||||
li.archived-lists-item
|
||||
div.board-header-btns
|
||||
button.board-header-btn.js-delete-board
|
||||
i.fa.fa-trash-o
|
||||
| 🗑️
|
||||
| {{_ 'delete-board'}}
|
||||
button.board-header-btn.js-restore-board
|
||||
i.fa.fa-undo
|
||||
| ↩️
|
||||
| {{_ 'restore-board'}}
|
||||
= title
|
||||
span {{ moment archivedAt 'LLL' }}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
|
||||
@@ -6,6 +6,27 @@
|
||||
bottom: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
/* When zoom is 50% or lower, ensure full width like content */
|
||||
.board-wrapper[style*="transform: scale(0.5)"] {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.board-wrapper[style*="transform: scale(0.4)"] {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.board-wrapper[style*="transform: scale(0.3)"] {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.board-wrapper .board-canvas {
|
||||
position: absolute;
|
||||
@@ -15,6 +36,243 @@
|
||||
bottom: 0;
|
||||
transition: margin 0.1s;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
/* Ensure horizontal scrollbar is visible for high zoom levels */
|
||||
.board-wrapper .board-canvas[style*="overflow-x: auto"] {
|
||||
overflow-x: auto !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
/* Style the scrollbar for better visibility */
|
||||
.board-wrapper .board-canvas::-webkit-scrollbar {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.board-wrapper .board-canvas::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.board-wrapper .board-canvas::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.board-wrapper .board-canvas::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Ensure content area can scroll horizontally for high zoom levels */
|
||||
#content[style*="overflow-x: auto"] {
|
||||
overflow-x: auto !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
/* Style content area scrollbar */
|
||||
#content::-webkit-scrollbar {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
#content::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
#content::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
#content::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Ensure scrollbars are always visible for high zoom levels */
|
||||
#content[style*="overflow-x: auto"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#content[style*="overflow-x: auto"]::-webkit-scrollbar {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
/* Force vertical scrollbar to always be visible */
|
||||
#content[style*="overflow-y: auto"] {
|
||||
overflow-y: scroll !important;
|
||||
}
|
||||
|
||||
/* Mobile - make all text 2x bigger inside #content by default (icons stay same size) */
|
||||
@media screen and (max-width: 800px),
|
||||
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) {
|
||||
#content {
|
||||
font-size: 2em !important; /* 2x bigger base font size for content area */
|
||||
}
|
||||
|
||||
/* Make all text elements 2x bigger */
|
||||
#content h1, #content h2, #content h3, #content h4, #content h5, #content h6,
|
||||
#content p, #content span, #content div, #content a, #content button,
|
||||
#content .minicard, #content .list-header-name, #content .board-header-btn,
|
||||
#content .card-title, #content .card-details, #content .card-description,
|
||||
#content .swimlane-header, #content .list-title, #content .card-text,
|
||||
#content .member, #content .member-name, #content .member-initials,
|
||||
#content .checklist-item, #content .checklist-title, #content .comment,
|
||||
#content .activity, #content .activity-text, #content .activity-time,
|
||||
#content .board-title, #content .board-description, #content .list-name,
|
||||
#content .card-text, #content .card-title, #content .card-description,
|
||||
#content .swimlane-title, #content .swimlane-description,
|
||||
#content .board-header-title, #content .board-header-description,
|
||||
#content .card-detail-title, #content .card-detail-description,
|
||||
#content .list-header-title, #content .list-header-description,
|
||||
#content .swimlane-header-title, #content .swimlane-header-description,
|
||||
#content .minicard-title, #content .minicard-description,
|
||||
#content .card-comment, #content .card-comment-text,
|
||||
#content .checklist-item-text, #content .checklist-item-title,
|
||||
#content .activity-item, #content .activity-item-text,
|
||||
#content .board-member, #content .board-member-name,
|
||||
#content .team-member, #content .team-member-name,
|
||||
#content .org-member, #content .org-member-name,
|
||||
#content .template-member, #content .template-member-name,
|
||||
#content .user-name, #content .user-email, #content .user-role,
|
||||
#content .setting-title, #content .setting-description,
|
||||
#content .popup-title, #content .popup-description,
|
||||
#content .modal-title, #content .modal-description,
|
||||
#content .notification-title, #content .notification-text,
|
||||
#content .announcement-title, #content .announcement-text,
|
||||
#content .offline-warning-title, #content .offline-warning-text,
|
||||
#content .error-title, #content .error-text,
|
||||
#content .success-title, #content .success-text,
|
||||
#content .info-title, #content .info-text,
|
||||
#content .warning-title, #content .warning-text {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
/* Keep icons the same size (don't scale them) */
|
||||
#content .fa, #content .icon, #content i {
|
||||
font-size: 1em !important; /* Keep original icon size */
|
||||
}
|
||||
|
||||
/* Reset specific icon sizes to prevent double scaling */
|
||||
#content .fa-home, #content .fa-bars, #content .fa-search,
|
||||
#content .fa-bell, #content .fa-user, #content .fa-cog,
|
||||
#content .fa-plus, #content .fa-minus, #content .fa-edit,
|
||||
#content .fa-trash, #content .fa-save, #content .fa-cancel,
|
||||
#content .fa-arrow-left, #content .fa-arrow-right,
|
||||
#content .fa-arrow-up, #content .fa-arrow-down,
|
||||
#content .fa-check, #content .fa-times, #content .fa-close,
|
||||
#content .fa-star, #content .fa-heart, #content .fa-thumbs-up,
|
||||
#content .fa-thumbs-down, #content .fa-comment, #content .fa-reply,
|
||||
#content .fa-share, #content .fa-download, #content .fa-upload,
|
||||
#content .fa-copy, #content .fa-paste, #content .fa-cut,
|
||||
#content .fa-undo, #content .fa-redo, #content .fa-refresh,
|
||||
#content .fa-sync, #content .fa-spinner, #content .fa-loading,
|
||||
#content .fa-info, #content .fa-question, #content .fa-exclamation,
|
||||
#content .fa-warning, #content .fa-error, #content .fa-success,
|
||||
#content .fa-check-circle, #content .fa-times-circle,
|
||||
#content .fa-exclamation-circle, #content .fa-question-circle,
|
||||
#content .fa-info-circle, #content .fa-warning-circle,
|
||||
#content .fa-error-circle, #content .fa-success-circle {
|
||||
font-size: 1em !important; /* Keep original icon size */
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback for iPhone devices using JavaScript detection */
|
||||
.iphone-device #content {
|
||||
font-size: 2em !important; /* 2x bigger base font size for content area */
|
||||
}
|
||||
|
||||
.iphone-device #content h1, .iphone-device #content h2, .iphone-device #content h3, .iphone-device #content h4, .iphone-device #content h5, .iphone-device #content h6,
|
||||
.iphone-device #content p, .iphone-device #content span, .iphone-device #content div, .iphone-device #content a, .iphone-device #content button,
|
||||
.iphone-device #content .minicard, .iphone-device #content .list-header-name, .iphone-device #content .board-header-btn,
|
||||
.iphone-device #content .card-title, .iphone-device #content .card-details, .iphone-device #content .card-description,
|
||||
.iphone-device #content .swimlane-header, .iphone-device #content .list-title, .iphone-device #content .card-text,
|
||||
.iphone-device #content .member, .iphone-device #content .member-name, .iphone-device #content .member-initials,
|
||||
.iphone-device #content .checklist-item, .iphone-device #content .checklist-title, .iphone-device #content .comment,
|
||||
.iphone-device #content .activity, .iphone-device #content .activity-text, .iphone-device #content .activity-time,
|
||||
.iphone-device #content .board-title, .iphone-device #content .board-description, .iphone-device #content .list-name,
|
||||
.iphone-device #content .card-text, .iphone-device #content .card-title, .iphone-device #content .card-description,
|
||||
.iphone-device #content .swimlane-title, .iphone-device #content .swimlane-description,
|
||||
.iphone-device #content .board-header-title, .iphone-device #content .board-header-description,
|
||||
.iphone-device #content .card-detail-title, .iphone-device #content .card-detail-description,
|
||||
.iphone-device #content .list-header-title, .iphone-device #content .list-header-description,
|
||||
.iphone-device #content .swimlane-header-title, .iphone-device #content .swimlane-header-description,
|
||||
.iphone-device #content .minicard-title, .iphone-device #content .minicard-description,
|
||||
.iphone-device #content .card-comment, .iphone-device #content .card-comment-text,
|
||||
.iphone-device #content .checklist-item-text, .iphone-device #content .checklist-item-title,
|
||||
.iphone-device #content .activity-item, .iphone-device #content .activity-item-text,
|
||||
.iphone-device #content .board-member, .iphone-device #content .board-member-name,
|
||||
.iphone-device #content .team-member, .iphone-device #content .team-member-name,
|
||||
.iphone-device #content .org-member, .iphone-device #content .org-member-name,
|
||||
.iphone-device #content .template-member, .iphone-device #content .template-member-name,
|
||||
.iphone-device #content .user-name, .iphone-device #content .user-email, .iphone-device #content .user-role,
|
||||
.iphone-device #content .setting-title, .iphone-device #content .setting-description,
|
||||
.iphone-device #content .popup-title, .iphone-device #content .popup-description,
|
||||
.iphone-device #content .modal-title, .iphone-device #content .modal-description,
|
||||
.iphone-device #content .notification-title, .iphone-device #content .notification-text,
|
||||
.iphone-device #content .announcement-title, .iphone-device #content .announcement-text,
|
||||
.iphone-device #content .offline-warning-title, .iphone-device #content .offline-warning-text,
|
||||
.iphone-device #content .error-title, .iphone-device #content .error-text,
|
||||
.iphone-device #content .success-title, .iphone-device #content .success-text,
|
||||
.iphone-device #content .info-title, .iphone-device #content .info-text,
|
||||
.iphone-device #content .warning-title, .iphone-device #content .warning-text {
|
||||
font-size: 1em !important; /* Use inherited 2x scaling */
|
||||
}
|
||||
|
||||
/* Keep icons the same size for iPhone devices */
|
||||
.iphone-device #content .fa, .iphone-device #content .icon, .iphone-device #content i {
|
||||
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;
|
||||
}
|
||||
|
||||
#content[style*="overflow-x: auto"]::-webkit-scrollbar:horizontal {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
/* Force both scrollbars to always be visible for high zoom levels */
|
||||
#content[style*="overflow-x: auto"][style*="overflow-y: auto"] {
|
||||
overflow-x: scroll !important;
|
||||
overflow-y: scroll !important;
|
||||
}
|
||||
|
||||
/* Ensure the right scrollbar is always visible on the inner right edge */
|
||||
#content[style*="overflow-y: auto"] {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
.board-wrapper .board-canvas .board-overlay {
|
||||
position: fixed;
|
||||
@@ -29,19 +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;
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
|
||||
.board-wrapper.mobile-view {
|
||||
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: 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: block !important;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||
.board-wrapper {
|
||||
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: 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 0px 0px 0;
|
||||
overflow-x: hidden;
|
||||
padding: 0;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
}
|
||||
}
|
||||
.calendar-event-green {
|
||||
@@ -219,3 +564,10 @@
|
||||
font-size: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Global file drag over state for board canvas */
|
||||
.board-canvas.file-drag-over {
|
||||
background-color: rgba(0, 123, 255, 0.05) !important;
|
||||
border: 2px dashed #007bff !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
template(name="board")
|
||||
if isBoardReady.get
|
||||
|
||||
if isConverting.get
|
||||
+boardConversionProgress
|
||||
else if isBoardReady.get
|
||||
if currentBoard
|
||||
if onlyShowCurrentCard
|
||||
+cardDetails(currentCard)
|
||||
@@ -16,13 +19,18 @@ template(name="boardBody")
|
||||
if notDisplayThisBoard
|
||||
| {{_ 'tableVisibilityMode-allowPrivateOnly'}}
|
||||
else
|
||||
.board-wrapper(class=currentBoard.colorClass)
|
||||
// Debug information (remove in production)
|
||||
if debugBoardState
|
||||
.debug-info(style="position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.8); color: white; padding: 10px; z-index: 9999; font-size: 12px;")
|
||||
| {{_ 'board'}}: {{currentBoard.title}} | {{_ 'view'}}: {{boardView}} | {{_ 'has-swimlanes'}}: {{hasSwimlanes}} | {{_ 'swimlanes'}}: {{currentBoard.swimlanes.length}}
|
||||
.board-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
.board-canvas.js-swimlanes(
|
||||
class="{{#if hasSwimlanes}}dragscroll{{/if}}"
|
||||
class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
|
||||
class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
|
||||
class="{{#if draggingActive.get}}is-dragging-active{{/if}}"
|
||||
class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}")
|
||||
class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}"
|
||||
class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
if showOverlay.get
|
||||
.board-overlay
|
||||
if currentBoard.isTemplatesBoard
|
||||
@@ -33,15 +41,25 @@ template(name="boardBody")
|
||||
each currentBoard.swimlanes
|
||||
+swimlane(this)
|
||||
else
|
||||
a.js-empty-board-add-swimlane(title="{{_ 'add-swimlane'}}")
|
||||
h1.big-message.quiet
|
||||
| {{_ 'add-swimlane'}} +
|
||||
// Fallback: If no swimlanes exist, show lists instead of empty message
|
||||
+listsGroup(currentBoard)
|
||||
else if isViewLists
|
||||
+listsGroup(currentBoard)
|
||||
else if isViewCalendar
|
||||
+calendarView
|
||||
else if isViewGantt
|
||||
+ganttView
|
||||
else
|
||||
+listsGroup(currentBoard)
|
||||
// Default view - show swimlanes if they exist, otherwise show lists
|
||||
if hasSwimlanes
|
||||
each currentBoard.swimlanes
|
||||
+swimlane(this)
|
||||
else
|
||||
+listsGroup(currentBoard)
|
||||
//- Render multiple open cards in desktop mode
|
||||
unless isMiniScreen
|
||||
each openCards
|
||||
+cardDetails(this cardIndex=@index)
|
||||
+sidebar
|
||||
|
||||
template(name="calendarView")
|
||||
@@ -49,4 +67,4 @@ template(name="calendarView")
|
||||
.calendar-view.swimlane
|
||||
if currentCard
|
||||
+cardDetails(currentCard)
|
||||
+fullcalendar(calendarOptions)
|
||||
+fullcalendar(calendarOptions)
|
||||
@@ -1,6 +1,12 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import '../gantt/gantt.js';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import dragscroll from '@wekanteam/dragscroll';
|
||||
import { boardConverter } from '/client/lib/boardConverter';
|
||||
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
|
||||
import Swimlanes from '/models/swimlanes';
|
||||
import Lists from '/models/lists';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
const { calculateIndex } = Utils;
|
||||
@@ -9,6 +15,10 @@ const swimlaneWhileSortingHeight = 150;
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.isBoardReady = new ReactiveVar(false);
|
||||
this.isConverting = new ReactiveVar(false);
|
||||
this._swimlaneCreated = new Set(); // Track boards where we've created swimlanes
|
||||
this._boardProcessed = false; // Track if board has been processed
|
||||
this._lastProcessedBoardId = null; // Track last processed board ID
|
||||
|
||||
// The pattern we use to manually handle data loading is described here:
|
||||
// https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
|
||||
@@ -17,22 +27,116 @@ BlazeComponent.extendComponent({
|
||||
this.autorun(() => {
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
if (!currentBoardId) return;
|
||||
|
||||
const handle = subManager.subscribe('board', currentBoardId, false);
|
||||
Tracker.nonreactive(() => {
|
||||
Tracker.autorun(() => {
|
||||
this.isBoardReady.set(handle.ready());
|
||||
});
|
||||
|
||||
// Use a separate autorun for subscription ready state to avoid reactive loops
|
||||
this.subscriptionReadyAutorun = Tracker.autorun(() => {
|
||||
if (handle.ready()) {
|
||||
if (!this._boardProcessed || this._lastProcessedBoardId !== currentBoardId) {
|
||||
this._boardProcessed = true;
|
||||
this._lastProcessedBoardId = currentBoardId;
|
||||
|
||||
// Ensure default swimlane exists (only once per board)
|
||||
this.ensureDefaultSwimlane(currentBoardId);
|
||||
// Check if board needs conversion
|
||||
this.checkAndConvertBoard(currentBoardId);
|
||||
}
|
||||
} else {
|
||||
this.isBoardReady.set(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onDestroyed() {
|
||||
// Clean up the subscription ready autorun to prevent memory leaks
|
||||
if (this.subscriptionReadyAutorun) {
|
||||
this.subscriptionReadyAutorun.stop();
|
||||
}
|
||||
},
|
||||
|
||||
ensureDefaultSwimlane(boardId) {
|
||||
// Only create swimlane once per board
|
||||
if (this._swimlaneCreated.has(boardId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) return;
|
||||
|
||||
const swimlanes = board.swimlanes();
|
||||
|
||||
if (swimlanes.length === 0) {
|
||||
// Check if any swimlane exists in the database to avoid race conditions
|
||||
const existingSwimlanes = ReactiveCache.getSwimlanes({ boardId });
|
||||
if (existingSwimlanes.length === 0) {
|
||||
const swimlaneId = Swimlanes.insert({
|
||||
title: 'Default',
|
||||
boardId: boardId,
|
||||
});
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Created default swimlane ${swimlaneId} for board ${boardId}`);
|
||||
}
|
||||
}
|
||||
this._swimlaneCreated.add(boardId);
|
||||
} else {
|
||||
this._swimlaneCreated.add(boardId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating default swimlane:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async checkAndConvertBoard(boardId) {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) {
|
||||
this.isBoardReady.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isBoardReady.set(true);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during board conversion check:', error);
|
||||
this.isConverting.set(false);
|
||||
this.isBoardReady.set(true); // Show board even if conversion check failed
|
||||
}
|
||||
},
|
||||
|
||||
onlyShowCurrentCard() {
|
||||
return Utils.isMiniScreen() && Utils.getCurrentCardId(true);
|
||||
const isMiniScreen = Utils.isMiniScreen();
|
||||
const currentCardId = Utils.getCurrentCardId(true);
|
||||
return isMiniScreen && currentCardId;
|
||||
},
|
||||
|
||||
openCards() {
|
||||
// In desktop mode, return array of all open cards
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (!isMobile) {
|
||||
const openCardIds = Session.get('openCards') || [];
|
||||
return openCardIds.map(id => ReactiveCache.getCard(id)).filter(card => card);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
goHome() {
|
||||
FlowRouter.go('home');
|
||||
},
|
||||
|
||||
isConverting() {
|
||||
return this.isConverting.get();
|
||||
},
|
||||
|
||||
isBoardReady() {
|
||||
return this.isBoardReady.get();
|
||||
},
|
||||
|
||||
currentBoard() {
|
||||
return Utils.getCurrentBoard();
|
||||
},
|
||||
}).register('board');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
@@ -43,39 +147,241 @@ BlazeComponent.extendComponent({
|
||||
this._isDragging = false;
|
||||
// Used to set the overlay
|
||||
this.mouseHasEnterCardDetails = false;
|
||||
this._sortFieldsFixed = new Set(); // Track which boards have had sort fields fixed
|
||||
|
||||
// fix swimlanes sort field if there are null values
|
||||
const currentBoardData = Utils.getCurrentBoard();
|
||||
const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
|
||||
if (nullSortSwimlanes.length > 0) {
|
||||
const swimlanes = currentBoardData.swimlanes();
|
||||
let count = 0;
|
||||
swimlanes.forEach(s => {
|
||||
Swimlanes.update(s._id, {
|
||||
$set: {
|
||||
sort: count,
|
||||
},
|
||||
});
|
||||
count += 1;
|
||||
});
|
||||
if (currentBoardData && Swimlanes) {
|
||||
const boardId = currentBoardData._id;
|
||||
// Only fix sort fields once per board to prevent reactive loops
|
||||
if (!this._sortFieldsFixed.has(`swimlanes-${boardId}`)) {
|
||||
const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
|
||||
if (nullSortSwimlanes.length > 0) {
|
||||
const swimlanes = currentBoardData.swimlanes();
|
||||
let count = 0;
|
||||
swimlanes.forEach(s => {
|
||||
Swimlanes.update(s._id, {
|
||||
$set: {
|
||||
sort: count,
|
||||
},
|
||||
});
|
||||
count += 1;
|
||||
});
|
||||
}
|
||||
this._sortFieldsFixed.add(`swimlanes-${boardId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// fix lists sort field if there are null values
|
||||
const nullSortLists = currentBoardData.nullSortLists();
|
||||
if (nullSortLists.length > 0) {
|
||||
const lists = currentBoardData.lists();
|
||||
let count = 0;
|
||||
lists.forEach(l => {
|
||||
Lists.update(l._id, {
|
||||
$set: {
|
||||
sort: count,
|
||||
},
|
||||
});
|
||||
count += 1;
|
||||
});
|
||||
if (currentBoardData && Lists) {
|
||||
const boardId = currentBoardData._id;
|
||||
// Only fix sort fields once per board to prevent reactive loops
|
||||
if (!this._sortFieldsFixed.has(`lists-${boardId}`)) {
|
||||
const nullSortLists = currentBoardData.nullSortLists();
|
||||
if (nullSortLists.length > 0) {
|
||||
const lists = currentBoardData.lists();
|
||||
let count = 0;
|
||||
lists.forEach(l => {
|
||||
Lists.update(l._id, {
|
||||
$set: {
|
||||
sort: count,
|
||||
},
|
||||
});
|
||||
count += 1;
|
||||
});
|
||||
}
|
||||
this._sortFieldsFixed.add(`lists-${boardId}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
onRendered() {
|
||||
// Initialize user settings (zoom and mobile mode)
|
||||
Utils.initializeUserSettings();
|
||||
|
||||
// Detect iPhone devices and add class for better CSS targeting
|
||||
const isIPhone = /iPhone|iPod/.test(navigator.userAgent);
|
||||
if (isIPhone) {
|
||||
document.body.classList.add('iphone-device');
|
||||
}
|
||||
|
||||
// Accessibility: Focus management for popups and menus
|
||||
function focusFirstInteractive(container) {
|
||||
if (!container) return;
|
||||
// Find first focusable element
|
||||
const focusable = container.querySelectorAll('button, [role="button"], a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
for (let i = 0; i < focusable.length; i++) {
|
||||
if (!focusable[i].disabled && focusable[i].offsetParent !== null) {
|
||||
focusable[i].focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Observe for new popups/menus and set focus (but exclude swimlane content)
|
||||
const popupObserver = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
mutation.addedNodes.forEach(function(node) {
|
||||
if (node.nodeType === 1 &&
|
||||
(node.classList.contains('popup') || node.classList.contains('modal') || node.classList.contains('menu')) &&
|
||||
!node.closest('.js-swimlanes') &&
|
||||
!node.closest('.swimlane') &&
|
||||
!node.closest('.list') &&
|
||||
!node.closest('.minicard')) {
|
||||
setTimeout(function() { focusFirstInteractive(node); }, 10);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
popupObserver.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// Remove tabindex from non-interactive elements (e.g., user abbreviations, labels)
|
||||
document.querySelectorAll('.user-abbreviation, .user-label, .card-header-label, .edit-label, .private-label').forEach(function(el) {
|
||||
if (el.hasAttribute('tabindex')) {
|
||||
el.removeAttribute('tabindex');
|
||||
}
|
||||
});
|
||||
/*
|
||||
// Add a toggle button for keyboard shortcuts accessibility
|
||||
if (!document.getElementById('wekan-shortcuts-toggle')) {
|
||||
const toggleContainer = document.createElement('div');
|
||||
toggleContainer.id = 'wekan-shortcuts-toggle';
|
||||
toggleContainer.style.position = 'fixed';
|
||||
toggleContainer.style.top = '10px';
|
||||
toggleContainer.style.right = '10px';
|
||||
toggleContainer.style.zIndex = '1000';
|
||||
toggleContainer.style.background = '#fff';
|
||||
toggleContainer.style.border = '2px solid #005fcc';
|
||||
toggleContainer.style.borderRadius = '6px';
|
||||
toggleContainer.style.padding = '8px 12px';
|
||||
toggleContainer.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
|
||||
toggleContainer.style.fontSize = '16px';
|
||||
toggleContainer.style.color = '#005fcc';
|
||||
toggleContainer.setAttribute('role', 'region');
|
||||
toggleContainer.setAttribute('aria-label', 'Keyboard Shortcuts Settings');
|
||||
toggleContainer.innerHTML = `
|
||||
<label for="shortcuts-toggle-checkbox" style="cursor:pointer;">
|
||||
<input type="checkbox" id="shortcuts-toggle-checkbox" ${window.wekanShortcutsEnabled ? 'checked' : ''} style="margin-right:8px;" />
|
||||
Enable keyboard shortcuts
|
||||
</label>
|
||||
`;
|
||||
document.body.appendChild(toggleContainer);
|
||||
const checkbox = document.getElementById('shortcuts-toggle-checkbox');
|
||||
checkbox.addEventListener('change', function(e) {
|
||||
window.toggleWekanShortcuts(e.target.checked);
|
||||
});
|
||||
}
|
||||
*/
|
||||
// Ensure toggle-buttons, color choices, reactions, renaming, and calendar controls are focusable and have ARIA roles
|
||||
document.querySelectorAll('.js-toggle').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
// Short, descriptive label for favorite/star toggle
|
||||
if (el.classList.contains('js-favorite-toggle')) {
|
||||
el.setAttribute('aria-label', TAPi18n.__('favorite-toggle-label'));
|
||||
} else {
|
||||
el.setAttribute('aria-label', 'Toggle');
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('.js-color-choice').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
el.setAttribute('aria-label', 'Choose color');
|
||||
});
|
||||
document.querySelectorAll('.js-reaction').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
el.setAttribute('aria-label', 'React');
|
||||
});
|
||||
document.querySelectorAll('.js-rename-swimlane').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
el.setAttribute('aria-label', 'Rename swimlane');
|
||||
});
|
||||
document.querySelectorAll('.js-rename-list').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
el.setAttribute('aria-label', 'Rename list');
|
||||
});
|
||||
document.querySelectorAll('.fc-button').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
});
|
||||
// Set the language attribute on the <html> element for accessibility
|
||||
document.documentElement.lang = TAPi18n.getLanguage();
|
||||
|
||||
// Ensure the accessible name for the board view switcher matches the visible label "Swimlanes"
|
||||
// This fixes WCAG 2.5.3: Label in Name
|
||||
const swimlanesSwitcher = this.$('.js-board-view-swimlanes');
|
||||
if (swimlanesSwitcher.length) {
|
||||
swimlanesSwitcher.attr('aria-label', swimlanesSwitcher.text().trim() || 'Swimlanes');
|
||||
}
|
||||
|
||||
// Add a highly visible focus indicator and improve contrast for interactive elements
|
||||
if (!document.getElementById('wekan-accessible-focus-style')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'wekan-accessible-focus-style';
|
||||
style.innerHTML = `
|
||||
/* Focus indicator */
|
||||
button:focus, [role="button"]:focus, a:focus, input:focus, select:focus, textarea:focus, .dropdown-menu:focus, .js-board-view-swimlanes:focus, .js-add-card:focus {
|
||||
outline: 3px solid #005fcc !important;
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
/* Input borders */
|
||||
input, textarea, select {
|
||||
border: 2px solid #222 !important;
|
||||
}
|
||||
/* Plus icon for adding a new card */
|
||||
.js-add-card {
|
||||
color: #005fcc !important; /* dark blue for contrast */
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.js-add-card[tabindex] {
|
||||
outline: none;
|
||||
}
|
||||
/* Hamburger menu */
|
||||
.fa-bars, .icon-hamburger {
|
||||
color: #222 !important;
|
||||
}
|
||||
/* Grey icons in card detail header */
|
||||
.card-detail-header .fa, .card-detail-header .icon {
|
||||
color: #444 !important;
|
||||
}
|
||||
/* Grey operating elements in card detail */
|
||||
.card-detail .fa, .card-detail .icon {
|
||||
color: #444 !important;
|
||||
}
|
||||
/* Blue bar in checklists */
|
||||
.checklist-progress-bar {
|
||||
background-color: #005fcc !important;
|
||||
}
|
||||
/* Green checkmark in checklists */
|
||||
.checklist .fa-check {
|
||||
color: #007a33 !important;
|
||||
}
|
||||
/* X-Button and arrow button in menus */
|
||||
.close, .fa-arrow-left, .icon-arrow-left {
|
||||
color: #005fcc !important;
|
||||
}
|
||||
/* Cross icon to move boards */
|
||||
.js-move-board {
|
||||
color: #005fcc !important;
|
||||
}
|
||||
/* Current date background */
|
||||
.current-date {
|
||||
background-color: #005fcc !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
// Ensure plus/add elements are focusable and have ARIA roles
|
||||
document.querySelectorAll('.js-add-card').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
el.setAttribute('aria-label', 'Add new card');
|
||||
});
|
||||
|
||||
const boardComponent = this;
|
||||
const $swimlanesDom = boardComponent.$('.js-swimlanes');
|
||||
|
||||
@@ -198,22 +504,20 @@ BlazeComponent.extendComponent({
|
||||
// Always reset dragscroll on view switch
|
||||
dragscroll.reset();
|
||||
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$swimlanesDom.sortable({
|
||||
handle: '.js-swimlane-header-handle',
|
||||
});
|
||||
} else {
|
||||
$swimlanesDom.sortable({
|
||||
handle: '.swimlane-header',
|
||||
});
|
||||
}
|
||||
if ($swimlanesDom.data('uiSortable') || $swimlanesDom.data('sortable')) {
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$swimlanesDom.sortable('option', 'handle', '.js-swimlane-header-handle');
|
||||
} else {
|
||||
$swimlanesDom.sortable('option', 'handle', '.swimlane-header');
|
||||
}
|
||||
|
||||
// Disable drag-dropping if the current user is not a board member
|
||||
$swimlanesDom.sortable(
|
||||
'option',
|
||||
'disabled',
|
||||
!ReactiveCache.getCurrentUser()?.isBoardAdmin(),
|
||||
);
|
||||
// Disable drag-dropping if the current user is not a board member
|
||||
$swimlanesDom.sortable(
|
||||
'option',
|
||||
'disabled',
|
||||
!ReactiveCache.getCurrentUser()?.isBoardAdmin(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// If there is no data in the board (ie, no lists) we autofocus the list
|
||||
@@ -230,51 +534,132 @@ BlazeComponent.extendComponent({
|
||||
notDisplayThisBoard() {
|
||||
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
|
||||
let currentBoard = Utils.getCurrentBoard();
|
||||
if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard.permission == 'public') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard && currentBoard.permission == 'public';
|
||||
},
|
||||
|
||||
isViewSwimlanes() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
if (currentUser) {
|
||||
return (currentUser.profile || {}).boardView === 'board-view-swimlanes';
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
return (
|
||||
window.localStorage.getItem('boardView') === 'board-view-swimlanes'
|
||||
);
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
},
|
||||
|
||||
hasSwimlanes() {
|
||||
return Utils.getCurrentBoard().swimlanes().length > 0;
|
||||
|
||||
// If no board view is set, default to swimlanes
|
||||
if (!boardView) {
|
||||
boardView = 'board-view-swimlanes';
|
||||
}
|
||||
|
||||
return boardView === 'board-view-swimlanes';
|
||||
},
|
||||
|
||||
isViewLists() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
if (currentUser) {
|
||||
return (currentUser.profile || {}).boardView === 'board-view-lists';
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
return window.localStorage.getItem('boardView') === 'board-view-lists';
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
|
||||
return boardView === 'board-view-lists';
|
||||
},
|
||||
|
||||
isViewCalendar() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
if (currentUser) {
|
||||
return (currentUser.profile || {}).boardView === 'board-view-cal';
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
return window.localStorage.getItem('boardView') === 'board-view-cal';
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
|
||||
return boardView === 'board-view-cal';
|
||||
},
|
||||
|
||||
isViewGantt() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
if (currentUser) {
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
|
||||
return boardView === 'board-view-gantt';
|
||||
},
|
||||
|
||||
hasSwimlanes() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
if (!currentBoard) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('hasSwimlanes: No current board');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const swimlanes = currentBoard.swimlanes();
|
||||
const hasSwimlanes = swimlanes && swimlanes.length > 0;
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('hasSwimlanes: Board has', swimlanes ? swimlanes.length : 0, 'swimlanes');
|
||||
}
|
||||
return hasSwimlanes;
|
||||
} catch (error) {
|
||||
console.error('hasSwimlanes: Error getting swimlanes:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
isVerticalScrollbars() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
return user && user.isVerticalScrollbars();
|
||||
},
|
||||
|
||||
boardView() {
|
||||
return Utils.boardView();
|
||||
},
|
||||
|
||||
debugBoardState() {
|
||||
// Enable debug mode by setting ?debug=1 in URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('debug') === '1';
|
||||
},
|
||||
|
||||
debugBoardStateData() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
const isBoardReady = this.isBoardReady.get();
|
||||
const isConverting = this.isConverting.get();
|
||||
const boardView = Utils.boardView();
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('=== BOARD DEBUG STATE ===');
|
||||
console.log('currentBoardId:', currentBoardId);
|
||||
console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none');
|
||||
console.log('isBoardReady:', isBoardReady);
|
||||
console.log('isConverting:', isConverting);
|
||||
console.log('boardView:', boardView);
|
||||
console.log('========================');
|
||||
}
|
||||
|
||||
return {
|
||||
currentBoardId,
|
||||
hasCurrentBoard: !!currentBoard,
|
||||
currentBoardTitle: currentBoard ? currentBoard.title : 'none',
|
||||
isBoardReady,
|
||||
isConverting,
|
||||
boardView
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
openNewListForm() {
|
||||
if (this.isViewSwimlanes()) {
|
||||
// The form had been removed in 416b17062e57f215206e93a85b02ef9eb1ab4902
|
||||
@@ -298,6 +683,31 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
},
|
||||
'click .js-empty-board-add-swimlane': Popup.open('swimlaneAdd'),
|
||||
// Global drag and drop file upload handlers for better visual feedback
|
||||
'dragover .board-canvas'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
// Add visual indicator that files can be dropped
|
||||
$('.board-canvas').addClass('file-drag-over');
|
||||
}
|
||||
},
|
||||
'dragleave .board-canvas'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
// Only remove class if we're leaving the board canvas entirely
|
||||
if (!event.currentTarget.contains(event.relatedTarget)) {
|
||||
$('.board-canvas').removeClass('file-drag-over');
|
||||
}
|
||||
}
|
||||
},
|
||||
'drop .board-canvas'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
$('.board-canvas').removeClass('file-drag-over');
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
@@ -326,8 +736,109 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
}).register('boardBody');
|
||||
|
||||
// Accessibility: Allow users to enable/disable keyboard shortcuts
|
||||
window.wekanShortcutsEnabled = true;
|
||||
window.toggleWekanShortcuts = function(enabled) {
|
||||
window.wekanShortcutsEnabled = !!enabled;
|
||||
};
|
||||
|
||||
// Example: Wrap your character key shortcut handler like this
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// Example: "W" key shortcut (replace with your actual shortcut logic)
|
||||
if (!window.wekanShortcutsEnabled) return;
|
||||
if (e.key === 'w' || e.key === 'W') {
|
||||
// ...existing shortcut logic...
|
||||
// e.g. open swimlanes view, etc.
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard accessibility for card actions (favorite, archive, duplicate, etc.)
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (!window.wekanShortcutsEnabled) return;
|
||||
// Only proceed if focus is on a card action element
|
||||
const active = document.activeElement;
|
||||
if (active && active.classList.contains('js-card-action')) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
active.click();
|
||||
}
|
||||
// Move card up/down with arrow keys
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (active.dataset.cardId) {
|
||||
Meteor.call('moveCardUp', active.dataset.cardId);
|
||||
}
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (active.dataset.cardId) {
|
||||
Meteor.call('moveCardDown', active.dataset.cardId);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Make plus/add elements keyboard accessible
|
||||
if (active && active.classList.contains('js-add-card')) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
active.click();
|
||||
}
|
||||
}
|
||||
// Keyboard move for cards (alternative to drag & drop)
|
||||
if (active && active.classList.contains('js-move-card')) {
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (active.dataset.cardId) {
|
||||
Meteor.call('moveCardUp', active.dataset.cardId);
|
||||
}
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (active.dataset.cardId) {
|
||||
Meteor.call('moveCardDown', active.dataset.cardId);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ensure move card buttons are focusable and have ARIA roles
|
||||
document.querySelectorAll('.js-move-card').forEach(function(el) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
el.setAttribute('aria-label', 'Move card');
|
||||
});
|
||||
// Make toggle-buttons, color choices, reactions, and X-buttons keyboard accessible
|
||||
if (active && (active.classList.contains('js-toggle') || active.classList.contains('js-color-choice') || active.classList.contains('js-reaction') || active.classList.contains('close'))) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
active.click();
|
||||
}
|
||||
}
|
||||
// Prevent scripts from removing focus when received
|
||||
if (active) {
|
||||
active.addEventListener('focus', function(e) {
|
||||
// Do not remove focus
|
||||
// No-op: This prevents F55 failure
|
||||
}, { once: true });
|
||||
}
|
||||
// Make swimlane/list renaming keyboard accessible
|
||||
if (active && (active.classList.contains('js-rename-swimlane') || active.classList.contains('js-rename-list'))) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
active.click();
|
||||
}
|
||||
}
|
||||
// Calendar navigation buttons
|
||||
if (active && active.classList.contains('fc-button')) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
active.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
// Set the language attribute on the <html> element for accessibility
|
||||
document.documentElement.lang = TAPi18n.getLanguage();
|
||||
|
||||
this.autorun(function () {
|
||||
$('#calendar-view').fullCalendar('refetchEvents');
|
||||
});
|
||||
@@ -341,11 +852,19 @@ BlazeComponent.extendComponent({
|
||||
timezone: 'local',
|
||||
weekNumbers: true,
|
||||
header: {
|
||||
left: 'title today prev,next',
|
||||
left: 'title today prev,next',
|
||||
center:
|
||||
'agendaDay,listDay,timelineDay agendaWeek,listWeek,timelineWeek month,listMonth',
|
||||
right: '',
|
||||
},
|
||||
buttonText: {
|
||||
prev: TAPi18n.__('calendar-previous-month-label'), // e.g. "Previous month"
|
||||
next: TAPi18n.__('calendar-next-month-label'), // e.g. "Next month"
|
||||
},
|
||||
ariaLabel: {
|
||||
prev: TAPi18n.__('calendar-previous-month-label'),
|
||||
next: TAPi18n.__('calendar-next-month-label'),
|
||||
},
|
||||
// height: 'parent', nope, doesn't work as the parent might be small
|
||||
height: 'auto',
|
||||
/* TODO: lists as resources: https://fullcalendar.io/docs/vertical-resource-view */
|
||||
@@ -465,9 +984,13 @@ BlazeComponent.extendComponent({
|
||||
const firstSwimlane = currentBoard.swimlanes()[0];
|
||||
Meteor.call('createCardWithDueDate', currentBoard._id, firstList._id, myTitle, startDate.toDate(), firstSwimlane._id, function(error, result) {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(error);
|
||||
}
|
||||
} else {
|
||||
console.log("Card Created", result);
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log("Card Created", result);
|
||||
}
|
||||
}
|
||||
});
|
||||
closeModal();
|
||||
@@ -476,6 +999,9 @@ BlazeComponent.extendComponent({
|
||||
document.body.appendChild(modalElement);
|
||||
const openModal = function() {
|
||||
modalElement.style.display = 'flex';
|
||||
// Set focus to the input field for better keyboard accessibility
|
||||
const input = modalElement.querySelector('#card-title-input');
|
||||
if (input) input.focus();
|
||||
};
|
||||
const closeModal = function() {
|
||||
modalElement.style.display = 'none';
|
||||
@@ -495,3 +1021,8 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
},
|
||||
}).register('calendarView');
|
||||
/**
|
||||
* Gantt View Component
|
||||
* Displays cards as a Gantt chart with start/due dates
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - NEPHRITIS
|
||||
=================*/
|
||||
|
||||
@@ -99,7 +99,7 @@ THEME - NEPHRITIS
|
||||
|
||||
/* === END NEPHRITIS THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Pomegranate
|
||||
=================*/
|
||||
|
||||
@@ -200,7 +200,7 @@ THEME - Pomegranate
|
||||
|
||||
/* === END Pomegranate THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Belize
|
||||
=================*/
|
||||
|
||||
@@ -301,7 +301,7 @@ THEME - Belize
|
||||
|
||||
/* === END Belize THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Wisteria
|
||||
=================*/
|
||||
|
||||
@@ -402,7 +402,7 @@ THEME - Wisteria
|
||||
|
||||
/* === END Wisteria THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Midnight
|
||||
=================*/
|
||||
|
||||
@@ -503,7 +503,7 @@ THEME - Midnight
|
||||
|
||||
/* === END Midnight THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Pumpkin
|
||||
=================*/
|
||||
|
||||
@@ -604,7 +604,7 @@ THEME - Pumpkin
|
||||
|
||||
/* === END Pumpkin THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Moderate Pink
|
||||
=================*/
|
||||
|
||||
@@ -705,7 +705,7 @@ THEME - Moderate Pink
|
||||
|
||||
/* === END Moderatepink THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Strong Cyan
|
||||
=================*/
|
||||
|
||||
@@ -806,7 +806,7 @@ THEME - Strong Cyan
|
||||
|
||||
/* === END Strongcyan THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Lime Green
|
||||
=================*/
|
||||
|
||||
@@ -907,7 +907,7 @@ THEME - Lime Green
|
||||
|
||||
/* === END Limegreen THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Dark
|
||||
=================*/
|
||||
|
||||
@@ -1111,7 +1111,7 @@ THEME - Dark
|
||||
|
||||
/* === END Dark THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Relax
|
||||
=================*/
|
||||
|
||||
@@ -1241,7 +1241,7 @@ THEME - Relax
|
||||
|
||||
/* === END Relax THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Corteza
|
||||
=================*/
|
||||
|
||||
@@ -1342,7 +1342,7 @@ THEME - Corteza
|
||||
|
||||
/* === END Corteza THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Clear Blue
|
||||
=================*/
|
||||
|
||||
@@ -1425,7 +1425,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;
|
||||
}
|
||||
@@ -1609,7 +1609,7 @@ THEME - Clear Blue
|
||||
|
||||
/* === END Clearblue THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Natural
|
||||
=================*/
|
||||
|
||||
@@ -1719,7 +1719,7 @@ THEME - Natural
|
||||
|
||||
/* === END Natural THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Modern
|
||||
=================*/
|
||||
|
||||
@@ -2006,7 +2006,7 @@ THEME - Modern
|
||||
|
||||
/* === END Modern THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Modern Dark
|
||||
=================*/
|
||||
|
||||
@@ -2496,7 +2496,7 @@ THEME - Modern Dark
|
||||
/* === END ModernDark THEME === */
|
||||
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Exodark
|
||||
=================*/
|
||||
|
||||
@@ -2544,8 +2544,8 @@ THEME - Exodark
|
||||
background: #1b1b1b;
|
||||
}
|
||||
.board-color-exodark .materialCheckBox.is-checked {
|
||||
border-bottom: 2px solid #222;
|
||||
border-right: 2px solid #222;
|
||||
border-bottom: 2px solid #dbdbdb!important;/*Fix contrast of checkbox*/
|
||||
border-right: 2px solid #dbdbdb!important;
|
||||
}
|
||||
.board-color-exodark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e9e9e9;
|
||||
@@ -2588,7 +2588,7 @@ THEME - Exodark
|
||||
background: #222;
|
||||
}
|
||||
.board-color-exodark .list {
|
||||
margin: 10px;
|
||||
margin: 10px 0;
|
||||
color: #fff;
|
||||
border-radius: 15px;
|
||||
background-color: #1c1c1c;
|
||||
@@ -2744,7 +2744,7 @@ THEME - Exodark
|
||||
|
||||
/* === END Exodark THEME === */
|
||||
|
||||
/* ===============
|
||||
/* ===============
|
||||
THEME - Clean Dark
|
||||
=================*/
|
||||
|
||||
@@ -2811,6 +2811,7 @@ THEME - Clean Dark
|
||||
|
||||
.board-color-cleandark .card-details {
|
||||
background: #23232B;
|
||||
scrollbar-color: #ffffff #2e2e39;
|
||||
border-radius: 20px;
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -3179,10 +3180,17 @@ THEME - Clean Dark
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* ===============
|
||||
.board-color-cleandark.background-box {
|
||||
background-color:#23232B;
|
||||
} /*Fixes contrast issues with background box in theme selection list*/
|
||||
/* ===============
|
||||
THEME - Clean Light
|
||||
=================*/
|
||||
/* Please note Clean Light theme elements also contain references to some cleandark theme elements so if unable to find code you're looking for under CleanDark it might be here. This should probably be cleaned up*/
|
||||
.board-color-cleanlight.background-box {
|
||||
background-color:#e0e0e0;
|
||||
color:#010101!important;
|
||||
} /*Fixes issues with text colour/background box being similar no contrast */
|
||||
|
||||
.board-color-cleanlight {
|
||||
background: #E0E0E0;
|
||||
@@ -3314,7 +3322,13 @@ THEME - Clean Light
|
||||
|
||||
.board-color-cleanlight .list .list-header .list-header-menu .js-collapse,
|
||||
.board-color-cleandark .list .list-header .list-header-menu .js-collapse {
|
||||
display: none;
|
||||
/* Make collapse button visible in Clean Light / Clean Dark themes.
|
||||
Previously this was hidden which caused the missing Collapse button
|
||||
when using these themes. Use inline-block so it lines up with other
|
||||
header controls. */
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.board-color-cleanlight .list-header-add,
|
||||
@@ -3485,7 +3499,7 @@ THEME - Clean Light
|
||||
|
||||
.board-color-cleandark .card-details-header {
|
||||
background: #2E2E39 !important;
|
||||
color: rgba(255, 255, 255, 1);
|
||||
color: #FFF !important;
|
||||
}
|
||||
|
||||
.board-color-cleanlight .card-details-header .card-details-title,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.integration-form {
|
||||
padding: 5px;
|
||||
padding: 0.7vh 0.7vw;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.flex,
|
||||
@@ -12,12 +12,928 @@
|
||||
display: flex;
|
||||
}
|
||||
.option {
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
-webkit-border-radius: 0.4vw;
|
||||
border-radius: 0.4vw;
|
||||
background: #fff;
|
||||
text-decoration: none;
|
||||
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
margin-top: 5px;
|
||||
padding: 5px;
|
||||
-webkit-box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.2);
|
||||
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.2);
|
||||
margin-top: 0.7vh;
|
||||
padding: 0.7vh 0.7vw;
|
||||
}
|
||||
|
||||
/* Zoom and Mobile Mode Controls */
|
||||
.board-header-btns.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5vw;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 0.5vw;
|
||||
box-shadow: 0 0.2vh 0.5vh rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn {
|
||||
padding: 0.5vh 0.8vw !important;
|
||||
border-radius: 0.3vw !important;
|
||||
background: #fff !important;
|
||||
border: 1px solid #000 !important;
|
||||
transition: all 0.2s ease !important;
|
||||
color: #000 !important;
|
||||
height: auto !important;
|
||||
line-height: normal !important;
|
||||
margin: 0 !important;
|
||||
float: none !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn i {
|
||||
color: #000 !important;
|
||||
float: none !important;
|
||||
display: inline !important;
|
||||
line-height: normal !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn:hover {
|
||||
background: #000 !important;
|
||||
border-color: #000 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn:hover i {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn.is-active {
|
||||
background: #0079bf;
|
||||
color: white;
|
||||
border-color: #005a8a;
|
||||
}
|
||||
|
||||
.zoom-controls .board-header-btn.is-active i {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.zoom-level {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
min-width: 3vw;
|
||||
text-align: center;
|
||||
font-size: clamp(12px, 2vw, 14px);
|
||||
cursor: pointer;
|
||||
padding: 0.3vh 0.5vw;
|
||||
border-radius: 0.3vw;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.zoom-level:hover {
|
||||
background: #f0f0f0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Mobile Mode Styles */
|
||||
.mobile-mode .board-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mobile-mode .board-canvas {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mobile-mode .minicard {
|
||||
font-size: clamp(16px, 4vw, 20px);
|
||||
padding: 1.2vh 1.5vw 0.5vh;
|
||||
min-height: 3vh;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header-name {
|
||||
font-size: clamp(18px, 4.5vw, 24px);
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btn {
|
||||
padding: 1vh 1.5vw;
|
||||
font-size: clamp(14px, 3.5vw, 18px);
|
||||
}
|
||||
|
||||
.mobile-mode .zoom-controls {
|
||||
padding: 1vh 1.5vw;
|
||||
gap: 1vw;
|
||||
}
|
||||
|
||||
.mobile-mode .zoom-controls .board-header-btn {
|
||||
padding: 1vh 1.5vw !important;
|
||||
font-size: clamp(14px, 3.5vw, 18px) !important;
|
||||
background: #fff !important;
|
||||
border: 1px solid #000 !important;
|
||||
color: #000 !important;
|
||||
height: auto !important;
|
||||
line-height: normal !important;
|
||||
margin: 0 !important;
|
||||
float: none !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.mobile-mode .zoom-controls .board-header-btn i {
|
||||
color: #000 !important;
|
||||
float: none !important;
|
||||
display: inline !important;
|
||||
line-height: normal !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .zoom-controls .board-header-btn:hover {
|
||||
background: #000 !important;
|
||||
border-color: #000 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.mobile-mode .zoom-controls .board-header-btn:hover i {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.mobile-mode .zoom-level {
|
||||
font-size: clamp(14px, 3.5vw, 18px);
|
||||
min-width: 4vw;
|
||||
}
|
||||
|
||||
/* Comprehensive Mobile Mode Styles - Works on all screen sizes */
|
||||
.mobile-mode .board-wrapper {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
transform: none !important;
|
||||
transform-origin: initial !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-canvas {
|
||||
height: 100% !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin-bottom: 2rem !important;
|
||||
display: block !important;
|
||||
float: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane-header {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
padding: 1rem !important;
|
||||
margin-bottom: 1rem !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
display: block !important;
|
||||
float: none !important;
|
||||
margin-bottom: 2rem !important;
|
||||
border-left: none !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
clear: both !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
padding: 1rem !important;
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
display: grid !important;
|
||||
grid-template-columns: 30px 1fr auto auto !important;
|
||||
gap: 10px !important;
|
||||
align-items: center !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header .list-header-name {
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
font-weight: bold !important;
|
||||
grid-row: 1 !important;
|
||||
grid-column: 2 !important;
|
||||
align-self: end !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header .cardCount {
|
||||
font-size: clamp(14px, 2vw, 24px) !important;
|
||||
grid-row: 2 !important;
|
||||
grid-column: 2 !important;
|
||||
align-self: start !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header .list-header-menu-icon {
|
||||
position: static !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 3 !important;
|
||||
padding: 14px !important;
|
||||
font-size: clamp(24px, 3vw, 48px) !important;
|
||||
text-align: center !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header .list-header-handle {
|
||||
position: static !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 4 !important;
|
||||
padding: 14px !important;
|
||||
font-size: clamp(28px, 3.5vw, 56px) !important;
|
||||
text-align: center !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-body {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
padding: 1rem !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.mobile-mode .minicard {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
font-size: clamp(16px, 2vw, 24px) !important;
|
||||
padding: 1.2vh 1.5vw 0.5vh !important;
|
||||
min-height: 3vh !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
display: block !important;
|
||||
float: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .minicard .minicard-title {
|
||||
font-size: clamp(16px, 2vw, 24px) !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.mobile-mode .minicard .minicard-members {
|
||||
font-size: clamp(12px, 1.5vw, 18px) !important;
|
||||
}
|
||||
|
||||
.mobile-mode .minicard .minicard-lists {
|
||||
font-size: clamp(12px, 1.5vw, 18px) !important;
|
||||
}
|
||||
|
||||
/* Desktop Mode Styles */
|
||||
.desktop-mode .board-wrapper {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.desktop-mode .swimlane {
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
}
|
||||
|
||||
.desktop-mode .list {
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
display: flex !important;
|
||||
float: left !important;
|
||||
margin-bottom: 0 !important;
|
||||
border-left: 1px solid #ccc !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.desktop-mode .list-header {
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
padding: 2.5vh 1.5vw 0.5vh !important;
|
||||
font-size: clamp(14px, 3vw, 18px) !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.desktop-mode .list-header .list-header-name {
|
||||
font-size: clamp(14px, 3vw, 18px) !important;
|
||||
display: inline !important;
|
||||
grid-row: auto !important;
|
||||
grid-column: auto !important;
|
||||
align-self: auto !important;
|
||||
}
|
||||
|
||||
.desktop-mode .list-header .cardCount {
|
||||
font-size: 12px !important;
|
||||
grid-row: auto !important;
|
||||
grid-column: auto !important;
|
||||
align-self: auto !important;
|
||||
}
|
||||
|
||||
.desktop-mode .list-header .list-header-menu-icon {
|
||||
position: absolute !important;
|
||||
right: 60px !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
grid-row: auto !important;
|
||||
grid-column: auto !important;
|
||||
padding: 14px !important;
|
||||
font-size: 40px !important;
|
||||
}
|
||||
|
||||
.desktop-mode .list-header .list-header-handle {
|
||||
position: absolute !important;
|
||||
right: 10px !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
grid-row: auto !important;
|
||||
grid-column: auto !important;
|
||||
padding: 7px !important;
|
||||
font-size: clamp(16px, 3vw, 20px) !important;
|
||||
}
|
||||
|
||||
.desktop-mode .list-body {
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
padding: 5px 11px !important;
|
||||
}
|
||||
|
||||
.desktop-mode .minicard {
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
font-size: clamp(12px, 2.5vw, 16px) !important;
|
||||
padding: 0.5vh 0.8vw !important;
|
||||
min-height: auto !important;
|
||||
margin-bottom: 9px !important;
|
||||
}
|
||||
|
||||
.desktop-mode .minicard .minicard-title {
|
||||
font-size: clamp(12px, 2.5vw, 16px) !important;
|
||||
}
|
||||
|
||||
.desktop-mode .minicard .minicard-members {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
.desktop-mode .minicard .minicard-lists {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
/* Additional Mobile Mode Styles for Other Elements - Works on all screen sizes */
|
||||
.mobile-mode .swimlane-header .swimlane-title {
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
font-weight: bold !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane-header .swimlane-description {
|
||||
font-size: clamp(14px, 2vw, 24px) !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header {
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
padding: 1rem !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-title {
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
font-weight: bold !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-description {
|
||||
font-size: clamp(14px, 2vw, 24px) !important;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn {
|
||||
font-size: clamp(14px, 2vw, 24px) !important;
|
||||
padding: 1vh 1.5vw !important;
|
||||
display: inline-block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn i {
|
||||
font-size: clamp(14px, 2vw, 24px) !important;
|
||||
display: inline !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Force mobile mode visibility on all screen sizes */
|
||||
.mobile-mode .list-header .fa-angle-right,
|
||||
.mobile-mode .list-header .fa-arrows {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
position: static !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header .fa-angle-right {
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 3 !important;
|
||||
padding: 14px !important;
|
||||
font-size: clamp(24px, 3vw, 48px) !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list-header .fa-arrows {
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 4 !important;
|
||||
padding: 14px !important;
|
||||
font-size: clamp(28px, 3.5vw, 56px) !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* Override any media queries that might hide elements in mobile mode */
|
||||
.mobile-mode * {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list,
|
||||
.mobile-mode .swimlane,
|
||||
.mobile-mode .board-wrapper,
|
||||
.mobile-mode .board-canvas {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
/* Force mobile mode list styling on all screen sizes - override desktop CSS */
|
||||
.mobile-mode .board-canvas {
|
||||
display: block !important;
|
||||
flex-direction: column !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !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: 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: 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: 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: 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;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list:last-child {
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list.ui-sortable-helper {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
height: auto !important;
|
||||
min-height: 60px !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;
|
||||
}
|
||||
|
||||
.mobile-mode .list.placeholder {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
height: auto !important;
|
||||
min-height: 60px !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;
|
||||
}
|
||||
|
||||
/* Override any existing responsive CSS that might interfere with mobile mode */
|
||||
.mobile-mode .board-canvas .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;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-canvas .swimlane .lists .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;
|
||||
}
|
||||
|
||||
/* Force mobile mode to override any media query styles */
|
||||
@media screen and (min-width: 801px) {
|
||||
.mobile-mode .board-canvas {
|
||||
display: block !important;
|
||||
flex-direction: column !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !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 .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 .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;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide desktop-only elements in mobile mode (like mobile media queries do) */
|
||||
.mobile-mode .board-header-btn i.fa + span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btn span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btn .fa + span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btn .fa + .board-header-btn-text {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btn .fa + .board-header-btn-label {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Show only icons in mobile mode */
|
||||
.mobile-mode .board-header-btn {
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
padding: 8px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btn i {
|
||||
display: inline-block !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Hide desktop-specific elements that shouldn't show in mobile mode */
|
||||
.mobile-mode .desktop-only,
|
||||
.mobile-mode .board-header .desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn.desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide desktop-specific board header buttons in mobile mode */
|
||||
.mobile-mode .board-header-btns.left {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btns.center {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Show only the right section buttons in mobile mode, but hide text labels */
|
||||
.mobile-mode .board-header-btns.right {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btns.right .board-header-btn span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btns.right .board-header-btn .fa + span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btns.right .board-header-btn .fa + .board-header-btn-text {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btns.right .board-header-btn .fa + .board-header-btn-label {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide specific desktop-only buttons that shouldn't show in mobile mode */
|
||||
.mobile-mode .board-header-btn.js-star-board span,
|
||||
.mobile-mode .board-header-btn.js-change-visibility span,
|
||||
.mobile-mode .board-header-btn.js-watch-board span,
|
||||
.mobile-mode .board-header-btn.js-sort-cards span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Show only icons for mobile mode buttons */
|
||||
.mobile-mode .board-header-btns.right .board-header-btn {
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
padding: 8px !important;
|
||||
text-align: center !important;
|
||||
margin: 0 2px !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btns.right .board-header-btn i {
|
||||
display: inline-block !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Ensure mobile mode looks like small screen mobile view */
|
||||
.mobile-mode .board-header {
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btns {
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn {
|
||||
height: 32px !important;
|
||||
line-height: 32px !important;
|
||||
font-size: 15px !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn i.fa {
|
||||
line-height: 32px !important;
|
||||
}
|
||||
|
||||
/* Copy mobile media query styles to mobile mode for consistent appearance */
|
||||
.mobile-mode .board-header {
|
||||
height: 40px !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btns {
|
||||
margin-top: 0px !important;
|
||||
height: 40px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn {
|
||||
height: 32px !important;
|
||||
line-height: 32px !important;
|
||||
font-size: 15px !important;
|
||||
margin: 0 2px !important;
|
||||
padding: 4px 8px !important;
|
||||
border-radius: 4px !important;
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2) !important;
|
||||
color: #fff !important;
|
||||
text-decoration: none !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
min-width: 32px !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
border-color: rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn i.fa {
|
||||
line-height: 32px !important;
|
||||
font-size: 15px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn i.fa + span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header .board-header-btn span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide the board title in mobile mode to match mobile view */
|
||||
.mobile-mode .header-board-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Ensure the board header takes full width in mobile mode */
|
||||
.mobile-mode .board-header {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: space-between !important;
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
|
||||
/* Additional Desktop Mode Styles for Other Elements */
|
||||
.desktop-mode .swimlane-header .swimlane-title {
|
||||
font-size: clamp(14px, 3vw, 18px) !important;
|
||||
}
|
||||
|
||||
.desktop-mode .swimlane-header .swimlane-description {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.desktop-mode .board-header {
|
||||
font-size: clamp(14px, 3vw, 18px) !important;
|
||||
padding: 2.5vh 1.5vw 0.5vh !important;
|
||||
}
|
||||
|
||||
.desktop-mode .board-header .board-header-title {
|
||||
font-size: clamp(14px, 3vw, 18px) !important;
|
||||
}
|
||||
|
||||
.desktop-mode .board-header .board-header-description {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.desktop-mode .board-header .board-header-btn {
|
||||
font-size: clamp(12px, 2.5vw, 16px) !important;
|
||||
padding: 0.5vh 0.8vw !important;
|
||||
}
|
||||
|
||||
.desktop-mode .board-header .board-header-btn i {
|
||||
font-size: clamp(12px, 2.5vw, 16px) !important;
|
||||
}
|
||||
@@ -14,91 +14,86 @@ template(name="boardHeaderBar")
|
||||
with currentBoard
|
||||
if currentUser.isBoardAdmin
|
||||
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
|
||||
i.fa.fa-pencil-square-o
|
||||
| ✏️
|
||||
|
||||
a.board-header-btn.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
|
||||
= currentBoard.stars
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
| {{#if currentBoard.isPublic}}🌐{{else}}🔒{{/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"
|
||||
| 👁️
|
||||
if $eq watchLevel "tracking"
|
||||
| 🔔
|
||||
if $eq watchLevel "muted"
|
||||
| 🔕
|
||||
span {{_ watchLevel}}
|
||||
a.board-header-btn.js-star-board(title="{{_ 'star-board'}}")
|
||||
if isStarred
|
||||
| ⭐
|
||||
else
|
||||
| ☆
|
||||
if showStarCounter
|
||||
span.board-star-counter {{currentBoard.stars}}
|
||||
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
||||
| {{sortCardsIcon}}
|
||||
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
|
||||
if isSortActive
|
||||
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
|
||||
i.fa.fa-times-thin
|
||||
| ❌
|
||||
|
||||
else
|
||||
a.board-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
| 🚪
|
||||
span {{_ 'log-in'}}
|
||||
|
||||
.board-header-btns.center
|
||||
|
||||
.board-header-btns.right
|
||||
if currentBoard
|
||||
if isMiniScreen
|
||||
if currentUser
|
||||
with currentBoard
|
||||
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
|
||||
i.fa.fa-pencil-square-o
|
||||
| ✏️
|
||||
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
if showStarCounter
|
||||
span
|
||||
= currentBoard.stars
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
| {{#if currentBoard.isPublic}}🌐{{else}}🔒{{/if}}
|
||||
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
span {{_ currentBoard.permission}}
|
||||
|
||||
a.board-header-btn.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
if $eq watchLevel "watching"
|
||||
i.fa.fa-eye
|
||||
if $eq watchLevel "tracking"
|
||||
i.fa.fa-bell
|
||||
if $eq watchLevel "muted"
|
||||
i.fa.fa-bell-slash
|
||||
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"
|
||||
| 👁️
|
||||
if $eq watchLevel "tracking"
|
||||
| 🔔
|
||||
if $eq watchLevel "muted"
|
||||
| 🔕
|
||||
a.board-header-btn.js-star-board(title="{{_ 'star-board'}}")
|
||||
if isStarred
|
||||
| ⭐
|
||||
else
|
||||
| ☆
|
||||
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
||||
| {{sortCardsIcon}}
|
||||
if isSortActive
|
||||
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
|
||||
i.fa.fa-times-thin
|
||||
| ❌
|
||||
|
||||
else
|
||||
a.board-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
span {{_ 'log-in'}}
|
||||
| 🚪
|
||||
|
||||
if isSandstorm
|
||||
if currentUser
|
||||
a.board-header-btn.js-open-archived-board
|
||||
i.fa.fa-archive
|
||||
span {{_ 'archives'}}
|
||||
| 📦
|
||||
|
||||
//if showSort
|
||||
// a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
|
||||
@@ -107,61 +102,73 @@ template(name="boardHeaderBar")
|
||||
|
||||
a.board-header-btn.js-open-filter-view(
|
||||
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
|
||||
class="{{#if Filter.isActive}}emphasis{{/if}}")
|
||||
i.fa.fa-filter
|
||||
span {{#if Filter.isActive}}{{_ 'filter-on'}}{{else}}{{_ 'filter'}}{{/if}}
|
||||
class="{{#if Filter.isActive}}js-filter-active{{/if}}")
|
||||
| 🎛️
|
||||
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'}}")
|
||||
i.fa.fa-caret-down
|
||||
| ▼
|
||||
if $eq boardView 'board-view-swimlanes'
|
||||
i.fa.fa-th-large
|
||||
| 🏊
|
||||
if $eq boardView 'board-view-lists'
|
||||
i.fa.fa-trello
|
||||
| 📋
|
||||
if $eq boardView 'board-view-cal'
|
||||
i.fa.fa-calendar
|
||||
span {{#if boardView}}{{_ boardView}}{{else}}{{_ 'board-view-swimlanes'}}{{/if}}
|
||||
|
||||
| 📅
|
||||
if $eq boardView 'board-view-gantt'
|
||||
| 📊
|
||||
span
|
||||
if $eq boardView 'board-view-swimlanes'
|
||||
| {{_ 'board-view-swimlanes'}}
|
||||
if $eq boardView 'board-view-lists'
|
||||
| {{_ 'board-view-lists'}}
|
||||
if $eq boardView 'board-view-cal'
|
||||
| {{_ 'board-view-cal'}}
|
||||
if $eq boardView 'board-view-gantt'
|
||||
| {{_ 'board-view-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
|
||||
class="{{#if MultiSelection.isActive}}js-multiselection-active{{/if}}")
|
||||
if MultiSelection.isActive
|
||||
| 🗂️
|
||||
else
|
||||
| 🗂️
|
||||
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
|
||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'multi-selection-off'}}")
|
||||
| ❌
|
||||
|
||||
.separator
|
||||
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
|
||||
i.fa.fa-navicon
|
||||
| ☰
|
||||
|
||||
template(name="boardVisibilityList")
|
||||
ul.pop-over-list
|
||||
li
|
||||
with "private"
|
||||
a.js-select-visibility
|
||||
i.fa.fa-lock.colorful
|
||||
| 🔒
|
||||
| {{_ 'private'}}
|
||||
if visibilityCheck
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
span.sub-name {{_ 'private-desc'}}
|
||||
if notAllowPrivateVisibilityOnly
|
||||
li
|
||||
with "public"
|
||||
a.js-select-visibility
|
||||
i.fa.fa-globe.colorful
|
||||
| 🌐
|
||||
| {{_ 'public'}}
|
||||
if visibilityCheck
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
span.sub-name {{_ 'public-desc'}}
|
||||
|
||||
template(name="boardChangeVisibilityPopup")
|
||||
@@ -172,26 +179,26 @@ template(name="boardChangeWatchPopup")
|
||||
li
|
||||
with "watching"
|
||||
a.js-select-watch
|
||||
i.fa.fa-eye.colorful
|
||||
| 👁️
|
||||
| {{_ 'watching'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
span.sub-name {{_ 'watching-info'}}
|
||||
li
|
||||
with "tracking"
|
||||
a.js-select-watch
|
||||
i.fa.fa-bell.colorful
|
||||
| 🔔
|
||||
| {{_ 'tracking'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
span.sub-name {{_ 'tracking-info'}}
|
||||
li
|
||||
with "muted"
|
||||
a.js-select-watch
|
||||
i.fa.fa-bell-slash.colorful
|
||||
| 🔕
|
||||
| {{_ 'muted'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
span.sub-name {{_ 'muted-info'}}
|
||||
|
||||
template(name="boardChangeViewPopup")
|
||||
@@ -199,24 +206,31 @@ template(name="boardChangeViewPopup")
|
||||
li
|
||||
with "board-view-swimlanes"
|
||||
a.js-open-swimlanes-view
|
||||
i.fa.fa-th-large.colorful
|
||||
| 🏊
|
||||
| {{_ 'board-view-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
|
||||
| 📋
|
||||
| {{_ '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
|
||||
| 📅
|
||||
| {{_ 'board-view-cal'}}
|
||||
if $eq Utils.boardView "board-view-cal"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
li
|
||||
with "board-view-gantt"
|
||||
a.js-open-gantt-view
|
||||
| 📊
|
||||
| {{_ 'board-view-gantt'}}
|
||||
if $eq Utils.boardView "board-view-gantt"
|
||||
| ✅
|
||||
|
||||
template(name="createBoard")
|
||||
form
|
||||
@@ -228,11 +242,70 @@ template(name="createBoard")
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility.get 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
span 🌐
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span.fa.fa-lock.colorful
|
||||
span 🔒
|
||||
= " "
|
||||
| {{{_ '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.get
|
||||
+boardVisibilityList
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility.get 'public'
|
||||
span 🌐
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span 🔒
|
||||
= " "
|
||||
| {{{_ '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.get
|
||||
+boardVisibilityList
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility.get 'public'
|
||||
span 🌐
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span 🔒
|
||||
= " "
|
||||
| {{{_ 'board-private-info'}}}
|
||||
a.js-change-visibility {{_ 'change'}}.
|
||||
@@ -256,10 +329,10 @@ template(name="createBoard")
|
||||
// li
|
||||
// a.js-sort-by(name="{{value.name}}")
|
||||
// if $eq sortby value.name
|
||||
// i(class="fa {{Direction}}")
|
||||
// | {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
|
||||
// | {{_ value.label }}{{_ value.shortLabel}}
|
||||
// if $eq sortby value.name
|
||||
// i(class="fa fa-check")
|
||||
// | ✅
|
||||
|
||||
template(name="boardChangeTitlePopup")
|
||||
form
|
||||
@@ -279,14 +352,22 @@ template(name="boardCreateRulePopup")
|
||||
template(name="cardsSortPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-sort-due {{_ 'due-date'}}
|
||||
a.js-sort-due
|
||||
| 📅
|
||||
| {{_ 'due-date'}}
|
||||
hr
|
||||
li
|
||||
a.js-sort-title {{_ 'title-alphabetically'}}
|
||||
a.js-sort-title
|
||||
| 🔤
|
||||
| {{_ 'title-alphabetically'}}
|
||||
hr
|
||||
li
|
||||
a.js-sort-created-desc {{_ 'created-at-newest-first'}}
|
||||
a.js-sort-created-desc
|
||||
| ⬇️
|
||||
| {{_ 'created-at-newest-first'}}
|
||||
hr
|
||||
li
|
||||
a.js-sort-created-asc {{_ 'created-at-oldest-first'}}
|
||||
a.js-sort-created-asc
|
||||
| ⬆️
|
||||
| {{_ 'created-at-oldest-first'}}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import dragscroll from '@wekanteam/dragscroll';
|
||||
|
||||
/*
|
||||
@@ -33,6 +34,8 @@ BlazeComponent.extendComponent({
|
||||
return currentBoard && currentBoard.getWatchLevel(Meteor.userId());
|
||||
},
|
||||
|
||||
|
||||
|
||||
isStarred() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
@@ -70,7 +73,10 @@ BlazeComponent.extendComponent({
|
||||
{
|
||||
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
|
||||
'click .js-star-board'() {
|
||||
ReactiveCache.getCurrentUser().toggleBoardStar(Session.get('currentBoard'));
|
||||
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'),
|
||||
@@ -80,10 +86,37 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
'click .js-toggle-board-view': Popup.open('boardChangeView'),
|
||||
'click .js-toggle-sidebar'() {
|
||||
Sidebar.toggle();
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Hamburger menu clicked');
|
||||
}
|
||||
// Use the same approach as keyboard shortcuts
|
||||
if (typeof Sidebar !== 'undefined' && Sidebar && typeof Sidebar.toggle === 'function') {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Using Sidebar.toggle()');
|
||||
}
|
||||
Sidebar.toggle();
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('Sidebar not available, trying alternative approach');
|
||||
}
|
||||
// Try to trigger the sidebar through the global Blaze helper
|
||||
if (typeof Blaze !== 'undefined' && Blaze._globalHelpers && Blaze._globalHelpers.Sidebar) {
|
||||
const sidebar = Blaze._globalHelpers.Sidebar();
|
||||
if (sidebar && typeof sidebar.toggle === 'function') {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Using Blaze helper Sidebar.toggle()');
|
||||
}
|
||||
sidebar.toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-open-filter-view'() {
|
||||
Sidebar.setView('filter');
|
||||
if (Sidebar) {
|
||||
Sidebar.setView('filter');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
},
|
||||
'click .js-sort-cards': Popup.open('cardsSort'),
|
||||
/*
|
||||
@@ -100,14 +133,22 @@ BlazeComponent.extendComponent({
|
||||
*/
|
||||
'click .js-filter-reset'(event) {
|
||||
event.stopPropagation();
|
||||
Sidebar.setView();
|
||||
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'() {
|
||||
Sidebar.setView('search');
|
||||
if (Sidebar) {
|
||||
Sidebar.setView('search');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
},
|
||||
'click .js-multiselection-activate'() {
|
||||
const currentCard = Utils.getCurrentCardId();
|
||||
@@ -126,6 +167,7 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
}).register('boardHeaderBar');
|
||||
|
||||
Template.boardHeaderBar.helpers({
|
||||
@@ -135,6 +177,23 @@ Template.boardHeaderBar.helpers({
|
||||
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.boardChangeViewPopup.events({
|
||||
@@ -150,6 +209,10 @@ Template.boardChangeViewPopup.events({
|
||||
Utils.setBoardView('board-view-cal');
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-open-gantt-view'() {
|
||||
Utils.setBoardView('board-view-gantt');
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
const CreateBoard = BlazeComponent.extendComponent({
|
||||
@@ -201,6 +264,7 @@ const CreateBoard = BlazeComponent.extendComponent({
|
||||
title: title,
|
||||
permission: 'private',
|
||||
type: 'template-container',
|
||||
migrationVersion: 1, // Latest version - no migration needed
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -235,6 +299,15 @@ const CreateBoard = BlazeComponent.extendComponent({
|
||||
},
|
||||
);
|
||||
|
||||
// Assign to space if one was selected
|
||||
const spaceId = Session.get('createBoardInWorkspace');
|
||||
if (spaceId) {
|
||||
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
|
||||
if (err) console.error('Error assigning board to space:', err);
|
||||
});
|
||||
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||
}
|
||||
|
||||
Utils.goBoardId(this.boardId.get());
|
||||
|
||||
} else {
|
||||
@@ -244,6 +317,7 @@ const CreateBoard = BlazeComponent.extendComponent({
|
||||
Boards.insert({
|
||||
title,
|
||||
permission: visibility,
|
||||
migrationVersion: 1, // Latest version - no migration needed
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -252,6 +326,15 @@ const CreateBoard = BlazeComponent.extendComponent({
|
||||
boardId: this.boardId.get(),
|
||||
});
|
||||
|
||||
// Assign to space if one was selected
|
||||
const spaceId = Session.get('createBoardInWorkspace');
|
||||
if (spaceId) {
|
||||
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
|
||||
if (err) console.error('Error assigning board to space:', err);
|
||||
});
|
||||
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||
}
|
||||
|
||||
Utils.goBoardId(this.boardId.get());
|
||||
}
|
||||
},
|
||||
@@ -273,6 +356,13 @@ const CreateBoard = BlazeComponent.extendComponent({
|
||||
},
|
||||
}).register('createBoardPopup');
|
||||
|
||||
(class CreateTemplateContainerPopup extends CreateBoard {
|
||||
onRendered() {
|
||||
// Always pre-check the template container checkbox for this popup
|
||||
$('#add-template-container').addClass('is-checked');
|
||||
}
|
||||
}).register('createTemplateContainerPopup');
|
||||
|
||||
(class HeaderBarCreateBoard extends CreateBoard {
|
||||
onSubmit(event) {
|
||||
super.onSubmit(event);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,147 +1,181 @@
|
||||
template(name="boardList")
|
||||
.wrapper
|
||||
ul.AllBoardTeamsOrgs
|
||||
li.AllBoardTeams
|
||||
if userHasTeams
|
||||
select.js-AllBoardTeams#jsAllBoardTeams("multiple")
|
||||
option(value="-1") {{_ 'teams'}} :
|
||||
each teamsDatas
|
||||
option(value="{{teamId}}") {{_ teamDisplayName}}
|
||||
.board-list-header
|
||||
|
||||
li.AllBoardOrgs
|
||||
if userHasOrgs
|
||||
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
|
||||
option(value="-1") {{_ 'organizations'}} :
|
||||
each orgsDatas
|
||||
option(value="{{orgId}}") {{orgDisplayName}}
|
||||
.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 ⭐
|
||||
| {{_ '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 📋
|
||||
| {{_ '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 📂
|
||||
| {{_ 'allboards.remaining'}}
|
||||
span.menu-count {{menuItemCount 'remaining'}}
|
||||
.workspaces-header
|
||||
span
|
||||
span.emoji-icon 🗂️
|
||||
| {{_ 'allboards.workspaces'}}
|
||||
a.js-add-workspace(title="{{_ 'allboards.add-workspace'}}") +
|
||||
// Workspaces tree
|
||||
+workspaceTree(nodes=workspacesTree selectedWorkspaceId=selectedWorkspaceId)
|
||||
|
||||
//li.AllBoardTemplates
|
||||
// if userHasTemplates
|
||||
// select.js-AllBoardTemplates#jsAllBoardTemplates("multiple")
|
||||
// option(value="-1") {{_ 'templates'}} :
|
||||
// each templatesDatas
|
||||
// option(value="{{templateId}}") {{_ templateDisplayName}}
|
||||
// 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.AllBoardBtns
|
||||
div.AllBoardButtonsContainer
|
||||
if userHasOrgsOrTeams
|
||||
i.fa.fa-filter
|
||||
input#filterBtn(type="button" value="{{_ 'filter'}}")
|
||||
input#resetBtn(type="button" value="{{_ 'filter-clear'}}")
|
||||
li.AllBoardOrgs
|
||||
if userHasOrgs
|
||||
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
|
||||
option(value="-1") {{_ 'organizations'}} :
|
||||
each orgsDatas
|
||||
option(value="{{orgId}}") {{orgDisplayName}}
|
||||
|
||||
ul.board-list.clearfix.js-boards
|
||||
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'}}")
|
||||
li.AllBoardBtns
|
||||
div.AllBoardButtonsContainer
|
||||
if userHasOrgsOrTeams
|
||||
span.emoji-icon 🔍
|
||||
input#filterBtn(type="button" value="{{_ 'filter'}}")
|
||||
button#resetBtn.filter-reset-btn
|
||||
span.reset-icon
|
||||
span.emoji-icon ❌
|
||||
span {{_ 'filter-clear'}}
|
||||
|
||||
// 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 📌
|
||||
| {{_ 'multi-selection-active'}}
|
||||
.path-right
|
||||
if canModifyBoards
|
||||
if hasBoardsSelected
|
||||
button.js-archive-selected-boards.board-header-btn
|
||||
span.emoji-icon 📦
|
||||
span {{_ 'archive-board'}}
|
||||
button.js-duplicate-selected-boards.board-header-btn
|
||||
span.emoji-icon 📋
|
||||
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 ☑️
|
||||
if BoardMultiSelection.isActive
|
||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||
span.emoji-icon ✖
|
||||
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
|
||||
li.js-add-board
|
||||
if isSelectedMenu 'templates'
|
||||
a.board-list-item.label(title="{{_ 'add-template-container'}}")
|
||||
span.emoji-icon ➕
|
||||
| {{_ 'add-template-container'}}
|
||||
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 ➕
|
||||
| {{_ '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 ↕️
|
||||
|
||||
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 ⏱️
|
||||
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}}
|
||||
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 ↕️
|
||||
|
||||
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 ⏱️
|
||||
a.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
span.emoji-icon
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
|
||||
template(name="boardListHeaderBar")
|
||||
h1 {{_ title }}
|
||||
@@ -152,3 +186,28 @@ template(name="boardListHeaderBar")
|
||||
// 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 ↕️
|
||||
|
||||
a.js-select-workspace(data-id="{{id}}")
|
||||
span.workspace-icon
|
||||
if icon
|
||||
+viewer
|
||||
= icon
|
||||
else
|
||||
span.emoji-icon 📁
|
||||
span.workspace-name= name
|
||||
a.js-edit-workspace(data-id="{{id}}" title="{{_ 'allboards.edit-workspace'}}")
|
||||
span.emoji-icon ✏️
|
||||
span.workspace-count {{workspaceCount id}}
|
||||
a.js-add-subworkspace(data-id="{{id}}" title="{{_ 'allboards.add-subworkspace'}}") +
|
||||
if children
|
||||
+workspaceTree(nodes=children selectedWorkspaceId=selectedWorkspaceId)
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
|
||||
@@ -14,7 +15,10 @@ Template.boardList.helpers({
|
||||
return Utils.isMiniScreen() && Session.get('currentBoard'); */
|
||||
return true;
|
||||
},
|
||||
})
|
||||
BoardMultiSelection() {
|
||||
return BoardMultiSelection;
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardListHeaderBar.events({
|
||||
'click .js-open-archived-board'() {
|
||||
@@ -22,6 +26,8 @@ Template.boardListHeaderBar.events({
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardList.events({});
|
||||
|
||||
Template.boardListHeaderBar.helpers({
|
||||
title() {
|
||||
//if (FlowRouter.getRouteName() === 'template-container') {
|
||||
@@ -42,17 +48,85 @@ BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
Meteor.subscribe('setting');
|
||||
Meteor.subscribe('tableVisibilityModeSettings');
|
||||
this.selectedMenu = new ReactiveVar('starred');
|
||||
this.selectedWorkspaceIdVar = new ReactiveVar(null);
|
||||
this.workspacesTreeVar = new ReactiveVar([]);
|
||||
let currUser = ReactiveCache.getCurrentUser();
|
||||
let userLanguage;
|
||||
if (currUser && currUser.profile) {
|
||||
userLanguage = currUser.profile.language
|
||||
userLanguage = currUser.profile.language;
|
||||
}
|
||||
if (userLanguage) {
|
||||
TAPi18n.setLanguage(userLanguage);
|
||||
}
|
||||
// Load workspaces tree reactively
|
||||
this.autorun(() => {
|
||||
const u = ReactiveCache.getCurrentUser();
|
||||
const tree = (u && u.profile && u.profile.boardWorkspacesTree) || [];
|
||||
this.workspacesTreeVar.set(tree);
|
||||
});
|
||||
},
|
||||
|
||||
reorderWorkspaces(draggedSpaceId, targetSpaceId) {
|
||||
const tree = this.workspacesTreeVar.get();
|
||||
|
||||
// Helper to remove a space from tree
|
||||
const removeSpace = (nodes, id) => {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].id === id) {
|
||||
const removed = nodes.splice(i, 1)[0];
|
||||
return { tree: nodes, removed };
|
||||
}
|
||||
if (nodes[i].children) {
|
||||
const result = removeSpace(nodes[i].children, id);
|
||||
if (result.removed) {
|
||||
return { tree: nodes, removed: result.removed };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { tree: nodes, removed: null };
|
||||
};
|
||||
|
||||
// Helper to insert a space after target
|
||||
const insertAfter = (nodes, targetId, spaceToInsert) => {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].id === targetId) {
|
||||
nodes.splice(i + 1, 0, spaceToInsert);
|
||||
return true;
|
||||
}
|
||||
if (nodes[i].children) {
|
||||
if (insertAfter(nodes[i].children, targetId, spaceToInsert)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Clone the tree
|
||||
const newTree = EJSON.clone(tree);
|
||||
|
||||
// Remove the dragged space
|
||||
const { tree: treeAfterRemoval, removed } = removeSpace(
|
||||
newTree,
|
||||
draggedSpaceId,
|
||||
);
|
||||
|
||||
if (removed) {
|
||||
// Insert after target
|
||||
insertAfter(treeAfterRemoval, targetSpaceId, removed);
|
||||
|
||||
// Save the new tree
|
||||
Meteor.call('setWorkspacesTree', treeAfterRemoval, (err) => {
|
||||
if (err) console.error(err);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onRendered() {
|
||||
// jQuery sortable is disabled in favor of HTML5 drag-and-drop for space management
|
||||
// The old sortable code has been removed to prevent conflicts
|
||||
/* OLD SORTABLE CODE - DISABLED
|
||||
const itemsSelector = '.js-board:not(.placeholder)';
|
||||
|
||||
const $boards = this.$('.js-boards');
|
||||
@@ -70,27 +144,20 @@ BlazeComponent.extendComponent({
|
||||
EscapeActions.executeUpTo('popup-close');
|
||||
},
|
||||
stop(evt, ui) {
|
||||
// To attribute the new index number, we need to get the DOM element
|
||||
// of the previous and the following card -- if any.
|
||||
const prevBoardDom = ui.item.prev('.js-board').get(0);
|
||||
const nextBoardBom = ui.item.next('.js-board').get(0);
|
||||
const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardBom, 1);
|
||||
const nextBoardDom = ui.item.next('.js-board').get(0);
|
||||
const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardDom, 1);
|
||||
|
||||
const boardDomElement = ui.item.get(0);
|
||||
const board = Blaze.getData(boardDomElement);
|
||||
// Normally the jquery-ui sortable library moves the dragged DOM element
|
||||
// to its new position, which disrupts Blaze reactive updates mechanism
|
||||
// (especially when we move the last card of a list, or when multiple
|
||||
// users move some cards at the same time). To prevent these UX glitches
|
||||
// we ask sortable to gracefully cancel the move, and to put back the
|
||||
// DOM in its initial state. The card move is then handled reactively by
|
||||
// Blaze with the below query.
|
||||
$boards.sortable('cancel');
|
||||
board.move(sortIndex.base);
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (currentUser && typeof currentUser.setBoardSortIndex === 'function') {
|
||||
currentUser.setBoardSortIndex(board._id, sortIndex.base);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Disable drag-dropping if the current user is not a board member or is comment only
|
||||
this.autorun(() => {
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$boards.sortable({
|
||||
@@ -98,56 +165,103 @@ BlazeComponent.extendComponent({
|
||||
});
|
||||
}
|
||||
});
|
||||
*/
|
||||
},
|
||||
userHasTeams() {
|
||||
if (ReactiveCache.getCurrentUser()?.teams?.length > 0)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
if (ReactiveCache.getCurrentUser()?.teams?.length > 0) return true;
|
||||
else return false;
|
||||
},
|
||||
teamsDatas() {
|
||||
const teams = ReactiveCache.getCurrentUser()?.teams
|
||||
const teams = ReactiveCache.getCurrentUser()?.teams;
|
||||
if (teams)
|
||||
return teams.sort((a, b) => a.teamDisplayName.localeCompare(b.teamDisplayName));
|
||||
else
|
||||
return [];
|
||||
return teams.sort((a, b) =>
|
||||
a.teamDisplayName.localeCompare(b.teamDisplayName),
|
||||
);
|
||||
else return [];
|
||||
},
|
||||
userHasOrgs() {
|
||||
if (ReactiveCache.getCurrentUser()?.orgs?.length > 0)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
if (ReactiveCache.getCurrentUser()?.orgs?.length > 0) return true;
|
||||
else return false;
|
||||
},
|
||||
orgsDatas() {
|
||||
const orgs = ReactiveCache.getCurrentUser()?.orgs;
|
||||
if (orgs)
|
||||
return orgs.sort((a, b) => a.orgDisplayName.localeCompare(b.orgDisplayName));
|
||||
else
|
||||
return [];
|
||||
return orgs.sort((a, b) =>
|
||||
a.orgDisplayName.localeCompare(b.orgDisplayName),
|
||||
);
|
||||
else return [];
|
||||
},
|
||||
userHasOrgsOrTeams() {
|
||||
const ret = this.userHasOrgs() || this.userHasTeams();
|
||||
return ret;
|
||||
},
|
||||
currentMenuPath() {
|
||||
const selectedMenuVar = this.selectedMenu;
|
||||
if (!selectedMenuVar) {
|
||||
return { icon: '🗂️', text: TAPi18n.__('allboards.workspaces') };
|
||||
}
|
||||
const sel = selectedMenuVar.get();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
|
||||
// Helper to find space by id in tree
|
||||
const findSpaceById = (nodes, targetId, path = []) => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === targetId) {
|
||||
return [...path, node];
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
const result = findSpaceById(node.children, targetId, [
|
||||
...path,
|
||||
node,
|
||||
]);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
if (sel === 'starred') {
|
||||
return { icon: '⭐', text: TAPi18n.__('allboards.starred') };
|
||||
} else if (sel === 'templates') {
|
||||
return { icon: '📋', text: TAPi18n.__('allboards.templates') };
|
||||
} else if (sel === 'remaining') {
|
||||
return { icon: '📂', text: TAPi18n.__('allboards.remaining') };
|
||||
} else {
|
||||
// sel is a workspaceId, build path
|
||||
const tree = this.workspacesTreeVar.get();
|
||||
const spacePath = findSpaceById(tree, sel);
|
||||
if (spacePath && spacePath.length > 0) {
|
||||
const pathText = spacePath.map((s) => s.name).join(' / ');
|
||||
return {
|
||||
icon: '🗂️',
|
||||
text: `${TAPi18n.__('allboards.workspaces') || 'Workspaces'} / ${pathText}`,
|
||||
};
|
||||
}
|
||||
return { icon: '🗂️', text: TAPi18n.__('allboards.workspaces') };
|
||||
}
|
||||
},
|
||||
boards() {
|
||||
let query = {
|
||||
// { type: 'board' },
|
||||
// { type: { $in: ['board','template-container'] } },
|
||||
$and: [
|
||||
{ archived: false },
|
||||
{ type: { $in: ['board', 'template-container'] } },
|
||||
{ $or: [] },
|
||||
{ title: { $not: { $regex: /^\^.*\^$/ } } }
|
||||
]
|
||||
{ title: { $not: { $regex: /^\^.*\^$/ } } },
|
||||
],
|
||||
};
|
||||
const membershipOrs = [];
|
||||
|
||||
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
|
||||
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne(
|
||||
'tableVisibilityMode-allowPrivateOnly',
|
||||
);
|
||||
|
||||
if (FlowRouter.getRouteName() === 'home') {
|
||||
query.$and[2].$or.push({ 'members.userId': Meteor.userId() });
|
||||
membershipOrs.push({ 'members.userId': Meteor.userId() });
|
||||
|
||||
if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue) {
|
||||
query.$and.push({ 'permission': 'private' });
|
||||
if (
|
||||
allowPrivateVisibilityOnly !== undefined &&
|
||||
allowPrivateVisibilityOnly.booleanValue
|
||||
) {
|
||||
query.$and.push({ permission: 'private' });
|
||||
}
|
||||
const currUser = ReactiveCache.getCurrentUser();
|
||||
|
||||
@@ -159,7 +273,7 @@ BlazeComponent.extendComponent({
|
||||
// }
|
||||
|
||||
//query.$and[2].$or.push({'orgs': {$elemMatch : {orgId: orgsIds[0]}}});
|
||||
query.$and[2].$or.push({ 'orgs.orgId': { $in: orgsIds } });
|
||||
membershipOrs.push({ 'orgs.orgId': { $in: orgsIds } });
|
||||
}
|
||||
|
||||
let teamIdsUserBelongs = currUser?.teamIdsUserBelongs() || '';
|
||||
@@ -169,10 +283,15 @@ BlazeComponent.extendComponent({
|
||||
// query.$or[2].$or.push({'teams.teamId': teamsIds[i]});
|
||||
// }
|
||||
//query.$and[2].$or.push({'teams': { $elemMatch : {teamId: teamsIds[0]}}});
|
||||
query.$and[2].$or.push({ 'teams.teamId': { $in: teamsIds } });
|
||||
membershipOrs.push({ 'teams.teamId': { $in: teamsIds } });
|
||||
}
|
||||
}
|
||||
else if (allowPrivateVisibilityOnly !== undefined && !allowPrivateVisibilityOnly.booleanValue) {
|
||||
if (membershipOrs.length) {
|
||||
query.$and.splice(2, 0, { $or: membershipOrs });
|
||||
}
|
||||
} else if (
|
||||
allowPrivateVisibilityOnly !== undefined &&
|
||||
!allowPrivateVisibilityOnly.booleanValue
|
||||
) {
|
||||
query = {
|
||||
archived: false,
|
||||
//type: { $in: ['board','template-container'] },
|
||||
@@ -181,10 +300,38 @@ BlazeComponent.extendComponent({
|
||||
};
|
||||
}
|
||||
|
||||
const ret = ReactiveCache.getBoards(query, {
|
||||
sort: { sort: 1 /* boards default sorting */ },
|
||||
});
|
||||
return ret;
|
||||
const boards = ReactiveCache.getBoards(query, {});
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let list = boards;
|
||||
// Apply left menu filtering
|
||||
const sel = this.selectedMenu.get();
|
||||
const assignments =
|
||||
(currentUser &&
|
||||
currentUser.profile &&
|
||||
currentUser.profile.boardWorkspaceAssignments) ||
|
||||
{};
|
||||
if (sel === 'starred') {
|
||||
list = list.filter((b) => currentUser && currentUser.hasStarred(b._id));
|
||||
} else if (sel === 'templates') {
|
||||
list = list.filter((b) => b.type === 'template-container');
|
||||
} else if (sel === 'remaining') {
|
||||
// Show boards not in any workspace AND not templates
|
||||
// Keep starred boards visible in Remaining too
|
||||
list = list.filter(
|
||||
(b) => !assignments[b._id] && b.type !== 'template-container',
|
||||
);
|
||||
} else {
|
||||
// assume sel is a workspaceId
|
||||
// Keep starred boards visible in their workspace too
|
||||
list = list.filter((b) => assignments[b._id] === sel);
|
||||
}
|
||||
|
||||
if (currentUser && typeof currentUser.sortBoardsForUser === 'function') {
|
||||
return currentUser.sortBoardsForUser(list);
|
||||
}
|
||||
return list
|
||||
.slice()
|
||||
.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
|
||||
},
|
||||
boardLists(boardId) {
|
||||
/* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
|
||||
@@ -232,41 +379,118 @@ BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-add-board': Popup.open('createBoard'),
|
||||
'click .js-star-board'(evt) {
|
||||
const boardId = this.currentData()._id;
|
||||
ReactiveCache.getCurrentUser().toggleBoardStar(boardId);
|
||||
'click .js-select-menu'(evt) {
|
||||
const type = evt.currentTarget.getAttribute('data-type');
|
||||
this.selectedWorkspaceIdVar.set(null);
|
||||
this.selectedMenu.set(type);
|
||||
},
|
||||
'click .js-select-workspace'(evt) {
|
||||
const id = evt.currentTarget.getAttribute('data-id');
|
||||
this.selectedWorkspaceIdVar.set(id);
|
||||
this.selectedMenu.set(id);
|
||||
},
|
||||
'click .js-add-workspace'(evt) {
|
||||
evt.preventDefault();
|
||||
const name = prompt(
|
||||
TAPi18n.__('allboards.add-workspace-prompt') || 'New Space name',
|
||||
);
|
||||
if (name && name.trim()) {
|
||||
Meteor.call(
|
||||
'createWorkspace',
|
||||
{ parentId: null, name: name.trim() },
|
||||
(err, res) => {
|
||||
if (err) console.error(err);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
'click .js-add-board'(evt) {
|
||||
// Store the currently selected workspace/menu for board creation
|
||||
const selectedWorkspaceId = this.selectedWorkspaceIdVar.get();
|
||||
const selectedMenu = this.selectedMenu.get();
|
||||
|
||||
if (selectedWorkspaceId) {
|
||||
Session.set('createBoardInWorkspace', selectedWorkspaceId);
|
||||
} else {
|
||||
Session.set('createBoardInWorkspace', null);
|
||||
}
|
||||
|
||||
// Open different popup based on context
|
||||
if (selectedMenu === 'templates') {
|
||||
Popup.open('createTemplateContainer')(evt);
|
||||
} else {
|
||||
Popup.open('createBoard')(evt);
|
||||
}
|
||||
},
|
||||
'click .js-star-board'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const boardId = this.currentData()._id;
|
||||
if (boardId) {
|
||||
Meteor.call('toggleBoardStar', boardId);
|
||||
}
|
||||
},
|
||||
// HTML5 DnD from boards to spaces
|
||||
'dragstart .js-board'(evt) {
|
||||
const boardId = this.currentData()._id;
|
||||
|
||||
// Support multi-drag
|
||||
if (
|
||||
BoardMultiSelection.isActive() &&
|
||||
BoardMultiSelection.isSelected(boardId)
|
||||
) {
|
||||
const selectedIds = BoardMultiSelection.getSelectedBoardIds();
|
||||
try {
|
||||
evt.originalEvent.dataTransfer.setData(
|
||||
'text/plain',
|
||||
JSON.stringify(selectedIds),
|
||||
);
|
||||
evt.originalEvent.dataTransfer.setData(
|
||||
'application/x-board-multi',
|
||||
'true',
|
||||
);
|
||||
} catch (e) {}
|
||||
} else {
|
||||
try {
|
||||
evt.originalEvent.dataTransfer.setData('text/plain', boardId);
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
'click .js-clone-board'(evt) {
|
||||
let title = getSlug(ReactiveCache.getBoard(this.currentData()._id).title) || 'cloned-board';
|
||||
Meteor.call(
|
||||
'copyBoard',
|
||||
this.currentData()._id,
|
||||
{
|
||||
sort: ReactiveCache.getBoards({ archived: false }).length,
|
||||
type: 'board',
|
||||
title: ReactiveCache.getBoard(this.currentData()._id).title,
|
||||
},
|
||||
(err, res) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
} else {
|
||||
Session.set('fromBoard', null);
|
||||
subManager.subscribe('board', res, false);
|
||||
FlowRouter.go('board', {
|
||||
id: res,
|
||||
slug: title,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
evt.preventDefault();
|
||||
if (confirm(TAPi18n.__('duplicate-board-confirm'))) {
|
||||
let title =
|
||||
getSlug(ReactiveCache.getBoard(this.currentData()._id).title) ||
|
||||
'cloned-board';
|
||||
Meteor.call(
|
||||
'copyBoard',
|
||||
this.currentData()._id,
|
||||
{
|
||||
sort: ReactiveCache.getBoards({ archived: false }).length,
|
||||
type: 'board',
|
||||
title: ReactiveCache.getBoard(this.currentData()._id).title,
|
||||
},
|
||||
(err, res) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
} else {
|
||||
Session.set('fromBoard', null);
|
||||
subManager.subscribe('board', res, false);
|
||||
FlowRouter.go('board', {
|
||||
id: res,
|
||||
slug: title,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
evt.preventDefault();
|
||||
}
|
||||
},
|
||||
'click .js-archive-board'(evt) {
|
||||
const boardId = this.currentData()._id;
|
||||
Meteor.call('archiveBoard', boardId);
|
||||
evt.preventDefault();
|
||||
if (confirm(TAPi18n.__('archive-board-confirm'))) {
|
||||
const boardId = this.currentData()._id;
|
||||
Meteor.call('archiveBoard', boardId);
|
||||
evt.preventDefault();
|
||||
}
|
||||
},
|
||||
'click .js-accept-invite'() {
|
||||
const boardId = this.currentData()._id;
|
||||
@@ -281,47 +505,115 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
});
|
||||
},
|
||||
'click .js-multiselection-activate'(evt) {
|
||||
evt.preventDefault();
|
||||
if (BoardMultiSelection.isActive()) {
|
||||
BoardMultiSelection.disable();
|
||||
} else {
|
||||
BoardMultiSelection.activate();
|
||||
}
|
||||
},
|
||||
'click .js-multiselection-reset'(evt) {
|
||||
evt.preventDefault();
|
||||
BoardMultiSelection.disable();
|
||||
},
|
||||
'click .js-toggle-board-multi-selection'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const boardId = this.currentData()._id;
|
||||
BoardMultiSelection.toogle(boardId);
|
||||
},
|
||||
'click .js-archive-selected-boards'(evt) {
|
||||
evt.preventDefault();
|
||||
const selectedBoards = BoardMultiSelection.getSelectedBoardIds();
|
||||
if (
|
||||
selectedBoards.length > 0 &&
|
||||
confirm(TAPi18n.__('archive-board-confirm'))
|
||||
) {
|
||||
selectedBoards.forEach((boardId) => {
|
||||
Meteor.call('archiveBoard', boardId);
|
||||
});
|
||||
BoardMultiSelection.reset();
|
||||
}
|
||||
},
|
||||
'click .js-duplicate-selected-boards'(evt) {
|
||||
evt.preventDefault();
|
||||
const selectedBoards = BoardMultiSelection.getSelectedBoardIds();
|
||||
if (
|
||||
selectedBoards.length > 0 &&
|
||||
confirm(TAPi18n.__('duplicate-board-confirm'))
|
||||
) {
|
||||
selectedBoards.forEach((boardId) => {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (board) {
|
||||
Meteor.call(
|
||||
'copyBoard',
|
||||
boardId,
|
||||
{
|
||||
sort: ReactiveCache.getBoards({ archived: false }).length,
|
||||
type: 'board',
|
||||
title: board.title,
|
||||
},
|
||||
(err, res) => {
|
||||
if (err) console.error(err);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
BoardMultiSelection.reset();
|
||||
}
|
||||
},
|
||||
'click #resetBtn'(event) {
|
||||
let allBoards = document.getElementsByClassName("js-board");
|
||||
let allBoards = document.getElementsByClassName('js-board');
|
||||
let currBoard;
|
||||
for (let i = 0; i < allBoards.length; i++) {
|
||||
currBoard = allBoards[i];
|
||||
currBoard.style.display = "block";
|
||||
currBoard.style.display = 'block';
|
||||
}
|
||||
},
|
||||
'click #filterBtn'(event) {
|
||||
event.preventDefault();
|
||||
let selectedTeams = document.querySelectorAll('#jsAllBoardTeams option:checked');
|
||||
let selectedTeamsValues = Array.from(selectedTeams).map(function (elt) { return elt.value });
|
||||
let index = selectedTeamsValues.indexOf("-1");
|
||||
let selectedTeams = document.querySelectorAll(
|
||||
'#jsAllBoardTeams option:checked',
|
||||
);
|
||||
let selectedTeamsValues = Array.from(selectedTeams).map(
|
||||
function (elt) {
|
||||
return elt.value;
|
||||
},
|
||||
);
|
||||
let index = selectedTeamsValues.indexOf('-1');
|
||||
if (index > -1) {
|
||||
selectedTeamsValues.splice(index, 1);
|
||||
}
|
||||
|
||||
let selectedOrgs = document.querySelectorAll('#jsAllBoardOrgs option:checked');
|
||||
let selectedOrgsValues = Array.from(selectedOrgs).map(function (elt) { return elt.value });
|
||||
index = selectedOrgsValues.indexOf("-1");
|
||||
let selectedOrgs = document.querySelectorAll(
|
||||
'#jsAllBoardOrgs option:checked',
|
||||
);
|
||||
let selectedOrgsValues = Array.from(selectedOrgs).map(function (elt) {
|
||||
return elt.value;
|
||||
});
|
||||
index = selectedOrgsValues.indexOf('-1');
|
||||
if (index > -1) {
|
||||
selectedOrgsValues.splice(index, 1);
|
||||
}
|
||||
|
||||
if (selectedTeamsValues.length > 0 || selectedOrgsValues.length > 0) {
|
||||
const query = {
|
||||
$and: [
|
||||
{ archived: false },
|
||||
{ type: 'board' },
|
||||
{ $or: [] }
|
||||
]
|
||||
$and: [{ archived: false }, { type: 'board' }],
|
||||
};
|
||||
const ors = [];
|
||||
if (selectedTeamsValues.length > 0) {
|
||||
query.$and[2].$or.push({ 'teams.teamId': { $in: selectedTeamsValues } });
|
||||
ors.push({ 'teams.teamId': { $in: selectedTeamsValues } });
|
||||
}
|
||||
if (selectedOrgsValues.length > 0) {
|
||||
query.$and[2].$or.push({ 'orgs.orgId': { $in: selectedOrgsValues } });
|
||||
ors.push({ 'orgs.orgId': { $in: selectedOrgsValues } });
|
||||
}
|
||||
if (ors.length) {
|
||||
query.$and.push({ $or: ors });
|
||||
}
|
||||
|
||||
let filteredBoards = ReactiveCache.getBoards(query, {});
|
||||
let allBoards = document.getElementsByClassName("js-board");
|
||||
let allBoards = document.getElementsByClassName('js-board');
|
||||
let currBoard;
|
||||
if (filteredBoards.length > 0) {
|
||||
let currBoardId;
|
||||
@@ -333,21 +625,312 @@ BlazeComponent.extendComponent({
|
||||
return board._id == currBoardId;
|
||||
});
|
||||
|
||||
if (found !== undefined)
|
||||
currBoard.style.display = "block";
|
||||
else
|
||||
currBoard.style.display = "none";
|
||||
if (found !== undefined) currBoard.style.display = 'block';
|
||||
else currBoard.style.display = 'none';
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
for (let i = 0; i < allBoards.length; i++) {
|
||||
currBoard = allBoards[i];
|
||||
currBoard.style.display = "none";
|
||||
currBoard.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-edit-workspace'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const workspaceId = evt.currentTarget.getAttribute('data-id');
|
||||
|
||||
// Find the space in the tree
|
||||
const findSpace = (nodes, id) => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node;
|
||||
if (node.children) {
|
||||
const found = findSpace(node.children, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const tree = this.workspacesTreeVar.get();
|
||||
const space = findSpace(tree, workspaceId);
|
||||
|
||||
if (space) {
|
||||
const newName = prompt(
|
||||
TAPi18n.__('allboards.edit-workspace-name') || 'Space name:',
|
||||
space.name,
|
||||
);
|
||||
const newIcon = prompt(
|
||||
TAPi18n.__('allboards.edit-workspace-icon') ||
|
||||
'Space icon (markdown):',
|
||||
space.icon || '📁',
|
||||
);
|
||||
|
||||
if (newName !== null && newName.trim()) {
|
||||
// Update space in tree
|
||||
const updateSpaceInTree = (nodes, id, updates) => {
|
||||
return nodes.map((node) => {
|
||||
if (node.id === id) {
|
||||
return { ...node, ...updates };
|
||||
}
|
||||
if (node.children) {
|
||||
return {
|
||||
...node,
|
||||
children: updateSpaceInTree(node.children, id, updates),
|
||||
};
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
const updatedTree = updateSpaceInTree(tree, workspaceId, {
|
||||
name: newName.trim(),
|
||||
icon: newIcon || '📁',
|
||||
});
|
||||
|
||||
Meteor.call('setWorkspacesTree', updatedTree, (err) => {
|
||||
if (err) console.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-add-subworkspace'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const parentId = evt.currentTarget.getAttribute('data-id');
|
||||
const name = prompt(
|
||||
TAPi18n.__('allboards.add-subworkspace-prompt') || 'Subspace name:',
|
||||
);
|
||||
|
||||
if (name && name.trim()) {
|
||||
Meteor.call(
|
||||
'createWorkspace',
|
||||
{ parentId, name: name.trim() },
|
||||
(err) => {
|
||||
if (err) console.error(err);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
'dragstart .workspace-node'(evt) {
|
||||
const workspaceId =
|
||||
evt.currentTarget.getAttribute('data-workspace-id');
|
||||
evt.originalEvent.dataTransfer.effectAllowed = 'move';
|
||||
evt.originalEvent.dataTransfer.setData(
|
||||
'application/x-workspace-id',
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
// Create a better drag image
|
||||
const dragImage = evt.currentTarget.cloneNode(true);
|
||||
dragImage.style.position = 'absolute';
|
||||
dragImage.style.top = '-9999px';
|
||||
dragImage.style.opacity = '0.8';
|
||||
document.body.appendChild(dragImage);
|
||||
evt.originalEvent.dataTransfer.setDragImage(dragImage, 0, 0);
|
||||
setTimeout(() => document.body.removeChild(dragImage), 0);
|
||||
|
||||
evt.currentTarget.classList.add('dragging');
|
||||
},
|
||||
'dragend .workspace-node'(evt) {
|
||||
evt.currentTarget.classList.remove('dragging');
|
||||
document.querySelectorAll('.workspace-node').forEach((el) => {
|
||||
el.classList.remove('drag-over');
|
||||
});
|
||||
},
|
||||
'dragover .workspace-node'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const draggingEl = document.querySelector('.workspace-node.dragging');
|
||||
const targetEl = evt.currentTarget;
|
||||
|
||||
// Allow dropping boards on any space
|
||||
// Or allow dropping spaces on other spaces (but not on itself or descendants)
|
||||
if (
|
||||
!draggingEl ||
|
||||
(targetEl !== draggingEl && !draggingEl.contains(targetEl))
|
||||
) {
|
||||
evt.originalEvent.dataTransfer.dropEffect = 'move';
|
||||
targetEl.classList.add('drag-over');
|
||||
}
|
||||
},
|
||||
'dragleave .workspace-node'(evt) {
|
||||
evt.currentTarget.classList.remove('drag-over');
|
||||
},
|
||||
'drop .workspace-node'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const targetEl = evt.currentTarget;
|
||||
targetEl.classList.remove('drag-over');
|
||||
|
||||
// Check what's being dropped - board or workspace
|
||||
const draggedWorkspaceId = evt.originalEvent.dataTransfer.getData(
|
||||
'application/x-workspace-id',
|
||||
);
|
||||
const isMultiBoard = evt.originalEvent.dataTransfer.getData(
|
||||
'application/x-board-multi',
|
||||
);
|
||||
const boardData =
|
||||
evt.originalEvent.dataTransfer.getData('text/plain');
|
||||
|
||||
if (draggedWorkspaceId && !boardData) {
|
||||
// This is a workspace reorder operation
|
||||
const targetWorkspaceId =
|
||||
targetEl.getAttribute('data-workspace-id');
|
||||
|
||||
if (draggedWorkspaceId !== targetWorkspaceId) {
|
||||
this.reorderWorkspaces(draggedWorkspaceId, targetWorkspaceId);
|
||||
}
|
||||
} else if (boardData) {
|
||||
// This is a board assignment operation
|
||||
// Get the workspace ID directly from the dropped workspace-node's data-workspace-id attribute
|
||||
const workspaceId = targetEl.getAttribute('data-workspace-id');
|
||||
|
||||
if (workspaceId) {
|
||||
if (isMultiBoard) {
|
||||
// Multi-board drag
|
||||
try {
|
||||
const boardIds = JSON.parse(boardData);
|
||||
boardIds.forEach((boardId) => {
|
||||
Meteor.call('assignBoardToWorkspace', boardId, workspaceId);
|
||||
});
|
||||
} catch (e) {
|
||||
// Error parsing multi-board data
|
||||
}
|
||||
} else {
|
||||
// Single board drag
|
||||
Meteor.call('assignBoardToWorkspace', boardData, workspaceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'dragover .js-select-menu'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const menuType = evt.currentTarget.getAttribute('data-type');
|
||||
// Only allow drop on "remaining" menu to unassign boards from spaces
|
||||
if (menuType === 'remaining') {
|
||||
evt.originalEvent.dataTransfer.dropEffect = 'move';
|
||||
evt.currentTarget.classList.add('drag-over');
|
||||
}
|
||||
},
|
||||
'dragleave .js-select-menu'(evt) {
|
||||
evt.currentTarget.classList.remove('drag-over');
|
||||
},
|
||||
'drop .js-select-menu'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const menuType = evt.currentTarget.getAttribute('data-type');
|
||||
evt.currentTarget.classList.remove('drag-over');
|
||||
|
||||
// Only handle drops on "remaining" menu
|
||||
if (menuType !== 'remaining') return;
|
||||
|
||||
const isMultiBoard = evt.originalEvent.dataTransfer.getData(
|
||||
'application/x-board-multi',
|
||||
);
|
||||
const boardData =
|
||||
evt.originalEvent.dataTransfer.getData('text/plain');
|
||||
|
||||
if (boardData) {
|
||||
if (isMultiBoard) {
|
||||
// Multi-board drag - unassign all from workspaces
|
||||
try {
|
||||
const boardIds = JSON.parse(boardData);
|
||||
boardIds.forEach((boardId) => {
|
||||
Meteor.call('unassignBoardFromWorkspace', boardId);
|
||||
});
|
||||
} catch (e) {
|
||||
// Error parsing multi-board data
|
||||
}
|
||||
} else {
|
||||
// Single board drag - unassign from workspace
|
||||
Meteor.call('unassignBoardFromWorkspace', boardData);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
// Helpers for templates
|
||||
workspacesTree() {
|
||||
return this.workspacesTreeVar.get();
|
||||
},
|
||||
selectedWorkspaceId() {
|
||||
return this.selectedWorkspaceIdVar.get();
|
||||
},
|
||||
isSelectedMenu(type) {
|
||||
return this.selectedMenu.get() === type;
|
||||
},
|
||||
isSpaceSelected(id) {
|
||||
return this.selectedWorkspaceIdVar.get() === id;
|
||||
},
|
||||
menuItemCount(type) {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const assignments =
|
||||
(currentUser &&
|
||||
currentUser.profile &&
|
||||
currentUser.profile.boardWorkspaceAssignments) ||
|
||||
{};
|
||||
|
||||
// Get all boards for counting
|
||||
let query = {
|
||||
$and: [
|
||||
{ archived: false },
|
||||
{ type: { $in: ['board', 'template-container'] } },
|
||||
{ $or: [{ 'members.userId': Meteor.userId() }] },
|
||||
{ title: { $not: { $regex: /^\^.*\^$/ } } },
|
||||
],
|
||||
};
|
||||
const allBoards = ReactiveCache.getBoards(query, {});
|
||||
|
||||
if (type === 'starred') {
|
||||
return allBoards.filter(
|
||||
(b) => currentUser && currentUser.hasStarred(b._id),
|
||||
).length;
|
||||
} else if (type === 'templates') {
|
||||
return allBoards.filter((b) => b.type === 'template-container').length;
|
||||
} else if (type === 'remaining') {
|
||||
// Count boards not in any workspace AND not templates
|
||||
// Include starred boards (they appear in both Starred and Remaining)
|
||||
return allBoards.filter(
|
||||
(b) => !assignments[b._id] && b.type !== 'template-container',
|
||||
).length;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
workspaceCount(workspaceId) {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const assignments =
|
||||
(currentUser &&
|
||||
currentUser.profile &&
|
||||
currentUser.profile.boardWorkspaceAssignments) ||
|
||||
{};
|
||||
|
||||
// Get all boards for counting
|
||||
let query = {
|
||||
$and: [
|
||||
{ archived: false },
|
||||
{ type: { $in: ['board', 'template-container'] } },
|
||||
{ $or: [{ 'members.userId': Meteor.userId() }] },
|
||||
{ title: { $not: { $regex: /^\^.*\^$/ } } },
|
||||
],
|
||||
};
|
||||
const allBoards = ReactiveCache.getBoards(query, {});
|
||||
|
||||
// Count boards directly assigned to this space (not including children)
|
||||
return allBoards.filter((b) => assignments[b._id] === workspaceId).length;
|
||||
},
|
||||
canModifyBoards() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
return currentUser && !currentUser.isCommentOnly();
|
||||
},
|
||||
hasBoardsSelected() {
|
||||
return BoardMultiSelection.count() > 0;
|
||||
},
|
||||
}).register('boardList');
|
||||
@@ -3,6 +3,6 @@ template(name="miniboard")
|
||||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
.fa.fa-arrows
|
||||
span.drag-handle(title="{{_ 'dragBoard'}}") ↕️
|
||||
+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,82 @@
|
||||
<template name="originalPositionsView">
|
||||
<div class="original-positions-view">
|
||||
<div class="original-positions-header">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="{{toggleOriginalPositions}}">
|
||||
<i class="fa fa-history"></i>
|
||||
{{#if isShowingOriginalPositions}}Hide{{else}}Show{{/if}} Original Positions
|
||||
</button>
|
||||
|
||||
{{#if isShowingOriginalPositions}}
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="{{refreshHistory}}">
|
||||
<i class="fa fa-refresh"></i> Refresh
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if isShowingOriginalPositions}}
|
||||
<div class="original-positions-content">
|
||||
{{#if isLoading}}
|
||||
<div class="original-positions-loading">
|
||||
<i class="fa fa-spinner fa-spin"></i> Loading original positions...
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="original-positions-filters">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'all'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'all'}}">
|
||||
All
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'swimlane'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'swimlane'}}">
|
||||
<i class="fa fa-bars"></i> Swimlanes
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'list'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'list'}}">
|
||||
<i class="fa fa-columns"></i> Lists
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'card'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'card'}}">
|
||||
<i class="fa fa-sticky-note"></i> Cards
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="original-positions-list">
|
||||
{{#each getFilteredHistory}}
|
||||
<div class="original-position-item">
|
||||
<div class="original-position-item-header">
|
||||
<i class="fa {{getEntityTypeIcon entityType}}"></i>
|
||||
<span class="entity-type">{{getEntityTypeLabel entityType}}</span>
|
||||
<span class="entity-name">{{getEntityDisplayName this}}</span>
|
||||
<span class="entity-id">({{entityId}})</span>
|
||||
</div>
|
||||
<div class="original-position-item-details">
|
||||
<div class="original-position-description">
|
||||
{{getEntityOriginalPositionDescription this}}
|
||||
</div>
|
||||
{{#if originalTitle}}
|
||||
<div class="original-title">
|
||||
<strong>Original title:</strong> {{originalTitle}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="original-position-date">
|
||||
<small class="text-muted">Created: {{formatDate createdAt}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="no-original-positions">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
No original position data available for this board.
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,148 @@
|
||||
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import './originalPositionsView.html';
|
||||
|
||||
/**
|
||||
* Component to display original positions for all entities on a board
|
||||
*/
|
||||
class OriginalPositionsViewComponent extends BlazeComponent {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
this.showOriginalPositions = new ReactiveVar(false);
|
||||
this.boardHistory = new ReactiveVar([]);
|
||||
this.isLoading = new ReactiveVar(false);
|
||||
this.filterType = new ReactiveVar('all'); // 'all', 'swimlane', 'list', 'card'
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
super.onRendered();
|
||||
this.loadBoardHistory();
|
||||
}
|
||||
|
||||
loadBoardHistory() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
if (!boardId) return;
|
||||
|
||||
this.isLoading.set(true);
|
||||
|
||||
Meteor.call('positionHistory.getBoardHistory', boardId, (error, result) => {
|
||||
this.isLoading.set(false);
|
||||
if (error) {
|
||||
console.error('Error loading board history:', error);
|
||||
this.boardHistory.set([]);
|
||||
} else {
|
||||
this.boardHistory.set(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleOriginalPositions() {
|
||||
this.showOriginalPositions.set(!this.showOriginalPositions.get());
|
||||
}
|
||||
|
||||
isShowingOriginalPositions() {
|
||||
return this.showOriginalPositions.get();
|
||||
}
|
||||
|
||||
isLoading() {
|
||||
return this.isLoading.get();
|
||||
}
|
||||
|
||||
getBoardHistory() {
|
||||
return this.boardHistory.get();
|
||||
}
|
||||
|
||||
getFilteredHistory() {
|
||||
const history = this.getBoardHistory();
|
||||
const filterType = this.filterType.get();
|
||||
|
||||
if (filterType === 'all') {
|
||||
return history;
|
||||
}
|
||||
|
||||
return history.filter(item => item.entityType === filterType);
|
||||
}
|
||||
|
||||
getSwimlanesHistory() {
|
||||
return this.getBoardHistory().filter(item => item.entityType === 'swimlane');
|
||||
}
|
||||
|
||||
getListsHistory() {
|
||||
return this.getBoardHistory().filter(item => item.entityType === 'list');
|
||||
}
|
||||
|
||||
getCardsHistory() {
|
||||
return this.getBoardHistory().filter(item => item.entityType === 'card');
|
||||
}
|
||||
|
||||
setFilterType(type) {
|
||||
this.filterType.set(type);
|
||||
}
|
||||
|
||||
getFilterType() {
|
||||
return this.filterType.get();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
refreshHistory() {
|
||||
this.loadBoardHistory();
|
||||
}
|
||||
}
|
||||
|
||||
OriginalPositionsViewComponent.register('originalPositionsView');
|
||||
|
||||
export default OriginalPositionsViewComponent;
|
||||
@@ -106,6 +106,142 @@
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 4em;
|
||||
}
|
||||
|
||||
/* Upload progress indicators for drag-and-drop uploads */
|
||||
.minicard-upload-progress,
|
||||
.card-details-upload-progress {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.upload-progress-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.upload-progress-header i {
|
||||
margin-right: 8px;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.upload-progress-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
background: white;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.upload-progress-item.upload-error {
|
||||
border-color: #dc3545;
|
||||
background: #f8d7da;
|
||||
}
|
||||
|
||||
.upload-progress-filename {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
color: #495057;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.upload-progress-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: #e9ecef;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.upload-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #007bff, #0056b3);
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.upload-progress-item.upload-error .upload-progress-fill {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.upload-progress-error,
|
||||
.upload-progress-success {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-progress-error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.upload-progress-success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.upload-progress-error i,
|
||||
.upload-progress-success i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* Minicard specific styles */
|
||||
.minicard-upload-progress {
|
||||
margin: 4px 0;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.minicard-upload-progress .upload-progress-item {
|
||||
padding: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.minicard-upload-progress .upload-progress-filename {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Card details specific styles */
|
||||
.card-details-upload-progress {
|
||||
margin: 12px 0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card-details-upload-progress .upload-progress-header {
|
||||
font-size: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-details-upload-progress .upload-progress-item {
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-details-upload-progress .upload-progress-filename {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Drag over state for minicards */
|
||||
.minicard.is-dragging-over {
|
||||
border: 2px dashed #007bff !important;
|
||||
background: rgba(0, 123, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Drag over state for card details */
|
||||
.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;
|
||||
|
||||
@@ -34,10 +34,10 @@ template(name="attachmentViewer")
|
||||
#viewer-overlay.hidden
|
||||
#viewer-top-bar
|
||||
span#attachment-name
|
||||
a#viewer-close.fa.fa-times-thin
|
||||
a#viewer-close ❌
|
||||
|
||||
#viewer-container
|
||||
i.fa.fa-chevron-left.attachment-arrow#prev-attachment
|
||||
| ◀️
|
||||
#viewer-content
|
||||
img#image-viewer.hidden
|
||||
video#video-viewer.hidden(controls="true")
|
||||
@@ -45,7 +45,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
|
||||
| ▶️
|
||||
|
||||
template(name="attachmentGallery")
|
||||
|
||||
@@ -53,11 +53,11 @@ template(name="attachmentGallery")
|
||||
|
||||
if canModifyCard
|
||||
a.attachment-item.add-attachment.js-add-attachment
|
||||
i.fa.fa-plus.icon
|
||||
| ➕
|
||||
|
||||
each attachments
|
||||
|
||||
.attachment-item
|
||||
.attachment-item(class="{{#if isAttachmentMigrating _id}}migrating{{/if}}")
|
||||
.attachment-thumbnail-container.open-preview(data-attachment-id="{{_id}}" data-card-id="{{ meta.cardId }}")
|
||||
if link
|
||||
if(isImage)
|
||||
@@ -86,25 +86,32 @@ template(name="attachmentGallery")
|
||||
= name
|
||||
span.file-size ({{fileSize size}})
|
||||
.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'}}")
|
||||
| ⬇️
|
||||
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'}}")
|
||||
| ✏️
|
||||
a.js-confirm-delete(title="{{_ 'delete'}}")
|
||||
| 🗑️
|
||||
a.js-open-attachment-menu(data-attachment-link="{{link}}", title="{{_ 'attachmentActionsPopup-title'}}")
|
||||
| ☰
|
||||
|
||||
// Migration spinner overlay
|
||||
if isAttachmentMigrating _id
|
||||
.attachment-migration-overlay
|
||||
.migration-spinner
|
||||
| ⚙️
|
||||
.migration-text {{_ 'migrating-attachment'}}
|
||||
|
||||
template(name="attachmentActionsPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
if isImage
|
||||
a(class="{{#if isCover}}js-remove-cover{{else}}js-add-cover{{/if}}")
|
||||
i.fa.fa-book
|
||||
i.fa.fa-picture-o
|
||||
| 📖
|
||||
| 🖼️
|
||||
if isCover
|
||||
| {{_ 'remove-cover'}}
|
||||
else
|
||||
@@ -112,7 +119,7 @@ template(name="attachmentActionsPopup")
|
||||
if currentUser.isBoardAdmin
|
||||
if isImage
|
||||
a(class="{{#if isBackgroundImage}}js-remove-background-image{{else}}js-add-background-image{{/if}}")
|
||||
i.fa.fa-picture-o
|
||||
| 🖼️
|
||||
if isBackgroundImage
|
||||
| {{_ 'remove-background-image'}}
|
||||
else
|
||||
@@ -120,19 +127,19 @@ template(name="attachmentActionsPopup")
|
||||
|
||||
if $neq versions.original.storage "fs"
|
||||
a.js-move-storage-fs
|
||||
i.fa.fa-arrow-right
|
||||
| ▶️
|
||||
| {{_ 'attachment-move-storage-fs'}}
|
||||
|
||||
if $neq versions.original.storage "gridfs"
|
||||
if versions.original.storage
|
||||
a.js-move-storage-gridfs
|
||||
i.fa.fa-arrow-right
|
||||
| ▶️
|
||||
| {{_ 'attachment-move-storage-gridfs'}}
|
||||
|
||||
if $neq versions.original.storage "s3"
|
||||
if versions.original.storage
|
||||
a.js-move-storage-s3
|
||||
i.fa.fa-arrow-right
|
||||
| ▶️
|
||||
| {{_ 'attachment-move-storage-s3'}}
|
||||
|
||||
template(name="attachmentRenamePopup")
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
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';
|
||||
|
||||
const filesize = require('filesize');
|
||||
const prettyMilliseconds = require('pretty-ms');
|
||||
@@ -268,7 +271,7 @@ Template.attachmentGallery.helpers({
|
||||
return ret;
|
||||
},
|
||||
sanitize(value) {
|
||||
return DOMPurify.sanitize(value);
|
||||
return sanitizeHTML(value);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -296,44 +299,13 @@ Template.cardAttachmentsPopup.events({
|
||||
const files = event.currentTarget.files;
|
||||
if (files) {
|
||||
let uploads = [];
|
||||
for (const file of files) {
|
||||
const fileId = new ObjectID().toString();
|
||||
let fileName = DOMPurify.sanitize(file.name);
|
||||
const uploaders = handleFileUpload(card, files);
|
||||
|
||||
// If sanitized filename is not same as original filename,
|
||||
// it could be XSS that is already fixed with sanitize,
|
||||
// or just normal mistake, so it is not a problem.
|
||||
// That is why here is no warning.
|
||||
if (fileName !== file.name) {
|
||||
// If filename is empty, only in that case add some filename
|
||||
if (fileName.length === 0) {
|
||||
fileName = 'Empty-filename-after-sanitize.txt';
|
||||
}
|
||||
}
|
||||
|
||||
const config = {
|
||||
file: file,
|
||||
fileId: fileId,
|
||||
fileName: fileName,
|
||||
meta: Utils.getCommonAttachmentMetaFrom(card),
|
||||
chunkSize: 'dynamic',
|
||||
};
|
||||
config.meta.fileId = fileId;
|
||||
const uploader = Attachments.insert(
|
||||
config,
|
||||
false,
|
||||
);
|
||||
uploaders.forEach(uploader => {
|
||||
uploader.on('start', function() {
|
||||
uploads.push(this);
|
||||
templateInstance.uploads.set(uploads);
|
||||
});
|
||||
uploader.on('uploaded', (error, fileRef) => {
|
||||
if (!error) {
|
||||
if (fileRef.isImage) {
|
||||
card.setCover(fileRef._id);
|
||||
}
|
||||
}
|
||||
});
|
||||
uploader.on('end', (error, fileRef) => {
|
||||
uploads = uploads.filter(_upload => _upload.config.fileId != fileRef._id);
|
||||
templateInstance.uploads.set(uploads);
|
||||
@@ -341,8 +313,7 @@ Template.cardAttachmentsPopup.events({
|
||||
Popup.back();
|
||||
}
|
||||
});
|
||||
uploader.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
'click .js-computer-upload'(event, templateInstance) {
|
||||
@@ -356,6 +327,105 @@ const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
|
||||
const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
|
||||
let pastedResults = null;
|
||||
|
||||
// Shared upload logic for drag-and-drop functionality
|
||||
export function handleFileUpload(card, files) {
|
||||
if (!files || files.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if board allows attachments
|
||||
const board = card.board();
|
||||
if (!board || !board.allowsAttachments) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('Attachments not allowed on this board');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if user can modify the card
|
||||
if (!Utils.canModifyCard()) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('User does not have permission to modify this card');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
const uploads = [];
|
||||
|
||||
for (const file of files) {
|
||||
// Basic file validation
|
||||
if (!file || !file.name) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('Invalid file object');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileId = new ObjectID().toString();
|
||||
let fileName = sanitizeText(file.name);
|
||||
|
||||
// If sanitized filename is not same as original filename,
|
||||
// it could be XSS that is already fixed with sanitize,
|
||||
// or just normal mistake, so it is not a problem.
|
||||
// That is why here is no warning.
|
||||
if (fileName !== file.name) {
|
||||
// If filename is empty, only in that case add some filename
|
||||
if (fileName.length === 0) {
|
||||
fileName = 'Empty-filename-after-sanitize.txt';
|
||||
}
|
||||
}
|
||||
|
||||
const config = {
|
||||
file: file,
|
||||
fileId: fileId,
|
||||
fileName: fileName,
|
||||
meta: Utils.getCommonAttachmentMetaFrom(card),
|
||||
chunkSize: 'dynamic',
|
||||
};
|
||||
config.meta.fileId = fileId;
|
||||
|
||||
try {
|
||||
const uploader = Attachments.insert(
|
||||
config,
|
||||
false,
|
||||
);
|
||||
|
||||
// Add to progress manager for tracking
|
||||
const uploadId = uploadProgressManager.addUpload(card._id, uploader, file);
|
||||
|
||||
uploader.on('uploaded', (error, fileRef) => {
|
||||
if (!error) {
|
||||
if (fileRef.isImage) {
|
||||
card.setCover(fileRef._id);
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Set cover image for card ${card._id}: ${fileRef.name}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.error('Upload error:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
uploader.on('error', (error) => {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.error('Upload error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
uploads.push(uploader);
|
||||
uploader.start();
|
||||
} catch (error) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.error('Failed to create uploader:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uploads;
|
||||
}
|
||||
|
||||
Template.previewClipboardImagePopup.onRendered(() => {
|
||||
// we can paste image from clipboard
|
||||
const handle = results => {
|
||||
@@ -498,7 +568,7 @@ BlazeComponent.extendComponent({
|
||||
const name = this.$('.js-edit-attachment-name')[0]
|
||||
.value
|
||||
.trim() + this.data().extensionWithDot;
|
||||
if (name === DOMPurify.sanitize(name)) {
|
||||
if (name === sanitizeText(name)) {
|
||||
Meteor.call('renameAttachment', this.data()._id, name);
|
||||
}
|
||||
Popup.back();
|
||||
@@ -507,3 +577,20 @@ BlazeComponent.extendComponent({
|
||||
]
|
||||
}
|
||||
}).register('attachmentRenamePopup');
|
||||
|
||||
// Template helpers for attachment migration status
|
||||
Template.registerHelper('attachmentMigrationStatus', function(attachmentId) {
|
||||
return attachmentMigrationManager.getAttachmentMigrationStatus(attachmentId);
|
||||
});
|
||||
|
||||
Template.registerHelper('isAttachmentMigrating', function(attachmentId) {
|
||||
return attachmentMigrationManager.isAttachmentBeingMigrated(attachmentId);
|
||||
});
|
||||
|
||||
Template.registerHelper('attachmentMigrationProgress', function() {
|
||||
return attachmentMigrationManager.attachmentMigrationProgress.get();
|
||||
});
|
||||
|
||||
Template.registerHelper('attachmentMigrationStatusText', function() {
|
||||
return attachmentMigrationManager.attachmentMigrationStatus.get();
|
||||
});
|
||||
@@ -6,10 +6,10 @@ template(name="cardCustomFieldsPopup")
|
||||
span.full-name
|
||||
= name
|
||||
if hasCustomField
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
hr
|
||||
a.quiet-button.full.js-settings
|
||||
i.fa.fa-cog
|
||||
| ⚙️
|
||||
span {{_ 'settings'}}
|
||||
|
||||
template(name="cardCustomField")
|
||||
@@ -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,9 @@ 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 {{#if data.value }}✅{{else}}⬜{{/if}}
|
||||
else
|
||||
.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
|
||||
span.check-box-unicode {{#if data.value }}✅{{else}}⬜{{/if}}
|
||||
|
||||
template(name="cardCustomField-currency")
|
||||
if canModifyCard
|
||||
@@ -66,7 +65,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
|
||||
@@ -113,7 +112,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 +133,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,6 +1,27 @@
|
||||
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 {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatDateByUserPreference,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
import Cards from '/models/cards';
|
||||
import { CustomFieldStringTemplate } from '/client/lib/customFields'
|
||||
|
||||
@@ -91,6 +112,7 @@ CardCustomField.register('cardCustomField');
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-checklist-item .check-box-unicode': this.toggleItem,
|
||||
'click .js-checklist-item .check-box-container': this.toggleItem,
|
||||
},
|
||||
];
|
||||
@@ -134,31 +156,33 @@ CardCustomField.register('cardCustomField');
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.date = ReactiveVar();
|
||||
self.now = ReactiveVar(moment());
|
||||
self.now = ReactiveVar(now());
|
||||
window.setInterval(() => {
|
||||
self.now.set(moment());
|
||||
self.now.set(now());
|
||||
}, 60000);
|
||||
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().value));
|
||||
self.date.set(new Date(self.data().value));
|
||||
});
|
||||
}
|
||||
|
||||
showWeek() {
|
||||
return this.date.get().week().toString();
|
||||
return getISOWeek(this.date.get()).toString();
|
||||
}
|
||||
|
||||
showWeekOfYear() {
|
||||
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (!user) {
|
||||
// For non-logged-in users, week of year is not shown
|
||||
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(this.date.get(), dateFormat, true);
|
||||
}
|
||||
|
||||
showISODate() {
|
||||
@@ -167,8 +191,8 @@ CardCustomField.register('cardCustomField');
|
||||
|
||||
classes() {
|
||||
if (
|
||||
this.date.get().isBefore(this.now.get(), 'minute') &&
|
||||
this.now.get().isBefore(this.data().value)
|
||||
isBefore(this.date.get(), this.now.get(), 'minute') &&
|
||||
isBefore(this.now.get(), this.data().value, 'minute')
|
||||
) {
|
||||
return 'current';
|
||||
}
|
||||
@@ -176,7 +200,7 @@ CardCustomField.register('cardCustomField');
|
||||
}
|
||||
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
|
||||
return `${TAPi18n.__('card-start-on')} ${this.date.get().toLocaleString()}`;
|
||||
}
|
||||
|
||||
events() {
|
||||
@@ -195,7 +219,7 @@ CardCustomField.register('cardCustomField');
|
||||
const self = this;
|
||||
self.card = Utils.getCurrentCard();
|
||||
self.customFieldId = this.data()._id;
|
||||
this.data().value && this.date.set(moment(this.data().value));
|
||||
this.data().value && this.date.set(new Date(this.data().value));
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -21,3 +21,132 @@ template(name="dateCustomField")
|
||||
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")
|
||||
form.edit-card-received-date
|
||||
.datepicker
|
||||
// Date input field (existing)
|
||||
// Insert calendar selector right after date input
|
||||
.calendar-selector
|
||||
label(for="calendar-received") 🗓️
|
||||
input#calendar-received.js-calendar-date(type="date")
|
||||
// Time input field (if present)
|
||||
.clear-date
|
||||
a.js-clear-date {{_ 'clear'}}
|
||||
.datepicker-actions
|
||||
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||
|
||||
template(name="editCardStartDatePopup")
|
||||
form.edit-card-start-date
|
||||
.datepicker
|
||||
// Date input field (existing)
|
||||
.calendar-selector
|
||||
label(for="calendar-start") 🗓️
|
||||
input#calendar-start.js-calendar-date(type="date")
|
||||
// Time input field (if present)
|
||||
.clear-date
|
||||
a.js-clear-date {{_ 'clear'}}
|
||||
.datepicker-actions
|
||||
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||
|
||||
template(name="editCardDueDatePopup")
|
||||
form.edit-card-due-date
|
||||
.datepicker
|
||||
// Date input field (existing)
|
||||
.calendar-selector
|
||||
label(for="calendar-due") 🗓️
|
||||
input#calendar-due.js-calendar-date(type="date")
|
||||
// Time input field (if present)
|
||||
.clear-date
|
||||
a.js-clear-date {{_ 'clear'}}
|
||||
.datepicker-actions
|
||||
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||
|
||||
template(name="editCardEndDatePopup")
|
||||
form.edit-card-end-date
|
||||
.datepicker
|
||||
// Date input field (existing)
|
||||
.calendar-selector
|
||||
label(for="calendar-end") 🗓️
|
||||
input#calendar-end.js-calendar-date(type="date")
|
||||
// Time input field (if present)
|
||||
.clear-date
|
||||
a.js-clear-date {{_ 'clear'}}
|
||||
.datepicker-actions
|
||||
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||
@@ -1,17 +1,38 @@
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { DatePicker } 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';
|
||||
|
||||
// editCardReceivedDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
super.onCreated(formatDateTime(now()));
|
||||
this.data().getReceived() &&
|
||||
this.date.set(moment(this.data().getReceived()));
|
||||
this.date.set(new Date(this.data().getReceived()));
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setReceived(date);
|
||||
this.card.setReceived(formatDateTime(date));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
@@ -22,22 +43,28 @@ import { DatePicker } from '/client/lib/datepicker';
|
||||
// editCardStartDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
this.data().getStart() && this.date.set(moment(this.data().getStart()));
|
||||
super.onCreated(formatDateTime(now()));
|
||||
this.data().getStart() && this.date.set(new Date(this.data().getStart()));
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
super.onRendered();
|
||||
if (moment.isDate(this.card.getReceived())) {
|
||||
this.$('.js-datepicker').datepicker(
|
||||
'setStartDate',
|
||||
this.card.getReceived(),
|
||||
);
|
||||
}
|
||||
// DatePicker base class handles initialization with native HTML inputs
|
||||
const self = this;
|
||||
this.$('.js-calendar-date').on('change', function(evt) {
|
||||
const currentUser = ReactiveCache.getCurrentUser && ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const value = evt.target.value;
|
||||
if (value) {
|
||||
// Format date according to user preference
|
||||
const formatted = formatDateByUserPreference(new Date(value), dateFormat, true);
|
||||
self._storeDate(new Date(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setStart(date);
|
||||
this.card.setStart(formatDateTime(date));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
@@ -49,18 +76,16 @@ import { DatePicker } from '/client/lib/datepicker';
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated('1970-01-01 17:00:00');
|
||||
this.data().getDue() && this.date.set(moment(this.data().getDue()));
|
||||
this.data().getDue() && this.date.set(new Date(this.data().getDue()));
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
super.onRendered();
|
||||
if (moment.isDate(this.card.getStart())) {
|
||||
this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
|
||||
}
|
||||
// DatePicker base class handles initialization with native HTML inputs
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setDue(date);
|
||||
this.card.setDue(formatDateTime(date));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
@@ -71,19 +96,17 @@ import { DatePicker } from '/client/lib/datepicker';
|
||||
// editCardEndDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
this.data().getEnd() && this.date.set(moment(this.data().getEnd()));
|
||||
super.onCreated(formatDateTime(now()));
|
||||
this.data().getEnd() && this.date.set(new Date(this.data().getEnd()));
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
super.onRendered();
|
||||
if (moment.isDate(this.card.getStart())) {
|
||||
this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
|
||||
}
|
||||
// DatePicker base class handles initialization with native HTML inputs
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setEnd(date);
|
||||
this.card.setEnd(formatDateTime(date));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
@@ -100,27 +123,29 @@ const CardDate = BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
const self = this;
|
||||
self.date = ReactiveVar();
|
||||
self.now = ReactiveVar(moment());
|
||||
self.now = ReactiveVar(now());
|
||||
window.setInterval(() => {
|
||||
self.now.set(moment());
|
||||
self.now.set(now());
|
||||
}, 60000);
|
||||
},
|
||||
|
||||
showWeek() {
|
||||
return this.date.get().week().toString();
|
||||
return getISOWeek(this.date.get()).toString();
|
||||
},
|
||||
|
||||
showWeekOfYear() {
|
||||
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (!user) {
|
||||
// For non-logged-in users, week of year is not shown
|
||||
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(this.date.get(), dateFormat, true);
|
||||
},
|
||||
|
||||
showISODate() {
|
||||
@@ -133,7 +158,7 @@ class CardReceivedDate extends CardDate {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getReceived()));
|
||||
self.date.set(new Date(self.data().getReceived()));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,21 +168,26 @@ class CardReceivedDate extends CardDate {
|
||||
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 now = this.now.get();
|
||||
|
||||
// Received date logic: if received date is after start, due, or end dates, it's overdue
|
||||
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 currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
|
||||
}
|
||||
|
||||
events() {
|
||||
@@ -173,26 +203,35 @@ class CardStartDate extends CardDate {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getStart()));
|
||||
self.date.set(new Date(self.data().getStart()));
|
||||
});
|
||||
}
|
||||
|
||||
classes() {
|
||||
let classes = 'start-date' + ' ';
|
||||
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';
|
||||
|
||||
// Start date logic: if start date is after due or end dates, it's overdue
|
||||
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
|
||||
classes += 'overdue';
|
||||
} else if (isAfter(theDate, now)) {
|
||||
// Start date is in the future - not due yet
|
||||
classes += 'not-due';
|
||||
} else {
|
||||
// Start date is today or in the past - current/active
|
||||
classes += 'current';
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
|
||||
}
|
||||
|
||||
events() {
|
||||
@@ -208,27 +247,48 @@ class CardDueDate extends CardDate {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getDue()));
|
||||
self.date.set(new Date(self.data().getDue()));
|
||||
});
|
||||
}
|
||||
|
||||
classes() {
|
||||
let classes = 'due-date' + ' ';
|
||||
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';
|
||||
|
||||
// If there's an end date and it's before the due date, task is completed early
|
||||
if (endAt && isBefore(endAt, theDate)) {
|
||||
classes += 'completed-early';
|
||||
}
|
||||
// If there's an end date, don't show due date status since task is completed
|
||||
else if (endAt) {
|
||||
classes += 'completed';
|
||||
}
|
||||
// Due date logic based on current time
|
||||
else {
|
||||
const daysDiff = diff(theDate, now, 'days');
|
||||
|
||||
if (daysDiff < 0) {
|
||||
// Due date is in the past - overdue
|
||||
classes += 'overdue';
|
||||
} else if (daysDiff <= 1) {
|
||||
// Due today or tomorrow - due soon
|
||||
classes += 'due-soon';
|
||||
} else {
|
||||
// Due date is more than 1 day away - not due yet
|
||||
classes += 'not-due';
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-due-on')} ${this.date.get().format('LLLL')}`;
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||
return `${TAPi18n.__('card-due-on')} ${formattedDate}`;
|
||||
}
|
||||
|
||||
events() {
|
||||
@@ -244,22 +304,33 @@ class CardEndDate extends CardDate {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getEnd()));
|
||||
self.date.set(new Date(self.data().getEnd()));
|
||||
});
|
||||
}
|
||||
|
||||
classes() {
|
||||
let classes = 'end-date' + ' ';
|
||||
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';
|
||||
|
||||
if (!dueAt) {
|
||||
// No due date set - just show as completed
|
||||
classes += 'completed';
|
||||
} else if (isBefore(theDate, dueAt)) {
|
||||
// End date is before due date - completed early
|
||||
classes += 'completed-early';
|
||||
} else if (isAfter(theDate, dueAt)) {
|
||||
// End date is after due date - completed late
|
||||
classes += 'completed-late';
|
||||
} else {
|
||||
// End date equals due date - completed on time
|
||||
classes += 'completed-on-time';
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
|
||||
return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`;
|
||||
}
|
||||
|
||||
events() {
|
||||
@@ -279,16 +350,21 @@ class CardCustomFieldDate extends CardDate {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().value));
|
||||
self.date.set(new Date(self.data().value));
|
||||
});
|
||||
}
|
||||
|
||||
showWeek() {
|
||||
return this.date.get().week().toString();
|
||||
return getISOWeek(this.date.get()).toString();
|
||||
}
|
||||
|
||||
showWeekOfYear() {
|
||||
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (!user) {
|
||||
// For non-logged-in users, week of year is not shown
|
||||
return false;
|
||||
}
|
||||
return user.isShowWeekOfYear();
|
||||
}
|
||||
|
||||
showDate() {
|
||||
@@ -301,7 +377,10 @@ class CardCustomFieldDate extends CardDate {
|
||||
}
|
||||
|
||||
showTitle() {
|
||||
return `${this.date.get().format('LLLL')}`;
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||
return `${formattedDate}`;
|
||||
}
|
||||
|
||||
classes() {
|
||||
@@ -315,32 +394,62 @@ class CardCustomFieldDate extends CardDate {
|
||||
CardCustomFieldDate.register('cardCustomFieldDate');
|
||||
|
||||
(class extends CardReceivedDate {
|
||||
template() {
|
||||
return 'minicardReceivedDate';
|
||||
}
|
||||
|
||||
showDate() {
|
||||
return this.date.get().format('L');
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||
}
|
||||
}.register('minicardReceivedDate'));
|
||||
|
||||
(class extends CardStartDate {
|
||||
template() {
|
||||
return 'minicardStartDate';
|
||||
}
|
||||
|
||||
showDate() {
|
||||
return this.date.get().format('L');
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||
}
|
||||
}.register('minicardStartDate'));
|
||||
|
||||
(class extends CardDueDate {
|
||||
template() {
|
||||
return 'minicardDueDate';
|
||||
}
|
||||
|
||||
showDate() {
|
||||
return this.date.get().format('L');
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||
}
|
||||
}.register('minicardDueDate'));
|
||||
|
||||
(class extends CardEndDate {
|
||||
template() {
|
||||
return 'minicardEndDate';
|
||||
}
|
||||
|
||||
showDate() {
|
||||
return this.date.get().format('L');
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||
}
|
||||
}.register('minicardEndDate'));
|
||||
|
||||
(class extends CardCustomFieldDate {
|
||||
template() {
|
||||
return 'minicardCustomFieldDate';
|
||||
}
|
||||
|
||||
showDate() {
|
||||
return this.date.get().format('L');
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||
}
|
||||
}.register('minicardCustomFieldDate'));
|
||||
|
||||
@@ -349,7 +458,7 @@ class VoteEndDate extends CardDate {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getVoteEnd()));
|
||||
self.date.set(new Date(self.data().getVoteEnd()));
|
||||
});
|
||||
}
|
||||
classes() {
|
||||
@@ -357,10 +466,12 @@ class VoteEndDate extends CardDate {
|
||||
return classes;
|
||||
}
|
||||
showDate() {
|
||||
return this.date.get().format('L LT');
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||
}
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
|
||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().toLocaleString()}`;
|
||||
}
|
||||
|
||||
events() {
|
||||
@@ -376,7 +487,7 @@ class PokerEndDate extends CardDate {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getPokerEnd()));
|
||||
self.date.set(new Date(self.data().getPokerEnd()));
|
||||
});
|
||||
}
|
||||
classes() {
|
||||
@@ -384,10 +495,12 @@ class PokerEndDate extends CardDate {
|
||||
return classes;
|
||||
}
|
||||
showDate() {
|
||||
return this.date.get().format('l LT');
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
return formatDateByUserPreference(this.date.get(), dateFormat, true);
|
||||
}
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
|
||||
return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`;
|
||||
}
|
||||
|
||||
events() {
|
||||
|
||||
@@ -1,10 +1,38 @@
|
||||
/* Date Format Selector */
|
||||
.card-details-item-date-format {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-details-item-date-format .card-details-item-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-details-item-date-format .js-date-format-selector {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.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: 3px;
|
||||
display: block;
|
||||
position: relative;
|
||||
float: left;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
height: clamp(24px, 3.5vw, 36px);
|
||||
width: clamp(24px, 3.5vw, 36px);
|
||||
margin: .3vh;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
@@ -70,32 +98,91 @@
|
||||
}
|
||||
.copied-tooltip {
|
||||
display: none;
|
||||
padding: 0px 10px;
|
||||
padding: 0 1.3vw;
|
||||
background-color: rgba(0,0,0,0.875);
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
border-radius: 0.7vw;
|
||||
}
|
||||
.card-details {
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: 600px;
|
||||
flex-basis: min(600px, 80vw);
|
||||
will-change: flex-basis;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
background: #f7f7f7;
|
||||
border-radius: bottom 3px;
|
||||
border-radius: bottom 0.4vw;
|
||||
z-index: 30;
|
||||
animation: flexGrowIn 0.1s;
|
||||
box-shadow: 0 0 7px 0 #b3b3b3;
|
||||
box-shadow: 0 0 0.9vh 0 #b3b3b3;
|
||||
transition: flex-basis 0.1s;
|
||||
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: 100;
|
||||
}
|
||||
|
||||
/* Default position for first card or when dragged */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):not([style*="left"]):not([style*="top"]) {
|
||||
top: 50px;
|
||||
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: 50px;
|
||||
left: 20px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(2) {
|
||||
top: 80px;
|
||||
left: 50px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(3) {
|
||||
top: 110px;
|
||||
left: 80px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(4) {
|
||||
top: 140px;
|
||||
left: 110px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(5) {
|
||||
top: 170px;
|
||||
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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.card-details .card-details-canvas {
|
||||
width: auto;
|
||||
padding: 0 20px;
|
||||
padding: 0 2.5vw;
|
||||
}
|
||||
.card-details .card-details-header {
|
||||
margin: 0 -20px 5px;
|
||||
@@ -111,6 +198,30 @@
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Drag handle */
|
||||
.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;
|
||||
}
|
||||
|
||||
.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 +239,16 @@
|
||||
font-size: 24px;
|
||||
padding: 5px 10px 5px 10px;
|
||||
margin-right: -8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.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;
|
||||
@@ -153,6 +269,36 @@
|
||||
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.4;
|
||||
}
|
||||
.card-details .card-details-header .card-details-watch {
|
||||
font-size: 17px;
|
||||
padding-left: 7px;
|
||||
@@ -256,6 +402,19 @@
|
||||
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;
|
||||
@@ -307,19 +466,53 @@ 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: 0px 0px 0px 0px !important;
|
||||
margin: 0px !important;
|
||||
transition: none;
|
||||
overflow-y: revert;
|
||||
overflow-x: revert;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
/* iOS Safari specific fixes */
|
||||
-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 +538,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;
|
||||
@@ -596,3 +845,11 @@ input[type="submit"].attachment-add-link-submit {
|
||||
overflow: hidden;
|
||||
background-color: #cecece;
|
||||
}
|
||||
|
||||
/* Drag and drop file upload visual feedback */
|
||||
.js-card-details.is-dragging-over {
|
||||
border: 2px dashed #0079bf;
|
||||
background-color: #e3f2fd !important;
|
||||
transform: scale(1.01);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
@@ -5,49 +5,75 @@ template(name="cardDetails")
|
||||
|
||||
+attachmentViewer
|
||||
|
||||
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}'): .card-details-canvas
|
||||
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}' class='{{#if cardCollapsed}}card-details-collapsed{{/if}}'): .card-details-canvas
|
||||
.card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
|
||||
+inlinedForm(classNames="js-card-details-title")
|
||||
+editCardTitleForm
|
||||
else
|
||||
unless isMiniScreen
|
||||
unless isPopup
|
||||
a.fa.fa-times-thin.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
if canModifyCard
|
||||
if cardMaximized
|
||||
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
span.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
|
||||
if cardCollapsed
|
||||
| ▶
|
||||
else
|
||||
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
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(
|
||||
| 🔽
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
| ❌
|
||||
if cardMaximized
|
||||
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
| 🔽
|
||||
else
|
||||
a.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
| 🔼
|
||||
a.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
| ☰
|
||||
a.card-copy-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
class="fa-link"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.emoji-icon 🔗
|
||||
if canModifyCard
|
||||
span.card-drag-handle.js-card-drag-handle(title="Drag card")
|
||||
| ↕️
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
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'}}")
|
||||
| ❌
|
||||
a.card-zoom-out.js-card-zoom-out(title="{{_ 'zoom-out'}}")
|
||||
| 🔍➖
|
||||
a.card-zoom-in.js-card-zoom-in(title="{{_ 'zoom-in'}}")
|
||||
| 🔍➕
|
||||
a.card-mobile-desktop-toggle.js-card-mobile-desktop-toggle(title="{{_ 'mobile-desktop-toggle'}}")
|
||||
if mobileMode
|
||||
| 🖥️
|
||||
else
|
||||
| 📱
|
||||
if cardMaximized
|
||||
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
| 🔽
|
||||
else
|
||||
a.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
| 🔼
|
||||
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
| ☰
|
||||
a.card-copy-mobile-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.emoji-icon 🔗
|
||||
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
|
||||
| 👁️
|
||||
.card-details-path
|
||||
each parentList
|
||||
| >
|
||||
@@ -65,13 +91,33 @@ template(name="cardDetails")
|
||||
else
|
||||
p.warning {{_ 'card-archived'}}
|
||||
|
||||
// Upload progress indicator for drag-and-drop uploads
|
||||
if hasActiveUploads
|
||||
.card-details-upload-progress
|
||||
.upload-progress-header
|
||||
| 📤
|
||||
span {{_ 'uploading-files'}} ({{uploadCount}})
|
||||
each uploads
|
||||
.upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
|
||||
.upload-progress-filename {{file.name}}
|
||||
.upload-progress-bar
|
||||
.upload-progress-fill(style="width: {{progress}}%")
|
||||
if $eq status 'error'
|
||||
.upload-progress-error
|
||||
| ⚠️
|
||||
span {{_ 'upload-failed'}}
|
||||
else if $eq status 'completed'
|
||||
.upload-progress-success
|
||||
| ✅
|
||||
span {{_ 'upload-completed'}}
|
||||
|
||||
.card-details-left
|
||||
|
||||
.card-details-items
|
||||
if currentBoard.allowsLabels
|
||||
.card-details-item.card-details-item-labels
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-tags
|
||||
| 🏷️
|
||||
| {{_ 'labels'}}
|
||||
a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
|
||||
each labels
|
||||
@@ -81,15 +127,25 @@ template(name="cardDetails")
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
if currentBoard.hasAnyAllowsDate
|
||||
hr
|
||||
|
||||
.card-details-item.card-details-item-date-format
|
||||
h3.card-details-item-title
|
||||
| 📅
|
||||
| {{_ '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
|
||||
i.fa.fa-sign-out
|
||||
| 📥
|
||||
| {{_ 'card-received'}}
|
||||
if getReceived
|
||||
+cardReceivedDate
|
||||
@@ -97,12 +153,12 @@ template(name="cardDetails")
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-received-date
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
if currentBoard.allowsStartDate
|
||||
.card-details-item.card-details-item-start
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-hourglass-start
|
||||
| 🚀
|
||||
| {{_ 'card-start'}}
|
||||
if getStart
|
||||
+cardStartDate
|
||||
@@ -110,12 +166,12 @@ template(name="cardDetails")
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-start-date
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
if currentBoard.allowsDueDate
|
||||
.card-details-item.card-details-item-due
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sign-in
|
||||
| ⏰
|
||||
| {{_ 'card-due'}}
|
||||
if getDue
|
||||
+cardDueDate
|
||||
@@ -123,12 +179,12 @@ template(name="cardDetails")
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-due-date
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
if currentBoard.allowsEndDate
|
||||
.card-details-item.card-details-item-end
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-hourglass-end
|
||||
| 🏁
|
||||
| {{_ 'card-end'}}
|
||||
if getEnd
|
||||
+cardEndDate
|
||||
@@ -136,7 +192,7 @@ template(name="cardDetails")
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-end-date
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
if currentBoard.hasAnyAllowsUser
|
||||
hr
|
||||
@@ -144,7 +200,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsCreator
|
||||
.card-details-item.card-details-item-creator
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user
|
||||
| 👤
|
||||
| {{_ 'creator'}}
|
||||
|
||||
+userAvatar(userId=userId noRemove=true)
|
||||
@@ -154,7 +210,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsMembers
|
||||
.card-details-item.card-details-item-members
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-users
|
||||
| 👥
|
||||
| {{_ 'members'}}
|
||||
each userId in getMembers
|
||||
+userAvatar(userId=userId cardId=_id)
|
||||
@@ -162,30 +218,30 @@ template(name="cardDetails")
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
//if assigneeSelected
|
||||
if currentBoard.allowsAssignee
|
||||
.card-details-item.card-details-item-assignees
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user
|
||||
| 👤
|
||||
| {{_ 'assignee'}}
|
||||
each userId in getAssignees
|
||||
+userAvatar(userId=userId cardId=_id assignee=true)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
if canModifyCard
|
||||
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
if currentUser.isWorker
|
||||
unless assigneeSelected
|
||||
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
//.card-details-items
|
||||
if currentBoard.allowsRequestedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-shopping-cart
|
||||
| 🛒
|
||||
| {{_ 'requested-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
@@ -205,7 +261,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsAssignedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user-plus
|
||||
| ✍️
|
||||
| {{_ 'assigned-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
@@ -228,7 +284,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsCardSortingByNumber
|
||||
.card-details-item.card-details-sort-order
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sort
|
||||
| 🔢
|
||||
| {{_ 'sort'}}
|
||||
if canModifyCard
|
||||
+inlinedForm(classNames="js-card-details-sort")
|
||||
@@ -241,7 +297,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsShowLists
|
||||
.card-details-item.card-details-show-lists
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-list
|
||||
| 📋
|
||||
| {{_ 'list'}}
|
||||
select.js-select-card-details-lists(disabled="{{#unless canModifyCard}}disabled{{/unless}}")
|
||||
each currentBoard.lists
|
||||
@@ -267,7 +323,7 @@ template(name="cardDetails")
|
||||
hr
|
||||
.card-details-item.card-details-item-customfield
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-list-alt
|
||||
| 📋
|
||||
= definition.name
|
||||
+cardCustomField
|
||||
|
||||
@@ -278,14 +334,14 @@ 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
|
||||
.vote-title
|
||||
div.flex
|
||||
h3
|
||||
i.fa.fa-thumbs-up
|
||||
| 👍
|
||||
| {{_ 'vote-question'}}
|
||||
if getVoteEnd
|
||||
+voteEndDate
|
||||
@@ -303,11 +359,11 @@ template(name="cardDetails")
|
||||
if showVotingButtons
|
||||
button.card-details-green.js-vote.js-vote-positive(class="{{#if voteState}}voted{{/if}}")
|
||||
if voteState
|
||||
i.fa.fa-thumbs-up
|
||||
| 👍
|
||||
| {{_ 'vote-for-it'}}
|
||||
button.card-details-red.js-vote.js-vote-negative(class="{{#if $eq voteState false}}voted{{/if}}")
|
||||
if $eq voteState false
|
||||
i.fa.fa-thumbs-down
|
||||
| 👎
|
||||
| {{_ 'vote-against'}}
|
||||
|
||||
if getPokerQuestion
|
||||
@@ -315,7 +371,7 @@ template(name="cardDetails")
|
||||
.poker-title
|
||||
div.flex
|
||||
h3
|
||||
i.fa.fa-thumbs-up
|
||||
| 👍
|
||||
| {{_ 'poker-question'}}
|
||||
if getPokerEnd
|
||||
+pokerEndDate
|
||||
@@ -330,52 +386,52 @@ template(name="cardDetails")
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-one(class="{{#if $eq pokerState 'one'}}poker-voted{{/if}}") {{_ 'poker-one'}}
|
||||
if $eq pokerState "one"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-two(class="{{#if $eq pokerState 'two'}}poker-voted{{/if}}") {{_ 'poker-two'}}
|
||||
if $eq pokerState "two"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-three(class="{{#if $eq pokerState 'three'}}poker-voted{{/if}}") {{_ 'poker-three'}}
|
||||
if $eq pokerState "three"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-five(class="{{#if $eq pokerState 'five'}}poker-voted{{/if}}") {{_ 'poker-five'}}
|
||||
if $eq pokerState "five"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-eight(class="{{#if $eq pokerState 'eight'}}poker-voted{{/if}}") {{_ 'poker-eight'}}
|
||||
if $eq pokerState "eight"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-thirteen(class="{{#if $eq pokerState 'thirteen'}}poker-voted{{/if}}") {{_ 'poker-thirteen'}}
|
||||
if $eq pokerState "thirteen"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-twenty(class="{{#if $eq pokerState 'twenty'}}poker-voted{{/if}}") {{_ 'poker-twenty'}}
|
||||
if $eq pokerState "twenty"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-forty(class="{{#if $eq pokerState 'forty'}}poker-voted{{/if}}") {{_ 'poker-forty'}}
|
||||
if $eq pokerState "forty"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-one-hundred(class="{{#if $eq pokerState 'oneHundred'}}poker-voted{{/if}}") {{_ 'poker-oneHundred'}}
|
||||
if $eq pokerState "oneHundred"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-unsure(class="{{#if $eq pokerState 'unsure'}}poker-voted{{/if}}") {{_ 'poker-unsure'}}
|
||||
if $eq pokerState "unsure"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
|
||||
if currentUser.isBoardAdmin
|
||||
button.card-details-blue.js-poker-finish(class="{{#if $eq voteState false}}poker-voted{{/if}}") {{_ 'poker-finish'}}
|
||||
@@ -505,7 +561,7 @@ template(name="cardDetails")
|
||||
button.card-details-red.js-poker-replay(class="{{#if $eq voteState false}}voted{{/if}}") {{_ 'poker-replay'}}
|
||||
div.estimation-add
|
||||
button.js-poker-estimation
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
| {{_ 'set-estimation'}}
|
||||
input(type=text,autofocus value=getPokerEstimation,id="pokerEstimation")
|
||||
|
||||
@@ -515,18 +571,18 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsDescriptionTitle
|
||||
hr
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-align-left
|
||||
| 📝
|
||||
| {{_ '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)
|
||||
i.fa.fa-pencil-square-o
|
||||
| ✏️
|
||||
a.js-open-inlined-form(title="{{_ 'edit'}}" value=title)
|
||||
if getDescription
|
||||
+viewer
|
||||
@@ -556,7 +612,7 @@ template(name="cardDetails")
|
||||
if currentBoard.allowsAttachments
|
||||
hr
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-paperclip
|
||||
| 📎
|
||||
| {{_ 'attachments'}}
|
||||
if Meteor.settings.public.attachmentsUploadMaxSize
|
||||
| {{_ 'max-upload-filesize'}} {{Meteor.settings.public.attachmentsUploadMaxSize}}
|
||||
@@ -572,22 +628,24 @@ template(name="cardDetails")
|
||||
unless currentUser.isNoComments
|
||||
.comment-title
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-comment-o
|
||||
| 💬
|
||||
| {{_ 'comments'}}
|
||||
|
||||
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
|
||||
| 📜
|
||||
| {{ _ 'activities'}}
|
||||
if currentUser.isBoardMember
|
||||
.material-toggle-switch(title="{{_ 'show-activities'}}")
|
||||
@@ -597,7 +655,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")
|
||||
@@ -607,41 +665,41 @@ template(name="cardDetails")
|
||||
+activities(card=this mode="card")
|
||||
|
||||
template(name="editCardTitleForm")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
a(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
textarea.js-edit-card-title(rows='1' autofocus dir="auto")
|
||||
= getTitle
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-title-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
|
||||
template(name="editCardRequesterForm")
|
||||
input.js-edit-card-requester(type='text' autofocus value=getRequestedBy dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-requester-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
|
||||
template(name="editCardAssignerForm")
|
||||
input.js-edit-card-assigner(type='text' autofocus value=getAssignedBy dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-assigner-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
|
||||
template(name="editCardSortOrderForm")
|
||||
input.js-edit-card-sort(type='text' autofocus value=sort dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-sort-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
|
||||
template(name="cardDetailsActionsPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-toggle-watch-card
|
||||
if isWatching
|
||||
i.fa.fa-eye
|
||||
| 👁️
|
||||
| {{_ 'unwatch'}}
|
||||
else
|
||||
i.fa.fa-eye-slash
|
||||
| 👁️
|
||||
| {{_ 'watch'}}
|
||||
hr
|
||||
if canModifyCard
|
||||
@@ -652,16 +710,16 @@ template(name="cardDetailsActionsPopup")
|
||||
//li: a.js-attachments {{_ 'card-edit-attachments'}}
|
||||
li
|
||||
a.js-start-voting
|
||||
i.fa.fa-thumbs-up
|
||||
| 👍
|
||||
| {{_ 'card-edit-voting'}}
|
||||
li
|
||||
a.js-start-planning-poker
|
||||
i.fa.fa-thumbs-up
|
||||
| 👍
|
||||
| {{_ 'card-edit-planning-poker'}}
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-custom-fields
|
||||
i.fa.fa-list-alt
|
||||
| 📋
|
||||
| {{_ 'card-edit-custom-fields'}}
|
||||
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
|
||||
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
|
||||
@@ -669,67 +727,127 @@ template(name="cardDetailsActionsPopup")
|
||||
//li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
|
||||
li
|
||||
a.js-spent-time
|
||||
i.fa.fa-clock-o
|
||||
| 🕐
|
||||
| {{_ 'editCardSpentTimePopup-title'}}
|
||||
li
|
||||
a.js-set-card-color
|
||||
i.fa.fa-paint-brush
|
||||
| 🎨
|
||||
| {{_ 'setCardColorPopup-title'}}
|
||||
hr
|
||||
li
|
||||
a.js-toggle-show-list-on-minicard
|
||||
if showListOnMinicard
|
||||
| 👁️
|
||||
| {{_ 'hide-list-on-minicard'}}
|
||||
else
|
||||
| 👁️
|
||||
| {{_ 'show-list-on-minicard'}}
|
||||
if canModifyCard
|
||||
hr
|
||||
else
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-export-card
|
||||
i.fa.fa-share-alt
|
||||
| 📤
|
||||
| {{_ '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
|
||||
| ⬆️
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
| ⬇️
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-move-card
|
||||
| ➡️
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
li
|
||||
a.js-copy-card
|
||||
| 📋
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-copy-checklist-cards
|
||||
| 📋
|
||||
| 📋
|
||||
| {{_ 'copyManyCardsPopup-title'}}
|
||||
unless archived
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-archive
|
||||
| ➡️
|
||||
| 📦
|
||||
| {{_ 'archive-card'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
span.emoji-icon 🔗
|
||||
| {{_ '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
|
||||
| ⬆️
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
| ⬇️
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-move-card
|
||||
| ➡️
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
li
|
||||
a.js-copy-card
|
||||
| 📋
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-copy-checklist-cards
|
||||
| 📋
|
||||
| 📋
|
||||
| {{_ 'copyManyCardsPopup-title'}}
|
||||
unless archived
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-archive
|
||||
| ➡️
|
||||
| 📦
|
||||
| {{_ 'archive-card'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
span.emoji-icon 🔗
|
||||
| {{_ 'cardMorePopup-title'}}
|
||||
|
||||
template(name="exportCardPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a(href="{{exportUrlCardPDF}}",, download="{{exportFilenameCardPDF}}")
|
||||
i.fa.fa-share-alt
|
||||
| 📤
|
||||
| {{_ 'export-card-pdf'}}
|
||||
|
||||
template(name="moveCardPopup")
|
||||
@@ -758,17 +876,29 @@ template(name="copyAndMoveCard")
|
||||
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'}}
|
||||
@@ -779,12 +909,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'}}")
|
||||
@@ -793,12 +924,13 @@ 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
|
||||
ul.pop-over-list.js-card-assignee-list
|
||||
li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
|
||||
@@ -806,9 +938,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
|
||||
@@ -832,7 +965,7 @@ template(name="cardMorePopup")
|
||||
span.clearfix
|
||||
span {{_ 'link-card'}}
|
||||
= ' '
|
||||
i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
| {{#if board.isPublic}}🌐{{else}}🔒{{/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'}}
|
||||
@@ -874,7 +1007,7 @@ template(name="setCardColorPopup")
|
||||
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'}}
|
||||
|
||||
@@ -908,12 +1041,12 @@ template(name="cardStartVotingPopup")
|
||||
.materialCheckBox#vote-public(name="vote-public" class="{{#if votePublic}}is-checked{{/if}}")
|
||||
span {{_ 'vote-public'}}
|
||||
.check-div.flex
|
||||
i.fa.fa-hourglass-end
|
||||
| ⏰
|
||||
a.js-end-date
|
||||
span
|
||||
| {{_ 'card-end'}}
|
||||
unless getVoteEnd
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
if getVoteEnd
|
||||
+voteEndDate
|
||||
|
||||
@@ -954,12 +1087,12 @@ template(name="cardStartPlanningPokerPopup")
|
||||
.materialCheckBox#poker-allow-non-members(name="poker-allow-non-members" class="{{#if pokerAllowNonBoardMembers}}is-checked{{/if}}")
|
||||
span {{_ 'allowNonBoardMembers'}}
|
||||
.check-div.flex
|
||||
i.fa.fa-hourglass-end
|
||||
| ⏰
|
||||
a.js-end-date
|
||||
span
|
||||
| {{_ 'card-end'}}
|
||||
unless getPokerEnd
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
if getPokerEnd
|
||||
+pokerEndDate
|
||||
|
||||
|
||||
@@ -1,7 +1,27 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import { DatePicker } from '/client/lib/datepicker';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
import Cards from '/models/cards';
|
||||
import Boards from '/models/boards';
|
||||
import Checklists from '/models/checklists';
|
||||
@@ -12,6 +32,9 @@ import CardComments from '/models/cardComments';
|
||||
import { ALLOWED_COLORS } from '/config/const';
|
||||
import { UserAvatar } from '../users/userAvatar';
|
||||
import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList';
|
||||
import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard';
|
||||
import { handleFileUpload } from './attachments';
|
||||
import uploadProgressManager from '../../lib/uploadProgressManager';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
const { calculateIndexData } = Utils;
|
||||
@@ -42,7 +65,11 @@ BlazeComponent.extendComponent({
|
||||
const boardBody = this.parentComponent().parentComponent();
|
||||
//in Miniview parent is Board, not BoardBody.
|
||||
if (boardBody !== null) {
|
||||
boardBody.showOverlay.set(true);
|
||||
// Only show overlay in mobile mode, not in desktop mode
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (isMobile) {
|
||||
boardBody.showOverlay.set(true);
|
||||
}
|
||||
boardBody.mouseHasEnterCardDetails = false;
|
||||
}
|
||||
}
|
||||
@@ -60,6 +87,7 @@ BlazeComponent.extendComponent({
|
||||
|
||||
isWatching() {
|
||||
const card = this.currentData();
|
||||
if (!card || typeof card.findWatcher !== 'function') return false;
|
||||
return card.findWatcher(Meteor.userId());
|
||||
},
|
||||
|
||||
@@ -72,6 +100,23 @@ BlazeComponent.extendComponent({
|
||||
return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized();
|
||||
},
|
||||
|
||||
showActivities() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
return user && user.hasShowActivities();
|
||||
},
|
||||
|
||||
cardCollapsed() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && user.profile) {
|
||||
return !!user.profile.cardCollapsed;
|
||||
}
|
||||
if (Users.getPublicCardCollapsed) {
|
||||
const stored = Users.getPublicCardCollapsed();
|
||||
if (typeof stored === 'boolean') return stored;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
presentParentTask() {
|
||||
let result = this.currentBoard.presentParentTask;
|
||||
if (result === null || result === undefined) {
|
||||
@@ -124,8 +169,9 @@ BlazeComponent.extendComponent({
|
||||
* @return is the list id the current list id ?
|
||||
*/
|
||||
isCurrentListId(listId) {
|
||||
const ret = this.data().listId == listId;
|
||||
return ret;
|
||||
const data = this.data();
|
||||
if (!data || typeof data.listId === 'undefined') return false;
|
||||
return data.listId == listId;
|
||||
},
|
||||
|
||||
onRendered() {
|
||||
@@ -275,17 +321,147 @@ BlazeComponent.extendComponent({
|
||||
return [
|
||||
{
|
||||
...events,
|
||||
'click .js-card-collapse-toggle'() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const currentState = user && user.profile ? !!user.profile.cardCollapsed : !!Users.getPublicCardCollapsed();
|
||||
if (user) {
|
||||
Meteor.call('setCardCollapsed', !currentState);
|
||||
} else if (Users.setPublicCardCollapsed) {
|
||||
Users.setPublicCardCollapsed(!currentState);
|
||||
}
|
||||
},
|
||||
'mousedown .js-card-drag-handle'(event) {
|
||||
event.preventDefault();
|
||||
const $card = $(event.target).closest('.card-details');
|
||||
const startX = event.clientX;
|
||||
const startY = event.clientY;
|
||||
const startLeft = $card.offset().left;
|
||||
const startTop = $card.offset().top;
|
||||
|
||||
const onMouseMove = (e) => {
|
||||
const deltaX = e.clientX - startX;
|
||||
const deltaY = e.clientY - startY;
|
||||
$card.css({
|
||||
left: startLeft + deltaX + 'px',
|
||||
top: startTop + deltaY + 'px'
|
||||
});
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
$(document).off('mousemove', onMouseMove);
|
||||
$(document).off('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
$(document).on('mousemove', onMouseMove);
|
||||
$(document).on('mouseup', onMouseUp);
|
||||
},
|
||||
'mousedown .js-card-title-drag-handle'(event) {
|
||||
// Allow dragging from title for ReadOnly users
|
||||
// Don't interfere with text selection
|
||||
if (event.target.tagName === 'A' || $(event.target).closest('a').length > 0) {
|
||||
return; // Don't drag if clicking on links
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const $card = $(event.target).closest('.card-details');
|
||||
const startX = event.clientX;
|
||||
const startY = event.clientY;
|
||||
const startLeft = $card.offset().left;
|
||||
const startTop = $card.offset().top;
|
||||
|
||||
const onMouseMove = (e) => {
|
||||
const deltaX = e.clientX - startX;
|
||||
const deltaY = e.clientY - startY;
|
||||
$card.css({
|
||||
left: startLeft + deltaX + 'px',
|
||||
top: startTop + deltaY + 'px'
|
||||
});
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
$(document).off('mousemove', onMouseMove);
|
||||
$(document).off('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
$(document).on('mousemove', onMouseMove);
|
||||
$(document).on('mouseup', onMouseUp);
|
||||
},
|
||||
'click .js-close-card-details'() {
|
||||
Utils.goBoardId(this.data().boardId);
|
||||
// Get board ID from either the card data or current board in session
|
||||
const card = this.currentData() || this.data();
|
||||
const boardId = (card && card.boardId) || Utils.getCurrentBoard()._id;
|
||||
const cardId = card && card._id;
|
||||
|
||||
if (boardId) {
|
||||
// In desktop mode, remove from openCards array
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (!isMobile && cardId) {
|
||||
const openCards = Session.get('openCards') || [];
|
||||
const filtered = openCards.filter(id => id !== cardId);
|
||||
Session.set('openCards', filtered);
|
||||
|
||||
// If this was the current card, clear it
|
||||
if (Session.get('currentCard') === cardId) {
|
||||
Session.set('currentCard', null);
|
||||
}
|
||||
// Don't navigate away in desktop mode - just close the card
|
||||
return;
|
||||
}
|
||||
|
||||
// Mobile mode: Clear the current card session to close the card
|
||||
Session.set('currentCard', null);
|
||||
|
||||
// Navigate back to board without card
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (board) {
|
||||
FlowRouter.go('board', {
|
||||
id: board._id,
|
||||
slug: board.slug,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-copy-link'(event) {
|
||||
event.preventDefault();
|
||||
const promise = Utils.copyTextToClipboard(event.target.href);
|
||||
const url = this.data().absoluteUrl();
|
||||
const promise = Utils.copyTextToClipboard(url);
|
||||
|
||||
const $tooltip = this.$('.card-details-header .copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
},
|
||||
'change .js-date-format-selector'(event) {
|
||||
const dateFormat = event.target.value;
|
||||
Meteor.call('changeDateFormat', dateFormat);
|
||||
},
|
||||
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
|
||||
// Mobile: switch to desktop popup view (maximize)
|
||||
'click .js-mobile-switch-to-desktop'(event) {
|
||||
event.preventDefault();
|
||||
// Switch global mode to desktop so the card appears as desktop popup
|
||||
Utils.setMobileMode(false);
|
||||
},
|
||||
'click .js-card-zoom-in'(event) {
|
||||
event.preventDefault();
|
||||
const current = Utils.getCardZoom();
|
||||
const newZoom = Math.min(3.0, current + 0.1);
|
||||
Utils.setCardZoom(newZoom);
|
||||
},
|
||||
'click .js-card-zoom-out'(event) {
|
||||
event.preventDefault();
|
||||
const current = Utils.getCardZoom();
|
||||
const newZoom = Math.max(0.5, current - 0.1);
|
||||
Utils.setCardZoom(newZoom);
|
||||
},
|
||||
'click .js-card-mobile-desktop-toggle'(event) {
|
||||
event.preventDefault();
|
||||
const currentMode = Utils.getMobileMode();
|
||||
Utils.setMobileMode(!currentMode);
|
||||
},
|
||||
'click .js-card-mobile-desktop-toggle'(event) {
|
||||
event.preventDefault();
|
||||
const currentMode = Utils.getMobileMode();
|
||||
Utils.setMobileMode(!currentMode);
|
||||
},
|
||||
'submit .js-card-description'(event) {
|
||||
event.preventDefault();
|
||||
const description = this.currentComponent().getValue();
|
||||
@@ -378,9 +554,6 @@ BlazeComponent.extendComponent({
|
||||
Session.set('cardDetailsIsDragging', false);
|
||||
Session.set('cardDetailsIsMouseDown', false);
|
||||
},
|
||||
'click #toggleShowActivitiesCard'() {
|
||||
this.data().toggleShowActivities();
|
||||
},
|
||||
'click #toggleHideCheckedChecklistItems'() {
|
||||
this.data().toggleHideCheckedChecklistItems();
|
||||
},
|
||||
@@ -405,56 +578,57 @@ BlazeComponent.extendComponent({
|
||||
) {
|
||||
newState = forIt;
|
||||
}
|
||||
this.data().setVote(Meteor.userId(), newState);
|
||||
// Use secure server method; direct client updates to vote are blocked
|
||||
Meteor.call('cards.vote', this.data()._id, newState);
|
||||
},
|
||||
'click .js-poker'(e) {
|
||||
let newState = null;
|
||||
if ($(e.target).hasClass('js-poker-vote-one')) {
|
||||
newState = 'one';
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-two')) {
|
||||
newState = 'two';
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-three')) {
|
||||
newState = 'three';
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-five')) {
|
||||
newState = 'five';
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-eight')) {
|
||||
newState = 'eight';
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-thirteen')) {
|
||||
newState = 'thirteen';
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-twenty')) {
|
||||
newState = 'twenty';
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-forty')) {
|
||||
newState = 'forty';
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-one-hundred')) {
|
||||
newState = 'oneHundred';
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-unsure')) {
|
||||
newState = 'unsure';
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
}
|
||||
},
|
||||
'click .js-poker-finish'(e) {
|
||||
if ($(e.target).hasClass('js-poker-finish')) {
|
||||
e.preventDefault();
|
||||
const now = moment().format('YYYY-MM-DD HH:mm');
|
||||
this.data().setPokerEnd(now);
|
||||
const now = new Date();
|
||||
Meteor.call('cards.setPokerEnd', this.data()._id, now);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -462,9 +636,9 @@ BlazeComponent.extendComponent({
|
||||
if ($(e.target).hasClass('js-poker-replay')) {
|
||||
e.preventDefault();
|
||||
this.currentCard = this.currentData();
|
||||
this.currentCard.replayPoker();
|
||||
this.data().unsetPokerEnd();
|
||||
this.data().unsetPokerEstimation();
|
||||
Meteor.call('cards.replayPoker', this.currentCard._id);
|
||||
Meteor.call('cards.unsetPokerEnd', this.currentCard._id);
|
||||
Meteor.call('cards.unsetPokerEstimation', this.currentCard._id);
|
||||
}
|
||||
},
|
||||
'click .js-poker-estimation'(event) {
|
||||
@@ -475,9 +649,65 @@ BlazeComponent.extendComponent({
|
||||
this.find('#pokerEstimation').value = '';
|
||||
|
||||
if (ruleTitle) {
|
||||
this.data().setPokerEstimation(parseInt(ruleTitle, 10));
|
||||
Meteor.call('cards.setPokerEstimation', this.data()._id, parseInt(ruleTitle, 10));
|
||||
} else {
|
||||
this.data().setPokerEstimation('');
|
||||
Meteor.call('cards.unsetPokerEstimation', this.data()._id);
|
||||
}
|
||||
}
|
||||
},
|
||||
// Drag and drop file upload handlers
|
||||
'dragover .js-card-details'(event) {
|
||||
// Only prevent default for file drags to avoid interfering with other drag operations
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
'dragenter .js-card-details'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const card = this.data();
|
||||
const board = card.board();
|
||||
// Only allow drag-and-drop if user can modify card and board allows attachments
|
||||
if (Utils.canModifyCard() && board && board.allowsAttachments) {
|
||||
$(event.currentTarget).addClass('is-dragging-over');
|
||||
}
|
||||
}
|
||||
},
|
||||
'dragleave .js-card-details'(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 .js-card-details'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
|
||||
const card = this.data();
|
||||
const board = card.board();
|
||||
|
||||
// Check permissions
|
||||
if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a file drop (not a checklist item reorder)
|
||||
if (!dataTransfer.files || dataTransfer.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileUpload(card, files);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -490,6 +720,21 @@ Template.cardDetails.helpers({
|
||||
isPopup() {
|
||||
let ret = !!Utils.getPopupCardId();
|
||||
return ret;
|
||||
},
|
||||
isDateFormat(format) {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (!currentUser) return format === 'YYYY-MM-DD';
|
||||
return currentUser.getDateFormat() === format;
|
||||
},
|
||||
// Upload progress helpers
|
||||
hasActiveUploads() {
|
||||
return uploadProgressManager.hasActiveUploads(this._id);
|
||||
},
|
||||
uploads() {
|
||||
return uploadProgressManager.getUploadsForCard(this._id);
|
||||
},
|
||||
uploadCount() {
|
||||
return uploadProgressManager.getUploadCountForCard(this._id);
|
||||
}
|
||||
});
|
||||
Template.cardDetailsPopup.onDestroyed(() => {
|
||||
@@ -586,12 +831,17 @@ Template.editCardSortOrderForm.onRendered(function () {
|
||||
|
||||
Template.cardDetailsActionsPopup.helpers({
|
||||
isWatching() {
|
||||
if (!this || typeof this.findWatcher !== 'function') return false;
|
||||
return this.findWatcher(Meteor.userId());
|
||||
},
|
||||
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
|
||||
showListOnMinicard() {
|
||||
return this.showListOnMinicard;
|
||||
},
|
||||
});
|
||||
|
||||
Template.cardDetailsActionsPopup.events({
|
||||
@@ -637,6 +887,12 @@ Template.cardDetailsActionsPopup.events({
|
||||
if (!err && ret) Popup.close();
|
||||
});
|
||||
},
|
||||
'click .js-toggle-show-list-on-minicard'() {
|
||||
const currentCard = this;
|
||||
const newValue = !currentCard.showListOnMinicard;
|
||||
Cards.update(currentCard._id, { $set: { showListOnMinicard: newValue } });
|
||||
Popup.close();
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
@@ -672,6 +928,12 @@ Template.cardMembersPopup.onCreated(function () {
|
||||
});
|
||||
|
||||
Template.cardMembersPopup.events({
|
||||
'click .js-select-member'(event) {
|
||||
const card = Utils.getCurrentCard();
|
||||
const memberId = this.userId;
|
||||
card.toggleMember(memberId);
|
||||
event.preventDefault();
|
||||
},
|
||||
'keyup .card-members-filter'(event) {
|
||||
const members = filterMembers(event.target.value);
|
||||
Template.instance().members.set(members);
|
||||
@@ -679,8 +941,23 @@ Template.cardMembersPopup.events({
|
||||
});
|
||||
|
||||
Template.cardMembersPopup.helpers({
|
||||
isCardMember() {
|
||||
const card = Template.parentData();
|
||||
const cardMembers = card.getMembers();
|
||||
|
||||
return _.contains(cardMembers, this.userId);
|
||||
},
|
||||
|
||||
members() {
|
||||
return _.sortBy(Template.instance().members.get(),'fullname');
|
||||
const members = Template.instance().members.get();
|
||||
const uniqueMembers = _.uniq(members, 'userId');
|
||||
return _.sortBy(uniqueMembers, member => {
|
||||
const user = ReactiveCache.getUser(member.userId);
|
||||
return user ? user.profile.fullname : '';
|
||||
});
|
||||
},
|
||||
userData() {
|
||||
return ReactiveCache.getUser(this.userId);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -729,26 +1006,42 @@ Template.editCardAssignerForm.events({
|
||||
});
|
||||
|
||||
/** Move Card Dialog */
|
||||
(class extends DialogWithBoardSwimlaneList {
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(boardId, swimlaneId, listId, options) {
|
||||
setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
||||
const card = this.data();
|
||||
const minOrder = card.getMinSort(listId, swimlaneId);
|
||||
card.move(boardId, swimlaneId, listId, minOrder - 1);
|
||||
let sortIndex = 0;
|
||||
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
const position = this.$('input[name="position"]:checked').val();
|
||||
if (position === 'above') {
|
||||
sortIndex = targetCard.sort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetCard.sort + 0.5;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no card selected, move to end
|
||||
sortIndex = card.getMaxSort(options.listId, options.swimlaneId) + 1;
|
||||
}
|
||||
|
||||
card.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
||||
}
|
||||
}).register('moveCardPopup');
|
||||
|
||||
/** Copy Card Dialog */
|
||||
(class extends DialogWithBoardSwimlaneList {
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(boardId, swimlaneId, listId, options) {
|
||||
setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
||||
const card = this.data();
|
||||
|
||||
@@ -757,8 +1050,30 @@ Template.editCardAssignerForm.events({
|
||||
const title = textarea.val().trim();
|
||||
|
||||
if (title) {
|
||||
// insert new card to the top of new list
|
||||
const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, listId, true, {title: title});
|
||||
const newCardId = Meteor.call('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, true, {title: title});
|
||||
|
||||
// Position the copied card
|
||||
if (newCardId) {
|
||||
const newCard = ReactiveCache.getCard(newCardId);
|
||||
let sortIndex = 0;
|
||||
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
const position = this.$('input[name="position"]:checked').val();
|
||||
if (position === 'above') {
|
||||
sortIndex = targetCard.sort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetCard.sort + 0.5;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no card selected, copy to end
|
||||
sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1;
|
||||
}
|
||||
|
||||
newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
||||
}
|
||||
|
||||
// In case the filter is active we need to add the newly inserted card in
|
||||
// the list of exceptions -- cards that are not filtered. Otherwise the
|
||||
@@ -770,12 +1085,12 @@ Template.editCardAssignerForm.events({
|
||||
}).register('copyCardPopup');
|
||||
|
||||
/** Convert Checklist-Item to card dialog */
|
||||
(class extends DialogWithBoardSwimlaneList {
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(boardId, swimlaneId, listId, options) {
|
||||
setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
||||
const card = this.data();
|
||||
|
||||
@@ -785,14 +1100,29 @@ Template.editCardAssignerForm.events({
|
||||
if (title) {
|
||||
const _id = Cards.insert({
|
||||
title: title,
|
||||
listId: listId,
|
||||
boardId: boardId,
|
||||
swimlaneId: swimlaneId,
|
||||
listId: options.listId,
|
||||
boardId: options.boardId,
|
||||
swimlaneId: options.swimlaneId,
|
||||
sort: 0,
|
||||
});
|
||||
const card = ReactiveCache.getCard(_id);
|
||||
const minOrder = card.getMinSort();
|
||||
card.move(card.boardId, card.swimlaneId, card.listId, minOrder - 1);
|
||||
const newCard = ReactiveCache.getCard(_id);
|
||||
|
||||
let sortIndex = 0;
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
const position = this.$('input[name="position"]:checked').val();
|
||||
if (position === 'above') {
|
||||
sortIndex = targetCard.sort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetCard.sort + 0.5;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1;
|
||||
}
|
||||
|
||||
newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
||||
|
||||
Filter.addException(_id);
|
||||
}
|
||||
@@ -800,12 +1130,12 @@ Template.editCardAssignerForm.events({
|
||||
}).register('convertChecklistItemToCardPopup');
|
||||
|
||||
/** Copy many cards dialog */
|
||||
(class extends DialogWithBoardSwimlaneList {
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(boardId, swimlaneId, listId, options) {
|
||||
setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
||||
const card = this.data();
|
||||
|
||||
@@ -815,7 +1145,29 @@ Template.editCardAssignerForm.events({
|
||||
if (title) {
|
||||
const titleList = JSON.parse(title);
|
||||
for (const obj of titleList) {
|
||||
const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, listId, false, {title: obj.title, description: obj.description});
|
||||
const newCardId = Meteor.call('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, false, {title: obj.title, description: obj.description});
|
||||
|
||||
// Position the copied card
|
||||
if (newCardId) {
|
||||
const newCard = ReactiveCache.getCard(newCardId);
|
||||
let sortIndex = 0;
|
||||
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
const position = this.$('input[name="position"]:checked').val();
|
||||
if (position === 'above') {
|
||||
sortIndex = targetCard.sort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetCard.sort + 0.5;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1;
|
||||
}
|
||||
|
||||
newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
||||
}
|
||||
|
||||
// In case the filter is active we need to add the newly inserted card in
|
||||
// the list of exceptions -- cards that are not filtered. Otherwise the
|
||||
@@ -865,6 +1217,51 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
}).register('setCardColorPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentColor = new ReactiveVar(null);
|
||||
},
|
||||
|
||||
colors() {
|
||||
return ALLOWED_COLORS.map((color) => ({ color, name: '' }));
|
||||
},
|
||||
|
||||
isSelected(color) {
|
||||
return this.currentColor.get() === color;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-palette-color'(event) {
|
||||
// Extract color from class name like "card-details-red"
|
||||
const classes = $(event.currentTarget).attr('class').split(' ');
|
||||
const colorClass = classes.find(cls => cls.startsWith('card-details-'));
|
||||
const color = colorClass ? colorClass.replace('card-details-', '') : null;
|
||||
this.currentColor.set(color);
|
||||
},
|
||||
'click .js-submit'(event) {
|
||||
event.preventDefault();
|
||||
const color = this.currentColor.get();
|
||||
// Use MultiSelection to get selected cards and set color on each
|
||||
ReactiveCache.getCards(MultiSelection.getMongoSelector()).forEach(card => {
|
||||
card.setColor(color);
|
||||
});
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-remove-color'(event) {
|
||||
event.preventDefault();
|
||||
// Use MultiSelection to get selected cards and remove color from each
|
||||
ReactiveCache.getCards(MultiSelection.getMongoSelector()).forEach(card => {
|
||||
card.setColor(null);
|
||||
});
|
||||
Popup.back();
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('setSelectionColorPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentCard = this.currentData();
|
||||
@@ -999,20 +1396,15 @@ BlazeComponent.extendComponent({
|
||||
'is-checked',
|
||||
);
|
||||
const endString = this.currentCard.getVoteEnd();
|
||||
|
||||
this.currentCard.setVoteQuestion(
|
||||
voteQuestion,
|
||||
publicVote,
|
||||
allowNonBoardMembers,
|
||||
);
|
||||
Meteor.call('cards.setVoteQuestion', this.currentCard._id, voteQuestion, publicVote, allowNonBoardMembers);
|
||||
if (endString) {
|
||||
this.currentCard.setVoteEnd(endString);
|
||||
Meteor.call('cards.setVoteEnd', this.currentCard._id, endString);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => {
|
||||
event.preventDefault();
|
||||
this.currentCard.unsetVote();
|
||||
Meteor.call('cards.unsetVote', this.currentCard._id);
|
||||
Popup.back();
|
||||
}),
|
||||
'click a.js-toggle-vote-public'(event) {
|
||||
@@ -1031,8 +1423,8 @@ BlazeComponent.extendComponent({
|
||||
// editVoteEndDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
this.data().getVoteEnd() && this.date.set(moment(this.data().getVoteEnd()));
|
||||
super.onCreated(formatDateTime(now()));
|
||||
this.data().getVoteEnd() && this.date.set(new Date(this.data().getVoteEnd()));
|
||||
}
|
||||
events() {
|
||||
return [
|
||||
@@ -1043,12 +1435,12 @@ BlazeComponent.extendComponent({
|
||||
// if no time was given, init with 12:00
|
||||
const time =
|
||||
evt.target.time.value ||
|
||||
moment(new Date().setHours(12, 0, 0)).format('LT');
|
||||
formatTime(new Date().setHours(12, 0, 0));
|
||||
|
||||
const dateString = `${evt.target.date.value} ${time}`;
|
||||
|
||||
/*
|
||||
const newDate = moment(dateString, 'L LT', true);
|
||||
const newDate = parseDate(dateString, ['L LT'], true);
|
||||
if (newDate.isValid()) {
|
||||
// if active vote - store it
|
||||
if (this.currentData().getVoteQuestion()) {
|
||||
@@ -1062,28 +1454,27 @@ BlazeComponent.extendComponent({
|
||||
|
||||
*/
|
||||
|
||||
// Try to parse different date formats of all languages.
|
||||
// This code is same for vote and planning poker.
|
||||
const usaDate = moment(dateString, 'L LT', true);
|
||||
const euroAmDate = moment(dateString, 'DD.MM.YYYY LT', true);
|
||||
const euro24hDate = moment(dateString, 'DD.MM.YYYY HH.mm', true);
|
||||
const eurodotDate = moment(dateString, 'DD.MM.YYYY HH:mm', true);
|
||||
const minusDate = moment(dateString, 'YYYY-MM-DD HH:mm', true);
|
||||
const slashDate = moment(dateString, 'DD/MM/YYYY HH.mm', true);
|
||||
const dotDate = moment(dateString, 'DD/MM/YYYY HH:mm', true);
|
||||
const brezhonegDate = moment(dateString, 'DD/MM/YYYY h[e]mm A', true);
|
||||
const hrvatskiDate = moment(dateString, 'DD. MM. YYYY H:mm', true);
|
||||
const latviaDate = moment(dateString, 'YYYY.MM.DD. H:mm', true);
|
||||
const nederlandsDate = moment(dateString, 'DD-MM-YYYY HH:mm', true);
|
||||
// greekDate does not work: el Greek Ελληνικά ,
|
||||
// it has date format DD/MM/YYYY h:mm MM like 20/06/2021 11:15 MM
|
||||
// where MM is maybe some text like AM/PM ?
|
||||
// Also some other languages that have non-ascii characters in dates
|
||||
// do not work.
|
||||
const greekDate = moment(dateString, 'DD/MM/YYYY h:mm A', true);
|
||||
const macedonianDate = moment(dateString, 'D.MM.YYYY H:mm', true);
|
||||
// Try to parse different date formats using native Date parsing
|
||||
const formats = [
|
||||
'YYYY-MM-DD HH:mm',
|
||||
'MM/DD/YYYY HH:mm',
|
||||
'DD.MM.YYYY HH:mm',
|
||||
'DD/MM/YYYY HH:mm',
|
||||
'DD-MM-YYYY HH:mm'
|
||||
];
|
||||
|
||||
let parsedDate = null;
|
||||
for (const format of formats) {
|
||||
parsedDate = parseDate(dateString, [format], true);
|
||||
if (parsedDate) break;
|
||||
}
|
||||
|
||||
// Fallback to native Date parsing
|
||||
if (!parsedDate) {
|
||||
parsedDate = new Date(dateString);
|
||||
}
|
||||
|
||||
if (usaDate.isValid()) {
|
||||
if (isValidDate(parsedDate)) {
|
||||
// if active poker - store it
|
||||
if (this.currentData().getPokerQuestion()) {
|
||||
this._storeDate(usaDate.toDate());
|
||||
@@ -1212,10 +1603,10 @@ BlazeComponent.extendComponent({
|
||||
];
|
||||
}
|
||||
_storeDate(newDate) {
|
||||
this.card.setVoteEnd(newDate);
|
||||
Meteor.call('cards.setVoteEnd', this.card._id, newDate);
|
||||
}
|
||||
_deleteDate() {
|
||||
this.card.unsetVoteEnd();
|
||||
Meteor.call('cards.unsetVoteEnd', this.card._id);
|
||||
}
|
||||
}.register('editVoteEndDatePopup'));
|
||||
|
||||
@@ -1237,17 +1628,14 @@ BlazeComponent.extendComponent({
|
||||
);
|
||||
const endString = this.currentCard.getPokerEnd();
|
||||
|
||||
this.currentCard.setPokerQuestion(
|
||||
pokerQuestion,
|
||||
allowNonBoardMembers,
|
||||
);
|
||||
Meteor.call('cards.setPokerQuestion', this.currentCard._id, pokerQuestion, allowNonBoardMembers);
|
||||
if (endString) {
|
||||
this.currentCard.setPokerEnd(endString);
|
||||
Meteor.call('cards.setPokerEnd', this.currentCard._id, new Date(endString));
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-remove-poker': Popup.afterConfirm('deletePoker', (event) => {
|
||||
this.currentCard.unsetPoker();
|
||||
Meteor.call('cards.unsetPoker', this.currentCard._id);
|
||||
Popup.back();
|
||||
}),
|
||||
'click a.js-toggle-poker-allow-non-members'(event) {
|
||||
@@ -1262,9 +1650,9 @@ BlazeComponent.extendComponent({
|
||||
// editPokerEndDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
super.onCreated(formatDateTime(now()));
|
||||
this.data().getPokerEnd() &&
|
||||
this.date.set(moment(this.data().getPokerEnd()));
|
||||
this.date.set(new Date(this.data().getPokerEnd()));
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1282,7 +1670,7 @@ BlazeComponent.extendComponent({
|
||||
return moment.localeData().longDateFormat('LT');
|
||||
}
|
||||
|
||||
const newDate = moment(dateString, dateformat() + ' ' + timeformat(), true);
|
||||
const newDate = parseDate(dateString, [dateformat() + ' ' + timeformat()], true);
|
||||
*/
|
||||
|
||||
events() {
|
||||
@@ -1294,7 +1682,7 @@ BlazeComponent.extendComponent({
|
||||
// if no time was given, init with 12:00
|
||||
const time =
|
||||
evt.target.time.value ||
|
||||
moment(new Date().setHours(12, 0, 0)).format('LT');
|
||||
formatTime(new Date().setHours(12, 0, 0));
|
||||
|
||||
const dateString = `${evt.target.date.value} ${time}`;
|
||||
|
||||
@@ -1305,7 +1693,7 @@ BlazeComponent.extendComponent({
|
||||
Maybe client/components/lib/datepicker.jade could have hidden input field for
|
||||
datepicker format that could be used to detect date format?
|
||||
|
||||
const newDate = moment(dateString, dateformat() + ' ' + timeformat(), true);
|
||||
const newDate = parseDate(dateString, [dateformat() + ' ' + timeformat()], true);
|
||||
|
||||
if (newDate.isValid()) {
|
||||
// if active poker - store it
|
||||
@@ -1318,28 +1706,27 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
*/
|
||||
|
||||
// Try to parse different date formats of all languages.
|
||||
// This code is same for vote and planning poker.
|
||||
const usaDate = moment(dateString, 'L LT', true);
|
||||
const euroAmDate = moment(dateString, 'DD.MM.YYYY LT', true);
|
||||
const euro24hDate = moment(dateString, 'DD.MM.YYYY HH.mm', true);
|
||||
const eurodotDate = moment(dateString, 'DD.MM.YYYY HH:mm', true);
|
||||
const minusDate = moment(dateString, 'YYYY-MM-DD HH:mm', true);
|
||||
const slashDate = moment(dateString, 'DD/MM/YYYY HH.mm', true);
|
||||
const dotDate = moment(dateString, 'DD/MM/YYYY HH:mm', true);
|
||||
const brezhonegDate = moment(dateString, 'DD/MM/YYYY h[e]mm A', true);
|
||||
const hrvatskiDate = moment(dateString, 'DD. MM. YYYY H:mm', true);
|
||||
const latviaDate = moment(dateString, 'YYYY.MM.DD. H:mm', true);
|
||||
const nederlandsDate = moment(dateString, 'DD-MM-YYYY HH:mm', true);
|
||||
// greekDate does not work: el Greek Ελληνικά ,
|
||||
// it has date format DD/MM/YYYY h:mm MM like 20/06/2021 11:15 MM
|
||||
// where MM is maybe some text like AM/PM ?
|
||||
// Also some other languages that have non-ascii characters in dates
|
||||
// do not work.
|
||||
const greekDate = moment(dateString, 'DD/MM/YYYY h:mm A', true);
|
||||
const macedonianDate = moment(dateString, 'D.MM.YYYY H:mm', true);
|
||||
// Try to parse different date formats using native Date parsing
|
||||
const formats = [
|
||||
'YYYY-MM-DD HH:mm',
|
||||
'MM/DD/YYYY HH:mm',
|
||||
'DD.MM.YYYY HH:mm',
|
||||
'DD/MM/YYYY HH:mm',
|
||||
'DD-MM-YYYY HH:mm'
|
||||
];
|
||||
|
||||
let parsedDate = null;
|
||||
for (const format of formats) {
|
||||
parsedDate = parseDate(dateString, [format], true);
|
||||
if (parsedDate) break;
|
||||
}
|
||||
|
||||
// Fallback to native Date parsing
|
||||
if (!parsedDate) {
|
||||
parsedDate = new Date(dateString);
|
||||
}
|
||||
|
||||
if (usaDate.isValid()) {
|
||||
if (isValidDate(parsedDate)) {
|
||||
// if active poker - store it
|
||||
if (this.currentData().getPokerQuestion()) {
|
||||
this._storeDate(usaDate.toDate());
|
||||
@@ -1469,10 +1856,10 @@ BlazeComponent.extendComponent({
|
||||
];
|
||||
}
|
||||
_storeDate(newDate) {
|
||||
this.card.setPokerEnd(newDate);
|
||||
Meteor.call('cards.setPokerEnd', this.card._id, newDate);
|
||||
}
|
||||
_deleteDate() {
|
||||
this.card.unsetPokerEnd();
|
||||
Meteor.call('cards.unsetPokerEnd', this.card._id);
|
||||
}
|
||||
}.register('editPokerEndDatePopup'));
|
||||
|
||||
@@ -1544,10 +1931,15 @@ Template.cardAssigneesPopup.helpers({
|
||||
},
|
||||
|
||||
members() {
|
||||
return _.sortBy(Template.instance().members.get(),'fullname');
|
||||
const members = Template.instance().members.get();
|
||||
const uniqueMembers = _.uniq(members, 'userId');
|
||||
return _.sortBy(uniqueMembers, member => {
|
||||
const user = ReactiveCache.getUser(member.userId);
|
||||
return user ? user.profile.fullname : '';
|
||||
});
|
||||
},
|
||||
|
||||
user() {
|
||||
userData() {
|
||||
return ReactiveCache.getUser(this.userId);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -15,8 +15,8 @@ template(name="editCardSpentTime")
|
||||
|
||||
template(name="timeBadge")
|
||||
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}}
|
||||
@@ -37,14 +37,23 @@ textarea.js-edit-checklist-item {
|
||||
.checklist-progress-bar-container .checklist-progress-bar {
|
||||
width: 80%;
|
||||
height: 10px;
|
||||
background-color: #d6ebff !important;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
|
||||
color: #fff !important;
|
||||
background-color: #2196f3 !important;
|
||||
background-color: #3cb500 !important;
|
||||
padding: 0.01em 16px;
|
||||
border-radius: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
/* Grey progress bar when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-bar {
|
||||
background-color: #d9d9d9;
|
||||
}
|
||||
body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
|
||||
background-color: #7a7a7a !important;
|
||||
}
|
||||
.checklist-title {
|
||||
padding: 10px;
|
||||
}
|
||||
@@ -67,10 +76,14 @@ textarea.js-edit-checklist-item {
|
||||
.checklist-title .checklist-stat.is-finished {
|
||||
color: #3cb500;
|
||||
}
|
||||
.checklist-title span.fa.checklist-handle {
|
||||
.checklist-title span.checklist-handle {
|
||||
padding-right: 20px;
|
||||
padding-top: 3px;
|
||||
float: left;
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
#card-details-overlay {
|
||||
top: 0;
|
||||
@@ -101,6 +114,25 @@ textarea.js-edit-checklist-item {
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* iPhone mobile: larger checklist titles and more spacing between items */
|
||||
body.mobile-mode.iphone-device .checklist-title .title {
|
||||
font-size: 1.3em !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
body.mobile-mode.iphone-device .checklist-item {
|
||||
margin-top: 12px !important;
|
||||
margin-bottom: 8px !important;
|
||||
padding: 8px 4px !important;
|
||||
min-height: 44px; /* iOS recommended touch target size */
|
||||
}
|
||||
|
||||
body.mobile-mode.iphone-device .checklist-item span.checklistitem-handle {
|
||||
font-size: 1.5em !important;
|
||||
padding-right: 15px !important;
|
||||
width: 1.5em !important;
|
||||
}
|
||||
.checklist-item.is-checked.invisible {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
@@ -130,6 +162,27 @@ textarea.js-edit-checklist-item {
|
||||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
}
|
||||
/* Unicode checkbox icons styling */
|
||||
.checklist-item .check-box-unicode,
|
||||
.cardCustomField-checkbox .check-box-unicode {
|
||||
font-size: 1.3em;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .checklist-item .check-box.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
body.grey-icons-enabled .checklist-item .check-box-unicode,
|
||||
body.grey-icons-enabled .cardCustomField-checkbox .check-box-unicode {
|
||||
filter: grayscale(100%);
|
||||
-webkit-filter: grayscale(100%);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.checklist-item .item-title {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -144,9 +197,14 @@ textarea.js-edit-checklist-item {
|
||||
word-wrap: break-word;
|
||||
max-width: 420px;
|
||||
}
|
||||
.checklist-item span.fa.checklistitem-handle {
|
||||
.checklist-item span.checklistitem-handle {
|
||||
padding-top: 2px;
|
||||
padding-right: 10px;
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
.js-delete-checklist-item,
|
||||
.js-convert-checklist-item-to-card {
|
||||
|
||||
@@ -1,34 +1,25 @@
|
||||
template(name="checklists")
|
||||
.checklists-title
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
| {{_ 'checklists'}}
|
||||
if canModifyCard
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId position="top")
|
||||
+addChecklistItemForm
|
||||
else
|
||||
a.add-checklist-top.js-open-inlined-form(title="{{_ 'add-checklist'}}")
|
||||
i.fa.fa-plus
|
||||
if currentUser.isBoardMember
|
||||
.material-toggle-switch(title="{{_ 'hide-finished-checklist'}}")
|
||||
//span.toggle-switch-title
|
||||
if card.hideFinishedChecklistIfItemsAreHidden
|
||||
input.toggle-switch(type="checkbox" id="toggleHideFinishedChecklist" checked="checked")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleHideFinishedChecklist")
|
||||
label.toggle-label(for="toggleHideFinishedChecklist")
|
||||
| ➕
|
||||
|
||||
.card-checklist-items
|
||||
each checklist in checklists
|
||||
if checklist.showChecklist card.hideFinishedChecklistIfItemsAreHidden
|
||||
+checklistDetail(checklist = checklist card = card)
|
||||
+checklistDetail(checklist = checklist card = card)
|
||||
|
||||
if canModifyCard
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId)
|
||||
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=false)
|
||||
else
|
||||
a.add-checklist.js-open-inlined-form(title="{{_ 'add-checklist'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
template(name="checklistDetail")
|
||||
.js-checklist.checklist.nodragscroll
|
||||
@@ -38,12 +29,12 @@ template(name="checklistDetail")
|
||||
.checklist-title
|
||||
span
|
||||
if canModifyCard
|
||||
a.fa.fa-navicon.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
|
||||
a.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}") ☰
|
||||
|
||||
if canModifyCard
|
||||
h4.title.js-open-inlined-form.is-editable
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
span.fa.checklist-handle(class="fa-arrows" title="{{_ 'dragChecklist'}}")
|
||||
span.checklist-handle(title="{{_ 'dragChecklist'}}") ↕️
|
||||
+viewer
|
||||
= checklist.title
|
||||
else
|
||||
@@ -62,13 +53,18 @@ template(name="checklistDeletePopup")
|
||||
p {{_ 'confirm-checklist-delete-popup'}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="checklistItemDeletePopup")
|
||||
p {{_ 'confirm-checklist-delete-popup'}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="addChecklistItemForm")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
a(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
textarea.js-add-checklist-item(rows='1' autofocus)
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
|
||||
a.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
|
||||
| ❌
|
||||
if showNewlineBecomesNewChecklistItem
|
||||
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItem'}}")
|
||||
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItem")
|
||||
@@ -81,7 +77,7 @@ template(name="addChecklistItemForm")
|
||||
| {{_ 'originOrder'}}
|
||||
|
||||
template(name="editChecklistItemForm")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
a(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
textarea.js-edit-checklist-item(rows='1' autofocus dir="auto")
|
||||
if $eq type 'item'
|
||||
@@ -90,13 +86,17 @@ template(name="editChecklistItemForm")
|
||||
= checklist.title
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
|
||||
a.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
|
||||
| ❌
|
||||
span(title=createdAt) {{ moment createdAt }}
|
||||
if canModifyCard
|
||||
a.js-delete-checklist-item {{_ "delete"}}...
|
||||
a.js-convert-checklist-item-to-card
|
||||
i.fa.fa-copy
|
||||
| {{_ 'convertChecklistItemToCardPopup-title'}}
|
||||
if $eq type 'item'
|
||||
a.js-delete-checklist-item {{_ "delete"}}...
|
||||
a.js-convert-checklist-item-to-card
|
||||
| 📋
|
||||
| {{_ 'convertChecklistItemToCardPopup-title'}}
|
||||
else
|
||||
a.js-delete-checklist {{_ "delete"}}...
|
||||
|
||||
template(name="checklistItems")
|
||||
if checklist.items.length
|
||||
@@ -105,7 +105,7 @@ template(name="checklistItems")
|
||||
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true position="top")
|
||||
else
|
||||
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
.checklist-items.js-checklist-items
|
||||
each item in checklist.items
|
||||
+inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
|
||||
@@ -117,21 +117,20 @@ template(name="checklistItems")
|
||||
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true)
|
||||
else
|
||||
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
template(name='checklistItemDetail')
|
||||
.js-checklist-item.checklist-item(class="{{#if item.isFinished }}is-checked{{#if checklist.hideCheckedChecklistItems}} invisible{{/if}}{{/if}}{{#if checklist.hideAllChecklistItems}} is-checked invisible{{/if}}"
|
||||
role="checkbox" aria-checked="{{#if item.isFinished }}true{{else}}false{{/if}}" tabindex="0")
|
||||
if canModifyCard
|
||||
.check-box-container
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
|
||||
span.checklistitem-handle(title="{{_ 'dragChecklistItem'}}") ↕️
|
||||
.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 {{#if item.isFinished }}✅{{else}}⬜{{/if}}
|
||||
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
@@ -140,16 +139,16 @@ template(name="checklistActionsPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-delete-checklist.delete-checklist
|
||||
i.fa.fa-trash
|
||||
| 🗑️
|
||||
| {{_ "delete"}} ...
|
||||
a.js-move-checklist.move-checklist
|
||||
i.fa.fa-arrow-right
|
||||
| ➡️
|
||||
| {{_ "moveChecklist"}} ...
|
||||
a.js-copy-checklist.copy-checklist
|
||||
i.fa.fa-copy
|
||||
| 📋
|
||||
| {{_ "copyChecklist"}} ...
|
||||
a.js-hide-checked-checklist-items
|
||||
i.fa.fa-eye-slash
|
||||
| 🙈
|
||||
| {{_ "hideCheckedChecklistItems"}} ...
|
||||
.material-toggle-switch(title="{{_ 'hide-checked-items'}}")
|
||||
if checklist.hideCheckedChecklistItems
|
||||
@@ -158,7 +157,7 @@ template(name="checklistActionsPopup")
|
||||
input.toggle-switch(type="checkbox" id="toggleHideCheckedChecklistItems_{{checklist._id}}")
|
||||
label.toggle-label(for="toggleHideCheckedChecklistItems_{{checklist._id}}")
|
||||
a.js-hide-all-checklist-items
|
||||
i.fa.fa-ban
|
||||
| 🚫
|
||||
| {{_ "hideAllChecklistItems"}} ...
|
||||
.material-toggle-switch(title="{{_ 'hideAllChecklistItems'}}")
|
||||
if checklist.hideAllChecklistItems
|
||||
@@ -166,34 +165,62 @@ template(name="checklistActionsPopup")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleHideAllChecklistItems_{{checklist._id}}")
|
||||
label.toggle-label(for="toggleHideAllChecklistItems_{{checklist._id}}")
|
||||
a.js-toggle-show-checklist-at-minicard
|
||||
| 📋
|
||||
| {{_ "showChecklistAtMinicard"}} ...
|
||||
.material-toggle-switch(title="{{_ 'showChecklistAtMinicard'}}")
|
||||
if checklist.showChecklistAtMinicard
|
||||
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}" checked="checked")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}")
|
||||
label.toggle-label(for="toggleShowChecklistAtMinicard_{{checklist._id}}")
|
||||
|
||||
template(name="copyChecklistPopup")
|
||||
+copyAndMoveChecklist
|
||||
|
||||
template(name="moveChecklistPopup")
|
||||
+copyAndMoveChecklist
|
||||
|
||||
template(name="copyAndMoveChecklist")
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{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'}}:
|
||||
label {{_ 'card'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
||||
template(name="moveChecklistPopup")
|
||||
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 {{_ 'card'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
@@ -65,7 +65,7 @@ BlazeComponent.extendComponent({
|
||||
$(self.itemsDom).sortable('option', 'disabled', !userIsMember());
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$(self.itemsDom).sortable({
|
||||
handle: 'span.fa.checklistitem-handle',
|
||||
handle: 'span.checklistitem-handle',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -157,14 +157,6 @@ BlazeComponent.extendComponent({
|
||||
textarea.focus();
|
||||
},
|
||||
|
||||
deleteItem() {
|
||||
const checklist = this.currentData().checklist;
|
||||
const item = this.currentData().item;
|
||||
if (checklist && item && item._id) {
|
||||
ChecklistItems.remove(item._id);
|
||||
}
|
||||
},
|
||||
|
||||
editChecklist(event) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-edit-checklist-item');
|
||||
@@ -216,14 +208,28 @@ BlazeComponent.extendComponent({
|
||||
'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,
|
||||
'click .js-delete-checklist-item'(event) {
|
||||
const item = this.currentData().item;
|
||||
const confirmFunc = Popup.afterConfirm('checklistItemDelete', function () {
|
||||
if (item && item._id) {
|
||||
ChecklistItems.remove(item._id);
|
||||
}
|
||||
});
|
||||
confirmFunc.call(this, event);
|
||||
},
|
||||
'click .js-delete-checklist'(event) {
|
||||
const checklist = this.currentData().checklist;
|
||||
const confirmFunc = Popup.afterConfirm('checklistDelete', function () {
|
||||
Popup.back(2);
|
||||
if (checklist && checklist._id) {
|
||||
Checklists.remove(checklist._id);
|
||||
}
|
||||
});
|
||||
confirmFunc.call(this, event);
|
||||
},
|
||||
'focus .js-add-checklist-item': this.focusChecklistItem,
|
||||
// add and delete checklist / checklist-item
|
||||
'click .js-open-inlined-form': this.closeAllInlinedForms,
|
||||
'click #toggleHideFinishedChecklist'(event) {
|
||||
event.preventDefault();
|
||||
this.data().card.toggleHideFinishedChecklist();
|
||||
},
|
||||
keydown: this.pressKey,
|
||||
},
|
||||
];
|
||||
@@ -275,8 +281,8 @@ BlazeComponent.extendComponent({
|
||||
Template.checklists.helpers({
|
||||
checklists() {
|
||||
const card = ReactiveCache.getCard(this.cardId);
|
||||
const ret = card.checklists();
|
||||
return ret;
|
||||
if (!card || typeof card.checklists !== 'function') return [];
|
||||
return card.checklists();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -303,13 +309,16 @@ 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-delete-checklist'(event) {
|
||||
const checklist = this.data().checklist;
|
||||
const confirmFunc = Popup.afterConfirm('checklistDelete', function () {
|
||||
Popup.back(2);
|
||||
if (checklist && checklist._id) {
|
||||
Checklists.remove(checklist._id);
|
||||
}
|
||||
});
|
||||
confirmFunc.call(this, event);
|
||||
},
|
||||
'click .js-move-checklist': Popup.open('moveChecklist'),
|
||||
'click .js-copy-checklist': Popup.open('copyChecklist'),
|
||||
'click .js-hide-checked-checklist-items'(event) {
|
||||
@@ -322,6 +331,12 @@ BlazeComponent.extendComponent({
|
||||
this.data().checklist.toggleHideAllChecklistItems();
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-toggle-show-checklist-at-minicard'(event) {
|
||||
event.preventDefault();
|
||||
const checklist = this.data().checklist;
|
||||
checklist.toggleShowChecklistAtMinicard();
|
||||
Popup.back();
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -360,6 +375,7 @@ BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-checklist-item .check-box-unicode': this.toggleItem,
|
||||
'click .js-checklist-item .check-box-container': this.toggleItem,
|
||||
},
|
||||
];
|
||||
@@ -374,7 +390,12 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
|
||||
this.data().checklist.move(cardId);
|
||||
const checklist = this.data().checklist;
|
||||
Meteor.call('moveChecklist', checklist._id, cardId, (error) => {
|
||||
if (error) {
|
||||
console.error('Error moving checklist:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}).register('moveChecklistPopup');
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
.palette-colors {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start; /* left-align color chips in wider popovers */
|
||||
}
|
||||
.palette-colors .palette-color {
|
||||
flex-grow: 1;
|
||||
@@ -222,9 +223,13 @@
|
||||
.card-label-edit-button:hover {
|
||||
background: #dbdbdb;
|
||||
}
|
||||
ul.edit-labels-pop-over span.fa.label-handle {
|
||||
ul.edit-labels-pop-over span.label-handle {
|
||||
padding-right: 10px;
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
ul.edit-labels-pop-over span.fa.label-handle + .card-label {
|
||||
ul.edit-labels-pop-over span.label-handle + .card-label {
|
||||
max-width: 180px;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ template(name="formLabel")
|
||||
.palette-colors: each labels
|
||||
span.card-label.palette-color.js-palette-color(class="card-label-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
|
||||
template(name="createLabelPopup")
|
||||
form.create-label
|
||||
@@ -28,13 +28,14 @@ template(name="cardLabelsPopup")
|
||||
ul.edit-labels-pop-over
|
||||
each board.labels
|
||||
li.js-card-label-item
|
||||
a.card-label-edit-button.fa.fa-pencil.js-edit-label
|
||||
a.card-label-edit-button.js-edit-label
|
||||
| ✏️
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
span.fa.label-handle(class="fa-arrows" title="{{_ 'dragLabel'}}")
|
||||
span.label-handle(title="{{_ 'dragLabel'}}") ↕️
|
||||
span.card-label.card-label-selectable.js-select-label.card-label-wrapper(class="card-label-{{color}}"
|
||||
class="{{# if isLabelSelected ../_id }}active{{/if}}")
|
||||
+viewer
|
||||
= name
|
||||
if(isLabelSelected ../_id)
|
||||
i.card-label-selectable-icon.fa.fa-check
|
||||
| ✅
|
||||
a.quiet-button.full.js-add-label {{_ 'label-create'}}
|
||||
@@ -125,8 +125,19 @@ Template.createLabelPopup.events({
|
||||
.$('#labelName')
|
||||
.val()
|
||||
.trim();
|
||||
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
|
||||
board.addLabel(name, color);
|
||||
|
||||
// Find the selected color by looking for the palette color that contains the checkmark
|
||||
let selectedColor = null;
|
||||
templateInstance.$('.js-palette-color').each(function() {
|
||||
if ($(this).text().includes('✅')) {
|
||||
selectedColor = Blaze.getData(this).color;
|
||||
return false; // break out of loop
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedColor) {
|
||||
board.addLabel(name, selectedColor);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
@@ -144,8 +155,19 @@ Template.editLabelPopup.events({
|
||||
.$('#labelName')
|
||||
.val()
|
||||
.trim();
|
||||
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
|
||||
board.editLabel(this._id, name, color);
|
||||
|
||||
// Find the selected color by looking for the palette color that contains the checkmark
|
||||
let selectedColor = null;
|
||||
templateInstance.$('.js-palette-color').each(function() {
|
||||
if ($(this).text().includes('✅')) {
|
||||
selectedColor = Blaze.getData(this).color;
|
||||
return false; // break out of loop
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedColor) {
|
||||
board.editLabel(this._id, name, selectedColor);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 9px;
|
||||
margin-bottom: 1.2vh;
|
||||
}
|
||||
.minicard-wrapper.placeholder {
|
||||
background: #ccc;
|
||||
border-radius: 9px;
|
||||
border-radius: 1.2vw;
|
||||
}
|
||||
.minicard-wrapper.ui-sortable-helper {
|
||||
cursor: grabbing;
|
||||
@@ -16,43 +16,45 @@
|
||||
}
|
||||
.minicard-wrapper.ui-sortable-helper .and-n-other {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
padding: 4px;
|
||||
height: 2.5vh;
|
||||
padding: 0.5vh;
|
||||
background-color: #f2f2f2;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
border-radius: 0.4vw;
|
||||
}
|
||||
.minicard-wrapper.ui-sortable-helper .multi-selection-checkbox {
|
||||
display: none;
|
||||
}
|
||||
.minicard-wrapper .multi-selection-checkbox + .minicard {
|
||||
margin-left: 8px;
|
||||
margin-left: 1vw;
|
||||
}
|
||||
@media only screen {
|
||||
.minicard {
|
||||
padding: 6px 8px 2px;
|
||||
padding: 0.8vh 1vw 0.3vh;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
background-color: #fff;
|
||||
min-height: 20px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
|
||||
border-radius: 2px;
|
||||
min-height: 2.5vh;
|
||||
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.15);
|
||||
border-radius: 0.3vw;
|
||||
color: #4d4d4d;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s, border-radius 0.2s;
|
||||
}
|
||||
}
|
||||
.minicard-details-menu-with-handle {
|
||||
float: right;
|
||||
font-size: 18px;
|
||||
padding-right: 30px;
|
||||
padding-left: 5px;
|
||||
position: absolute;
|
||||
right: 0.7vw;
|
||||
top: 0.7vh;
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
padding: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.minicard-details-menu {
|
||||
float: right;
|
||||
font-size: 18px;
|
||||
padding-left: 5px;
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
padding-left: 0.7vw;
|
||||
}
|
||||
@media print {
|
||||
.minicard-details-menu,
|
||||
@@ -63,7 +65,7 @@
|
||||
.minicard.linked-board .linked-icon,
|
||||
.minicard.linked-card .linked-icon {
|
||||
display: inline-block;
|
||||
margin-right: 11px;
|
||||
margin-right: 1.5vw;
|
||||
vertical-align: baseline;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -72,11 +74,11 @@
|
||||
color: #937760;
|
||||
}
|
||||
.is-selected .minicard {
|
||||
transform: translateX(11px);
|
||||
transform: translateX(1.5vw);
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
z-index: 25;
|
||||
box-shadow: -2px 1px 2px rgba(0,0,0,0.2);
|
||||
box-shadow: -0.3vw 0.2vh 0.3vh rgba(0,0,0,0.2);
|
||||
}
|
||||
.minicard:hover:not(.minicard-composer),
|
||||
.is-selected .minicard,
|
||||
@@ -90,20 +92,20 @@
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
height: 145px;
|
||||
height: 20vh;
|
||||
user-select: none;
|
||||
margin: 6px -8px 6px -8px;
|
||||
border-radius: top 2px;
|
||||
margin: 0.8vh -1vw 0.8vh -1vw;
|
||||
border-radius: top 0.3vw;
|
||||
}
|
||||
.minicard .minicard-labels {
|
||||
float: none;
|
||||
}
|
||||
.minicard .minicard-labels .minicard-label {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 2px;
|
||||
margin-right: 3px;
|
||||
margin-bottom: 3px;
|
||||
width: clamp(12px, 1.5vw, 16px);
|
||||
height: clamp(12px, 1.5vw, 16px);
|
||||
border-radius: 0.3vw;
|
||||
margin-right: 0.4vw;
|
||||
margin-bottom: 0.4vh;
|
||||
}
|
||||
.minicard .minicard-labels-no-text {
|
||||
display: flex;
|
||||
@@ -119,37 +121,41 @@
|
||||
flex-grow: 1;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
max-width: 100px;
|
||||
margin-right: 4px;
|
||||
max-width: 13vw;
|
||||
margin-right: 0.5vw;
|
||||
}
|
||||
.minicard .minicard-custom-field-item-fullwidth {
|
||||
flex-grow: 1;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
max-width: 100%;
|
||||
margin-right: 4px;
|
||||
margin-right: 0.5vw;
|
||||
}
|
||||
.minicard .handle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: clamp(20px, 2.5vw, 28px);
|
||||
height: clamp(20px, 2.5vw, 28px);
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
right: 3vw;
|
||||
top: 0.7vh;
|
||||
display: none;
|
||||
z-index: 10;
|
||||
}
|
||||
@media only screen {
|
||||
.minicard .handle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.minicard .handle .fa-arrows {
|
||||
font-size: 20px;
|
||||
.minicard .handle .drag-handle {
|
||||
font-size: clamp(16px, 3vw, 20px);
|
||||
color: #ccc;
|
||||
display: inline-block;
|
||||
width: 1.4em;
|
||||
text-align: center;
|
||||
}
|
||||
.minicard .minicard-title .card-number {
|
||||
color: #b3b3b3;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
margin-right: 0.7vw;
|
||||
}
|
||||
@media only screen {
|
||||
.minicard .minicard-title p:last-child {
|
||||
@@ -166,11 +172,140 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.minicard .date {
|
||||
margin-right: 3px;
|
||||
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: 7px;
|
||||
margin-top: 1vh;
|
||||
color: #808080;
|
||||
}
|
||||
.minicard .badges:empty {
|
||||
@@ -178,14 +313,14 @@
|
||||
}
|
||||
.minicard .badges .badge {
|
||||
float: left;
|
||||
margin-right: 11px;
|
||||
margin-bottom: 3px;
|
||||
margin-right: 1.5vw;
|
||||
margin-bottom: 0.4vh;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.minicard .badges .badge.is-finished {
|
||||
background: #3cb500;
|
||||
padding: 0px 3px;
|
||||
border-radius: 3px;
|
||||
padding: 0 0.4vw;
|
||||
border-radius: 0.4vw;
|
||||
color: #fff;
|
||||
}
|
||||
.minicard .badges .badge:last-of-type {
|
||||
@@ -201,41 +336,41 @@
|
||||
}
|
||||
.minicard .badges .badge .badge-text {
|
||||
font-size: 0.9em;
|
||||
padding-left: 2px;
|
||||
line-height: 14px;
|
||||
padding-left: 0.3vw;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.minicard .badges .badge .check-list-text {
|
||||
padding-left: 0px;
|
||||
line-height: 12px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.minicard .minicard-members,
|
||||
.minicard .minicard-assignees,
|
||||
.minicard .minicard-creator {
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
margin-bottom: 4px;
|
||||
margin-left: 0.7vw;
|
||||
margin-bottom: 0.5vh;
|
||||
}
|
||||
.minicard .minicard-members .member,
|
||||
.minicard .minicard-assignees .member,
|
||||
.minicard .minicard-creator .member {
|
||||
float: right;
|
||||
border-radius: 50%;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
margin-bottom: 4px;
|
||||
height: clamp(24px, 3.5vw, 32px);
|
||||
width: clamp(24px, 3.5vw, 32px);
|
||||
margin-bottom: 0.5vh;
|
||||
}
|
||||
.minicard .minicard-members .assignee,
|
||||
.minicard .minicard-assignees .assignee,
|
||||
.minicard .minicard-creator .assignee {
|
||||
float: right;
|
||||
border-radius: 50%;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
height: clamp(24px, 3.5vw, 32px);
|
||||
width: clamp(24px, 3.5vw, 32px);
|
||||
}
|
||||
.minicard .minicard-members + .badges,
|
||||
.minicard .minicard-assignees + .badges,
|
||||
.minicard .minicard-creator + .badges {
|
||||
margin-top: 5px;
|
||||
margin-top: 0.7vh;
|
||||
}
|
||||
.minicard .minicard-assignees {
|
||||
border-bottom: 1px solid #f00;
|
||||
@@ -248,13 +383,13 @@
|
||||
display: none;
|
||||
}
|
||||
.minicard .minicard-description {
|
||||
padding: 6px 0 0 8px;
|
||||
padding: 0.8vh 0 0 1vw;
|
||||
color: #000;
|
||||
background-color: #eee;
|
||||
width: 100%;
|
||||
margin-bottom: 2px;
|
||||
margin-left: -4px;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 0.3vh;
|
||||
margin-left: -0.5vw;
|
||||
border-radius: 0.4vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
@@ -262,7 +397,7 @@
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.minicard.minicard-composer {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 1.3vh;
|
||||
}
|
||||
.minicard.minicard-composer textarea.minicard-composer-textarea,
|
||||
.minicard.minicard-composer textarea.minicard-composer-textarea:focus {
|
||||
@@ -273,9 +408,9 @@
|
||||
height: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: 162px;
|
||||
min-height: 36px;
|
||||
margin-bottom: 20px;
|
||||
max-height: 22vh;
|
||||
min-height: 5vh;
|
||||
margin-bottom: 2.5vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.parent-prefix {
|
||||
@@ -580,3 +715,99 @@
|
||||
.text-green {
|
||||
color: #008000;
|
||||
}
|
||||
|
||||
/* Drag and drop file upload visual feedback */
|
||||
.minicard.is-dragging-over {
|
||||
border: 2px dashed #0079bf;
|
||||
background-color: #e3f2fd !important;
|
||||
transform: scale(1.02);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* List name display on minicard */
|
||||
.minicard-list-name {
|
||||
font-size: 0.75em;
|
||||
color: #8c8c8c;
|
||||
margin-top: 0.2vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3vw;
|
||||
}
|
||||
|
||||
/* 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,20 +3,16 @@ 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'}}")
|
||||
| ↕️
|
||||
if canModifyCard
|
||||
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") ☰
|
||||
.dates
|
||||
if getReceived
|
||||
unless getStart
|
||||
unless getDue
|
||||
unless getEnd
|
||||
.date
|
||||
+minicardReceivedDate
|
||||
.date
|
||||
+minicardReceivedDate
|
||||
if getStart
|
||||
.date
|
||||
+minicardStartDate
|
||||
@@ -31,6 +27,27 @@ template(name="minicard")
|
||||
if cover
|
||||
if currentBoard.allowsCoverAttachmentOnMinicard
|
||||
.minicard-cover(style="background-image: url('{{cover.link 'original'}}?dummyReloadAfterSessionEstablished={{sess}}');")
|
||||
|
||||
// Upload progress indicator for drag-and-drop uploads
|
||||
if hasActiveUploads
|
||||
.minicard-upload-progress
|
||||
.upload-progress-header
|
||||
| 📤
|
||||
span {{_ 'uploading-files'}} ({{uploadCount}})
|
||||
each uploads
|
||||
.upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
|
||||
.upload-progress-filename {{file.name}}
|
||||
.upload-progress-bar
|
||||
.upload-progress-fill(style="width: {{progress}}%")
|
||||
if $eq status 'error'
|
||||
.upload-progress-error
|
||||
| ⚠️
|
||||
span {{_ 'upload-failed'}}
|
||||
else if $eq status 'completed'
|
||||
.upload-progress-success
|
||||
| ✅
|
||||
span {{_ 'upload-completed'}}
|
||||
|
||||
.minicard-title
|
||||
if $eq 'prefix-with-full-path' currentBoard.presentParentTask
|
||||
.parent-prefix
|
||||
@@ -40,23 +57,17 @@ template(name="minicard")
|
||||
| {{ parentCardName }}
|
||||
if isLinkedBoard
|
||||
a.js-linked-link
|
||||
span.linked-icon.fa.fa-folder
|
||||
span.linked-icon | 📁
|
||||
else if isLinkedCard
|
||||
a.js-linked-link
|
||||
span.linked-icon.fa.fa-id-card
|
||||
span.linked-icon | 🃏
|
||||
if getArchived
|
||||
span.linked-icon.linked-archived.fa.fa-archive
|
||||
span.linked-icon.linked-archived | 📦
|
||||
+viewer
|
||||
if currentBoard.allowsCardNumber
|
||||
span.card-number
|
||||
| ##{getCardNumber}
|
||||
= getTitle
|
||||
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentString ' > ' }}
|
||||
if $eq 'subtext-with-parent' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentCardName }}
|
||||
if labels
|
||||
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
|
||||
each labels
|
||||
@@ -132,7 +143,7 @@ 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 💬
|
||||
= ' '
|
||||
= comments.length
|
||||
//span.badge-comment.badge-text
|
||||
@@ -140,88 +151,63 @@ template(name="minicard")
|
||||
if getDescription
|
||||
unless currentBoard.allowsDescriptionTextOnMinicard
|
||||
.badge.badge-state-image-only(title=getDescription)
|
||||
span.badge-icon.fa.fa-align-left
|
||||
span.badge-icon 📝
|
||||
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}}") 👍
|
||||
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}}") 👎
|
||||
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}}") ✅
|
||||
if expiredPoker
|
||||
span.badge-text {{ getPokerEstimation }}
|
||||
if attachments.length
|
||||
if currentBoard.allowsBadgeAttachmentOnMinicard
|
||||
.badge
|
||||
span.badge-icon.fa.fa-paperclip
|
||||
span.badge-icon 📎
|
||||
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-icon 🌐
|
||||
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
|
||||
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
|
||||
if currentBoard.allowsCardSortingByNumber
|
||||
if currentBoard.allowsCardSortingByNumberOnMinicard
|
||||
.badge
|
||||
span.badge-icon.fa.fa-sort
|
||||
span.badge-icon 🔢
|
||||
span.badge-text.check-list-sort {{ sort }}
|
||||
if shouldShowChecklistAtMinicard
|
||||
each shouldShowChecklistAtMinicard
|
||||
+minicardChecklist(checklist=. card=..)
|
||||
if currentBoard.allowsDescriptionTextOnMinicard
|
||||
if getDescription
|
||||
.minicard-description
|
||||
+viewer
|
||||
| {{ getDescription }}
|
||||
if shouldShowListOnMinicard
|
||||
.minicard-list-name
|
||||
| 📋
|
||||
| {{ listName }}
|
||||
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentString ' > ' }}
|
||||
if $eq 'subtext-with-parent' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
| {{ parentCardName }}
|
||||
|
||||
template(name="editCardSortOrderPopup")
|
||||
input.js-edit-card-sort-popup(type='text' autofocus value=sort dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-sort-popup(type="submit") {{_ 'save'}}
|
||||
|
||||
template(name="minicardDetailsActionsPopup")
|
||||
ul.pop-over-list
|
||||
if canModifyCard
|
||||
li
|
||||
a.js-move-card
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
li
|
||||
a.js-copy-card
|
||||
i.fa.fa-copy
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
hr
|
||||
li
|
||||
a.js-archive
|
||||
i.fa.fa-arrow-right
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archive-card'}}
|
||||
hr
|
||||
li
|
||||
a.js-move-card-to-top
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
li
|
||||
a.js-add-labels
|
||||
i.fa.fa-tags
|
||||
| {{_ 'card-edit-labels'}}
|
||||
li
|
||||
a.js-due-date
|
||||
i.fa.fa-sign-in
|
||||
| {{_ 'editCardDueDatePopup-title'}}
|
||||
li
|
||||
a.js-set-card-color
|
||||
i.fa.fa-paint-brush
|
||||
| {{_ 'setCardColorPopup-title'}}
|
||||
li
|
||||
a.js-link
|
||||
i.fa.fa-link
|
||||
| {{_ 'link-card'}}
|
||||
template(name="minicardChecklist")
|
||||
.minicard-checklist
|
||||
.checklist-header
|
||||
.checklist-title= checklist.title
|
||||
if canModifyCard
|
||||
a.checklist-menu.js-open-checklist-menu(title="{{_ 'checklistActionsPopup-title'}}") ☰
|
||||
each visibleItems
|
||||
+checklistItemDetail(item = . checklist = checklist card = card)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { CustomFieldStringTemplate } from '/client/lib/customFields'
|
||||
import { CustomFieldStringTemplate } from '/client/lib/customFields';
|
||||
import { handleFileUpload } from './attachments';
|
||||
import uploadProgressManager from '../../lib/uploadProgressManager';
|
||||
|
||||
// Template.cards.events({
|
||||
// 'click .member': Popup.open('cardMember')
|
||||
@@ -46,6 +48,10 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
isWatching() {
|
||||
const card = this.currentData();
|
||||
return card.findWatcher(Meteor.userId());
|
||||
},
|
||||
|
||||
showMembers() {
|
||||
// cache "board" to reduce the mini-mongodb access
|
||||
@@ -85,6 +91,13 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
},
|
||||
|
||||
toggleChecklistItem() {
|
||||
const item = this.currentData();
|
||||
if (item && item._id) {
|
||||
item.toggleItem();
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
@@ -102,12 +115,105 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
'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'),
|
||||
'click .js-open-minicard-details-menu': Popup.open('cardDetailsActions'),
|
||||
// Drag and drop file upload handlers
|
||||
'dragover .minicard'(event) {
|
||||
// Only prevent default for file drags to avoid interfering with sortable
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
'dragenter .minicard'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const card = this.data();
|
||||
const board = card.board();
|
||||
// Only allow drag-and-drop if user can modify card and board allows attachments
|
||||
if (Utils.canModifyCard() && board && board.allowsAttachments) {
|
||||
$(event.currentTarget).addClass('is-dragging-over');
|
||||
}
|
||||
}
|
||||
},
|
||||
'dragleave .minicard'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
}
|
||||
},
|
||||
'drop .minicard'(event) {
|
||||
const dataTransfer = event.originalEvent.dataTransfer;
|
||||
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$(event.currentTarget).removeClass('is-dragging-over');
|
||||
|
||||
const card = this.data();
|
||||
const board = card.board();
|
||||
|
||||
// Check permissions
|
||||
if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a file drop (not a card reorder)
|
||||
if (!dataTransfer.files || dataTransfer.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileUpload(card, files);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
];
|
||||
},
|
||||
}).register('minicard');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'minicardChecklist';
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-open-checklist-menu'(event) {
|
||||
const data = this.currentData();
|
||||
const checklist = data.checklist || data;
|
||||
const card = data.card || this.data();
|
||||
const context = { currentData: () => ({ checklist, card }) };
|
||||
Popup.open('checklistActions').call(context, event);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
visibleItems() {
|
||||
const checklist = this.currentData().checklist || this.currentData();
|
||||
const items = checklist.items();
|
||||
|
||||
return items.filter(item => {
|
||||
// Hide finished items if hideCheckedChecklistItems is true
|
||||
if (item.isFinished && checklist.hideCheckedChecklistItems) {
|
||||
return false;
|
||||
}
|
||||
// Hide all items if hideAllChecklistItems is true
|
||||
if (checklist.hideAllChecklistItems) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
}).register('minicardChecklist');
|
||||
|
||||
Template.minicard.helpers({
|
||||
hiddenMinicardLabelText() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
@@ -125,6 +231,52 @@ Template.minicard.helpers({
|
||||
? Meteor.connection._lastSessionId
|
||||
: null;
|
||||
},
|
||||
isWatching() {
|
||||
return this.findWatcher(Meteor.userId());
|
||||
},
|
||||
// Upload progress helpers
|
||||
hasActiveUploads() {
|
||||
return uploadProgressManager.hasActiveUploads(this._id);
|
||||
},
|
||||
uploads() {
|
||||
return uploadProgressManager.getUploadsForCard(this._id);
|
||||
},
|
||||
uploadCount() {
|
||||
return uploadProgressManager.getUploadCountForCard(this._id);
|
||||
},
|
||||
listName() {
|
||||
const list = this.list();
|
||||
return list ? list.title : '';
|
||||
},
|
||||
|
||||
shouldShowListOnMinicard() {
|
||||
// Show list name if either:
|
||||
// 1. Board-wide setting is enabled, OR
|
||||
// 2. This specific card has the setting enabled
|
||||
const currentBoard = this.board();
|
||||
if (!currentBoard) return false;
|
||||
return currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
|
||||
},
|
||||
|
||||
shouldShowChecklistAtMinicard() {
|
||||
// Return checklists that should be shown on minicard
|
||||
const currentBoard = this.board();
|
||||
if (!currentBoard) return [];
|
||||
|
||||
const checklists = this.checklists();
|
||||
const visibleChecklists = [];
|
||||
|
||||
checklists.forEach(checklist => {
|
||||
// Show checklist if either:
|
||||
// 1. Board-wide setting is enabled, OR
|
||||
// 2. This specific checklist has the setting enabled
|
||||
if (currentBoard.allowsChecklistAtMinicard || checklist.showChecklistAtMinicard) {
|
||||
visibleChecklists.push(checklist);
|
||||
}
|
||||
});
|
||||
|
||||
return visibleChecklists;
|
||||
}
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
@@ -154,7 +306,7 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
}).register('editCardSortOrderPopup');
|
||||
|
||||
Template.minicardDetailsActionsPopup.events({
|
||||
Template.cardDetailsActionsPopup.events({
|
||||
'click .js-due-date': Popup.open('editCardDueDate'),
|
||||
'click .js-move-card': Popup.open('moveCard'),
|
||||
'click .js-copy-card': Popup.open('copyCard'),
|
||||
@@ -178,4 +330,11 @@ Template.minicardDetailsActionsPopup.events({
|
||||
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();
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -11,9 +11,9 @@ 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
|
||||
= ' '
|
||||
| {{_ 'context-separator'}}
|
||||
@@ -25,9 +25,9 @@ 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
|
||||
= ' '
|
||||
| {{_ '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
|
||||
| 📦
|
||||
@@ -87,6 +87,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 +136,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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
template(name="subtasks")
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sitemap
|
||||
| 🌐
|
||||
| {{_ 'subtasks'}}
|
||||
if currentUser.isBoardAdmin
|
||||
if toggleDeleteDialog.get
|
||||
@@ -16,7 +16,7 @@ template(name="subtasks")
|
||||
+addSubtaskItemForm
|
||||
else
|
||||
a.js-open-inlined-form(title="{{_ 'add-subtask'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
template(name="subtaskDetail")
|
||||
.js-subtasks.subtask
|
||||
@@ -26,7 +26,7 @@ template(name="subtaskDetail")
|
||||
.subtask-title
|
||||
span
|
||||
if canModifyCard
|
||||
a.fa.fa-navicon.subtask-details-menu.js-open-subtask-details-menu(title="{{_ 'subtaskActionsPopup-title'}}")
|
||||
a.subtask-details-menu.js-open-subtask-details-menu(title="{{_ 'subtaskActionsPopup-title'}}")
|
||||
if canModifyCard
|
||||
h2.title.js-open-inlined-form.is-editable
|
||||
+viewer
|
||||
@@ -40,7 +40,7 @@ template(name="addSubtaskItemForm")
|
||||
textarea.js-add-subtask-item(rows='1' autofocus dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-add-subtask-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
a.js-close-inlined-form
|
||||
|
||||
template(name="editSubtaskItemForm")
|
||||
textarea.js-edit-subtask-item(rows='1' autofocus dir="auto")
|
||||
@@ -50,7 +50,7 @@ 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
|
||||
a.js-close-inlined-form
|
||||
span(title=createdAt) {{ moment createdAt }}
|
||||
if canModifyCard
|
||||
if currentUser.isBoardAdmin
|
||||
@@ -68,18 +68,18 @@ template(name="subtasksItems")
|
||||
+addSubtaskItemForm
|
||||
else
|
||||
a.add-subtask-item.js-open-inlined-form
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
| {{_ 'add-subtask-item'}}...
|
||||
|
||||
template(name='subtaskItemDetail')
|
||||
.js-subtasks-item.subtasks-item
|
||||
if canModifyCard
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/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 {{#if item.isFinished }}✅{{else}}⬜{{/if}}
|
||||
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
@@ -92,10 +92,10 @@ template(name="subtaskActionsPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-view-subtask(title="{{ subtask.title }}")
|
||||
i.fa.fa-eye
|
||||
| 👁️
|
||||
| {{_ "view-it"}}
|
||||
if currentUser.isBoardAdmin
|
||||
a.js-delete-subtask.delete-subtask
|
||||
i.fa.fa-trash
|
||||
| 🗑️
|
||||
| {{_ "delete"}} ...
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
addSubtask(event) {
|
||||
@@ -104,7 +105,19 @@ BlazeComponent.extendComponent({
|
||||
}).register('subtasks');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
// ...
|
||||
toggleItem() {
|
||||
const item = this.currentData().item;
|
||||
if (item && item._id) {
|
||||
item.toggleItem();
|
||||
}
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-subtasks-item .check-box-unicode': this.toggleItem,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('subtaskItemDetail');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
|
||||
@@ -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,27 @@
|
||||
<template name="originalPosition">
|
||||
<div class="original-position-info">
|
||||
{{#if isLoading}}
|
||||
<div class="original-position-loading">
|
||||
⏳ Loading original position...
|
||||
</div>
|
||||
{{else if showOriginalPosition}}
|
||||
<div class="original-position-details">
|
||||
{{#if hasMovedFromOriginal}}
|
||||
<div class="original-position-moved">
|
||||
<span class="original-position-text">ℹ️ {{getOriginalPositionDescription}}</span>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="original-position-unchanged">
|
||||
<span class="original-position-text">✅ In original position</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if getOriginalTitle}}
|
||||
<div class="original-title">
|
||||
<strong>Original title:</strong> {{getOriginalTitle}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,98 @@
|
||||
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import './originalPosition.html';
|
||||
|
||||
/**
|
||||
* Component to display original position information for swimlanes, lists, and cards
|
||||
*/
|
||||
class OriginalPositionComponent extends BlazeComponent {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
this.originalPosition = new ReactiveVar(null);
|
||||
this.isLoading = new ReactiveVar(false);
|
||||
this.hasMoved = new ReactiveVar(false);
|
||||
|
||||
this.autorun(() => {
|
||||
const data = this.data();
|
||||
if (data && data.entityId && data.entityType) {
|
||||
this.loadOriginalPosition(data.entityId, data.entityType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadOriginalPosition(entityId, entityType) {
|
||||
this.isLoading.set(true);
|
||||
|
||||
const methodName = `positionHistory.get${entityType.charAt(0).toUpperCase() + entityType.slice(1)}OriginalPosition`;
|
||||
|
||||
Meteor.call(methodName, entityId, (error, result) => {
|
||||
this.isLoading.set(false);
|
||||
if (error) {
|
||||
console.error('Error loading original position:', error);
|
||||
this.originalPosition.set(null);
|
||||
} else {
|
||||
this.originalPosition.set(result);
|
||||
|
||||
// Check if the entity has moved
|
||||
const movedMethodName = `positionHistory.has${entityType.charAt(0).toUpperCase() + entityType.slice(1)}Moved`;
|
||||
Meteor.call(movedMethodName, entityId, (movedError, movedResult) => {
|
||||
if (!movedError) {
|
||||
this.hasMoved.set(movedResult);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getOriginalPosition() {
|
||||
return this.originalPosition.get();
|
||||
}
|
||||
|
||||
isLoading() {
|
||||
return this.isLoading.get();
|
||||
}
|
||||
|
||||
hasMovedFromOriginal() {
|
||||
return this.hasMoved.get();
|
||||
}
|
||||
|
||||
getOriginalPositionDescription() {
|
||||
const position = this.getOriginalPosition();
|
||||
if (!position) return 'No original position data';
|
||||
|
||||
if (position.originalPosition) {
|
||||
const entityType = this.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 = this.getOriginalPosition();
|
||||
return position ? position.originalTitle : '';
|
||||
}
|
||||
|
||||
showOriginalPosition() {
|
||||
return this.getOriginalPosition() !== null;
|
||||
}
|
||||
}
|
||||
|
||||
OriginalPositionComponent.register('originalPosition');
|
||||
|
||||
export default OriginalPositionComponent;
|
||||
@@ -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'}}
|
||||
|
||||
@@ -5,11 +5,11 @@ button {
|
||||
box-sizing: border-box;
|
||||
background-color: #ebebeb;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
border-radius: 0.4vw;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
min-height: 34px;
|
||||
padding: 7px;
|
||||
margin-bottom: 1.5vh;
|
||||
min-height: 4.5vh;
|
||||
padding: 1vh 1vw;
|
||||
}
|
||||
select.full,
|
||||
textarea.full,
|
||||
@@ -31,17 +31,18 @@ button:focus {
|
||||
outline: 0;
|
||||
}
|
||||
input[type="file"] {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 2vh;
|
||||
}
|
||||
input[type="radio"] {
|
||||
-webkit-appearance: radio;
|
||||
appearance: radio;
|
||||
min-height: inherit;
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="email"] {
|
||||
transition: background 85ms ease-in, border-color 85ms ease-in;
|
||||
width: 250px;
|
||||
width: min(250px, 30vw);
|
||||
}
|
||||
input[type="text"].inline-input,
|
||||
input[type="password"].inline-input,
|
||||
@@ -49,10 +50,10 @@ input[type="email"].inline-input {
|
||||
background: none;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 2px;
|
||||
padding: 0.3vh;
|
||||
min-height: 0;
|
||||
height: 18px;
|
||||
width: 200px;
|
||||
height: 2.5vh;
|
||||
width: min(200px, 25vw);
|
||||
}
|
||||
input[type="text"].full-line,
|
||||
input[type="password"].full-line,
|
||||
@@ -96,30 +97,31 @@ input[type="email"]:disabled,
|
||||
textarea:disabled {
|
||||
background-color: #dcdcdc;
|
||||
border-color: #bfbfbf;
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
select {
|
||||
max-height: 300px;
|
||||
width: 256px;
|
||||
margin-bottom: 8px;
|
||||
max-height: 40vh;
|
||||
width: min(256px, 32vw);
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
select.inline {
|
||||
width: 100%;
|
||||
}
|
||||
option[disabled] {
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
}
|
||||
textarea {
|
||||
height: 150px;
|
||||
height: 20vh;
|
||||
transition: background 85ms ease-in, border-color 85ms ease-in;
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
}
|
||||
textarea.editor {
|
||||
resize: none;
|
||||
padding-bottom: 22px;
|
||||
padding-bottom: 3vh;
|
||||
}
|
||||
.button {
|
||||
border-radius: 3px;
|
||||
@@ -134,14 +136,14 @@ button {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
padding: 7px 20px;
|
||||
line-height: 1.3;
|
||||
padding: 1vh 2.5vw;
|
||||
text-align: center;
|
||||
}
|
||||
input[type="submit"] .wide,
|
||||
button .wide {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
padding-left: 4vw;
|
||||
padding-right: 4vw;
|
||||
}
|
||||
input[type="submit"]:hover,
|
||||
button:hover,
|
||||
@@ -222,8 +224,8 @@ button.disabled:active,
|
||||
}
|
||||
fieldset {
|
||||
border: 1px solid #bfbfbf;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
padding: 2vh 2vw;
|
||||
margin-bottom: 2vh;
|
||||
}
|
||||
input[type="hidden"] {
|
||||
display: none;
|
||||
@@ -231,18 +233,18 @@ input[type="hidden"] {
|
||||
.radio-div,
|
||||
.check-div {
|
||||
display: block;
|
||||
margin: 0 0 4px 20px;
|
||||
min-height: 20px;
|
||||
margin: 0 0 0.5vh 2.5vw;
|
||||
min-height: 2.5vh;
|
||||
position: relative;
|
||||
}
|
||||
.radio-div input,
|
||||
.check-div input {
|
||||
left: -18px;
|
||||
left: -2.5vw;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
top: 0.3vh;
|
||||
}
|
||||
.radio-div label,
|
||||
.check-div label {
|
||||
@@ -251,23 +253,23 @@ input[type="hidden"] {
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 0.5vh;
|
||||
}
|
||||
label.form-error {
|
||||
color: #ba1212;
|
||||
color: #d32f2f;
|
||||
}
|
||||
input::-webkit-input-placeholder,
|
||||
textarea::-webkit-input-placeholder,
|
||||
input::-moz-placeholder,
|
||||
textarea::-moz-placeholder {
|
||||
color: #8c8c8c;
|
||||
color: #333 !important;
|
||||
}
|
||||
.edit-controls,
|
||||
.add-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 1.5vh;
|
||||
}
|
||||
@media print {
|
||||
.add-controls {
|
||||
@@ -279,13 +281,13 @@ textarea::-moz-placeholder {
|
||||
.edit-controls input[type=button],
|
||||
.add-controls input[type=button] {
|
||||
float: left;
|
||||
height: 32px;
|
||||
height: 4.5vh;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.edit-controls .fa-times-thin,
|
||||
.add-controls .fa-times-thin {
|
||||
font-size: 26px;
|
||||
margin: 3px 10px;
|
||||
font-size: clamp(20px, 4vw, 26px);
|
||||
margin: 0.5vh 1.5vw;
|
||||
}
|
||||
[type="checkbox"]:not(:checked),
|
||||
[type="checkbox"]:checked {
|
||||
@@ -313,15 +315,24 @@ 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);
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
border: 1px solid #e3e3e3;
|
||||
border-bottom-color: #c2c2c2;
|
||||
@@ -374,14 +385,14 @@ textarea::-moz-placeholder {
|
||||
.button-link.setting.disabled {
|
||||
background: #fff;
|
||||
border-color: #e9e9e9;
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
cursor: default;
|
||||
}
|
||||
.button-link.setting.disabled select {
|
||||
display: none;
|
||||
}
|
||||
.button-link.setting.disabled:hover .label {
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
}
|
||||
.button-link.setting.disabled,
|
||||
.button-link.setting.disabled:hover,
|
||||
@@ -397,7 +408,7 @@ textarea::-moz-placeholder {
|
||||
color: #a8a8a8;
|
||||
}
|
||||
.button-link.setting .label {
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
@@ -505,8 +516,9 @@ button.loud-text-button:hover {
|
||||
.emphasis-button,
|
||||
.quiet-button {
|
||||
border-radius: 3px;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
display: block;
|
||||
margin: 2px 0;
|
||||
padding: 6px 8px;
|
||||
@@ -571,7 +583,7 @@ button.loud-text-button:hover {
|
||||
top: 6px;
|
||||
}
|
||||
.big-link.none {
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
text-decoration: none;
|
||||
}
|
||||
.big-link.none:hover {
|
||||
@@ -601,7 +613,7 @@ button.loud-text-button:hover {
|
||||
}
|
||||
.show-more {
|
||||
border-radius: 3px;
|
||||
color: #8c8c8c;
|
||||
color: #222;
|
||||
display: block;
|
||||
padding: 16px 8px 16px 40px;
|
||||
margin: 8px 0;
|
||||
|
||||
@@ -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,219 @@
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
|
||||
// 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';
|
||||
|
||||
// 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,45 @@
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
// Provide the expected parent component properties for cardDetails
|
||||
this.showOverlay = new ReactiveVar(false);
|
||||
this.mouseHasEnterCardDetails = false;
|
||||
},
|
||||
|
||||
selectedCard() {
|
||||
// The selected card is now passed as a parameter to the component
|
||||
return this.currentData();
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-close-card-details'(event) {
|
||||
event.preventDefault();
|
||||
// Find the ganttView template instance and clear selectedCardId
|
||||
let view = Blaze.currentView;
|
||||
while (view) {
|
||||
if (view.templateInstance && view.templateInstance().selectedCardId) {
|
||||
view.templateInstance().selectedCardId.set(null);
|
||||
break;
|
||||
}
|
||||
view = view.parentView;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('ganttCard');
|
||||
|
||||
// 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);
|
||||
}
|
||||
},
|
||||
});
|
||||
Vendored
+8
-8
@@ -5,17 +5,17 @@
|
||||
.map-members .mapping-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -4px;
|
||||
margin: 0 -0.5vw;
|
||||
}
|
||||
.map-members .mapping-list .mapping-item {
|
||||
max-width: 300px;
|
||||
min-width: 200px;
|
||||
padding: 6px;
|
||||
margin: 5px;
|
||||
max-width: min(300px, 40vw);
|
||||
min-width: min(200px, 26vw);
|
||||
padding: 0.8vh 0.8vw;
|
||||
margin: 0.7vh;
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
|
||||
border-radius: 0.4vw;
|
||||
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.15);
|
||||
}
|
||||
.map-members .mapping-list .mapping-item:hover {
|
||||
background: #f2f2f2;
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
.map-members .mapping-list .wekan {
|
||||
display: inline-block;
|
||||
width: 35px;
|
||||
width: 4.5vw;
|
||||
}
|
||||
.map-members .mapping-list .wekan .member {
|
||||
float: none;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
template(name="importHeaderBar")
|
||||
h1
|
||||
a.back-btn(href="{{pathFor 'home'}}")
|
||||
i.fa.fa-chevron-left
|
||||
| ⬅️
|
||||
| {{_ title}}
|
||||
|
||||
template(name="import")
|
||||
@@ -36,7 +36,7 @@ template(name="importMapMembers")
|
||||
+userAvatar(userId=wekanId)
|
||||
else
|
||||
a.member.add-member
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
//-
|
||||
Due to the way the flewbox layout is working, we need to set some
|
||||
invisible items so that the last row items have a consistent width.
|
||||
@@ -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'}}
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
|
||||
@@ -311,6 +312,73 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
}).register('importMapMembersAddPopup');
|
||||
|
||||
// Global reactive variables for import member popup
|
||||
const importMemberPopupState = {
|
||||
searching: new ReactiveVar(false),
|
||||
searchResults: new ReactiveVar([]),
|
||||
noResults: new ReactiveVar(false),
|
||||
searchTimeout: null
|
||||
};
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
// Use global state
|
||||
this.searching = importMemberPopupState.searching;
|
||||
this.searchResults = importMemberPopupState.searchResults;
|
||||
this.noResults = importMemberPopupState.noResults;
|
||||
this.searchTimeout = importMemberPopupState.searchTimeout;
|
||||
},
|
||||
|
||||
onRendered() {
|
||||
this.find('.js-search-member-input').focus();
|
||||
},
|
||||
|
||||
performSearch(query) {
|
||||
if (!query || query.length < 2) {
|
||||
this.searchResults.set([]);
|
||||
this.noResults.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.searching.set(true);
|
||||
this.noResults.set(false);
|
||||
|
||||
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
|
||||
this.searchResults.set(results);
|
||||
this.searching.set(false);
|
||||
|
||||
if (results.length === 0) {
|
||||
this.noResults.set(true);
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'keyup .js-search-member-input'(event) {
|
||||
const query = event.target.value.trim();
|
||||
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.performSearch(query);
|
||||
}, 300);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('importMapMembersAddPopupSearch');
|
||||
|
||||
Template.importMapMembersAddPopup.helpers({
|
||||
searchIndex: () => UserSearchIndex,
|
||||
searchResults() {
|
||||
return importMemberPopupState.searchResults.get();
|
||||
},
|
||||
searching() {
|
||||
return importMemberPopupState.searching;
|
||||
},
|
||||
noResults() {
|
||||
return importMemberPopupState.noResults;
|
||||
}
|
||||
})
|
||||
@@ -8,8 +8,216 @@
|
||||
padding: 0;
|
||||
float: left;
|
||||
}
|
||||
|
||||
/* List resize handle */
|
||||
.list-resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -3px;
|
||||
width: 6px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
z-index: 10;
|
||||
background: transparent;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 2px;
|
||||
/* Ensure the handle is clickable */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.list-resize-handle:hover {
|
||||
background: rgba(0, 123, 255, 0.4);
|
||||
box-shadow: 0 0 4px rgba(0, 123, 255, 0.3);
|
||||
}
|
||||
|
||||
.list-resize-handle:active {
|
||||
background: rgba(0, 123, 255, 0.6);
|
||||
box-shadow: 0 0 6px rgba(0, 123, 255, 0.4);
|
||||
}
|
||||
|
||||
/* Show resize handle only on hover */
|
||||
.list:hover .list-resize-handle {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.list:hover .list-resize-handle:hover {
|
||||
background: rgba(0, 123, 255, 0.4);
|
||||
box-shadow: 0 0 4px rgba(0, 123, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Add a subtle indicator line */
|
||||
.list-resize-handle::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 1px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.list-resize-handle:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Disable resize handle for collapsed lists and mobile view */
|
||||
.list.list-collapsed .list-resize-handle,
|
||||
.list.mobile-view .list-resize-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Disable resize handle for auto-width lists */
|
||||
.list.list-auto-width .list-resize-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Visual feedback during resize */
|
||||
.list.list-resizing {
|
||||
transition: none !important;
|
||||
box-shadow: 0 0 10px rgba(0, 123, 255, 0.3);
|
||||
/* Ensure the list maintains its new width during resize */
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
/* Override any conflicting layout properties */
|
||||
float: left !important;
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
/* Force width to be respected */
|
||||
width: var(--list-width, auto) !important;
|
||||
min-width: var(--list-width, auto) !important;
|
||||
max-width: var(--list-width, auto) !important;
|
||||
/* Ensure the width is applied immediately */
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
body.list-resizing-active {
|
||||
cursor: col-resize !important;
|
||||
}
|
||||
|
||||
body.list-resizing-active * {
|
||||
cursor: col-resize !important;
|
||||
}
|
||||
|
||||
/* Ensure swimlane container doesn't interfere with list resizing */
|
||||
.swimlane .list.list-resizing {
|
||||
/* Override any swimlane flex properties */
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
/* Ensure width is respected */
|
||||
width: var(--list-width, auto) !important;
|
||||
min-width: var(--list-width, auto) !important;
|
||||
max-width: var(--list-width, auto) !important;
|
||||
}
|
||||
|
||||
/* More aggressive override for any container that might interfere */
|
||||
.js-swimlane .list.list-resizing,
|
||||
.dragscroll .list.list-resizing,
|
||||
[id^="swimlane-"] .list.list-resizing {
|
||||
/* Force the width to be applied */
|
||||
width: var(--list-width, auto) !important;
|
||||
min-width: var(--list-width, auto) !important;
|
||||
max-width: var(--list-width, auto) !important;
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
float: left !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Ensure the width persists after resize is complete */
|
||||
.js-swimlane .list[style*="--list-width"],
|
||||
.dragscroll .list[style*="--list-width"],
|
||||
[id^="swimlane-"] .list[style*="--list-width"] {
|
||||
/* Maintain the width after resize */
|
||||
width: var(--list-width, auto) !important;
|
||||
min-width: var(--list-width, auto) !important;
|
||||
max-width: var(--list-width, auto) !important;
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
float: left !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Ensure consistent header height for all lists */
|
||||
.list-header {
|
||||
/* Maintain consistent height and padding for all lists */
|
||||
min-height: 2.5vh !important;
|
||||
height: auto !important;
|
||||
padding: 2.5vh 1.5vw 0.5vh !important;
|
||||
/* Make sure the background covers the full height */
|
||||
background-color: #e4e4e4 !important;
|
||||
border-bottom: 0.8vh solid #e4e4e4 !important;
|
||||
/* Use original display for consistent button positioning */
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
/* Prevent vertical expansion but allow normal height */
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* Ensure title text doesn't cause height changes for all lists */
|
||||
.list-header .list-header-name {
|
||||
/* Prevent text wrapping to maintain consistent height */
|
||||
white-space: nowrap !important;
|
||||
/* Truncate text with ellipsis if too long */
|
||||
text-overflow: ellipsis !important;
|
||||
/* Ensure proper line height */
|
||||
line-height: 1.2 !important;
|
||||
/* Ensure it doesn't overflow */
|
||||
overflow: hidden !important;
|
||||
/* Add margin to prevent overlap with buttons */
|
||||
margin-right: 120px !important;
|
||||
}
|
||||
|
||||
/* Position elements from right to left: hamburger, add card, drag handle */
|
||||
.list-header .js-open-list-menu {
|
||||
position: absolute !important;
|
||||
top: 2.5vh !important;
|
||||
right: 1.5vw !important;
|
||||
z-index: 15 !important;
|
||||
display: inline-block !important;
|
||||
padding: 4px !important;
|
||||
}
|
||||
|
||||
.list-header .list-header-plus-top {
|
||||
position: absolute !important;
|
||||
top: 2.5vh !important;
|
||||
right: 3.25vw !important;
|
||||
z-index: 15 !important;
|
||||
display: inline-block !important;
|
||||
padding: 4px !important;
|
||||
}
|
||||
|
||||
.list-header .list-header-handle-desktop {
|
||||
position: absolute !important;
|
||||
top: 2.5vh !important;
|
||||
right: 6.5vw !important;
|
||||
z-index: 15 !important;
|
||||
display: inline-block !important;
|
||||
cursor: move !important;
|
||||
pointer-events: auto !important;
|
||||
padding: 4px !important;
|
||||
}
|
||||
|
||||
/* Anchor header action buttons within header during resize */
|
||||
.list .list-header { position: relative; z-index: 5; }
|
||||
.list .list-header .js-open-list-menu,
|
||||
.list .list-header .list-header-plus-top,
|
||||
.list .list-header .list-header-handle-desktop {
|
||||
position: absolute !important;
|
||||
}
|
||||
[id^="swimlane-"] .list:first-child {
|
||||
min-width: 20px;
|
||||
min-width: 2.5vw;
|
||||
}
|
||||
.list.list-auto-width {
|
||||
flex: 1;
|
||||
@@ -33,11 +241,79 @@
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
height: 100px;
|
||||
height: 15vh;
|
||||
}
|
||||
.list.list-collapsed {
|
||||
flex: none;
|
||||
min-width: 60px;
|
||||
max-width: 80px;
|
||||
width: 60px;
|
||||
min-height: 60vh;
|
||||
height: 60vh;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
}
|
||||
.list.list-collapsed .list-header {
|
||||
padding: 1vh 1.5vw 0.5vh;
|
||||
min-height: 2.5vh !important;
|
||||
height: auto !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
overflow: visible !important;
|
||||
width: 100%;
|
||||
max-width: 60px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
margin: 0 auto 0 auto;
|
||||
z-index: 10;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
width: fit-content;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-header-handle {
|
||||
position: absolute !important;
|
||||
top: 30px !important;
|
||||
right: 1.5vw !important;
|
||||
z-index: 15 !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
margin: 20px 0 0 0 !important;
|
||||
position: relative !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
text-align: left;
|
||||
overflow: visible;
|
||||
white-space: nowrap;
|
||||
display: block !important;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
color: #333;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ddd;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
margin: 0;
|
||||
width: 100vh;
|
||||
height: 30px;
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
top: 50%;
|
||||
transform: translateY(calc(-50% + 20px)) rotate(0deg);
|
||||
z-index: 10;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.list.list-composer .open-list-composer,
|
||||
.list .list-composer .open-list-composer {
|
||||
color: #8c8c8c;
|
||||
@@ -45,24 +321,24 @@
|
||||
.list.list-composer .list-name-input,
|
||||
.list .list-composer .list-name-input {
|
||||
background: #fff;
|
||||
margin: -3px 0 8px;
|
||||
margin: -0.4vh 0 1vh;
|
||||
}
|
||||
.list-header-add {
|
||||
flex: 0 0 auto;
|
||||
padding: 12px;
|
||||
padding: 1.5vh 1.5vw;
|
||||
position: relative;
|
||||
min-height: 20px;
|
||||
min-height: 2.5vh;
|
||||
}
|
||||
.list-header {
|
||||
flex: 0 0 auto;
|
||||
padding: 20px 12px 4px;
|
||||
padding: 2.5vh 1.5vw 0.5vh;
|
||||
position: relative;
|
||||
min-height: 20px;
|
||||
min-height: 2.5vh;
|
||||
background-color: #e4e4e4;
|
||||
border-bottom: 6px solid #e4e4e4;
|
||||
border-bottom: 0.8vh solid #e4e4e4;
|
||||
}
|
||||
.list-header.list-header-card-count {
|
||||
min-height: 35px;
|
||||
min-height: 4.5vh;
|
||||
height: auto;
|
||||
}
|
||||
.list-header.ui-sortable-handle {
|
||||
@@ -73,29 +349,38 @@
|
||||
}
|
||||
.list-header .list-header-name {
|
||||
display: inline;
|
||||
font-size: 16px;
|
||||
line-height: 17px;
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
min-height: 9px;
|
||||
min-width: 30px;
|
||||
min-height: 1.2vh;
|
||||
min-width: 4vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
/* Sum badge shown before list title */
|
||||
.list-header .list-sum-badge {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: #8c8c8c;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.list-rotated {
|
||||
width: 10px;
|
||||
height: 250px;
|
||||
margin-top: -90px;
|
||||
margin-left: -110px;
|
||||
width: 1.3vw;
|
||||
height: 35vh;
|
||||
margin-top: -12vh;
|
||||
margin-left: -14vw;
|
||||
margin-right: 0;
|
||||
transform: rotate(90deg);
|
||||
position: relative;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.list-header .list-rotated {
|
||||
|
||||
}
|
||||
.list-header .list-header-watch-icon {
|
||||
padding-left: 10px;
|
||||
@@ -121,11 +406,175 @@
|
||||
color: #a6a6a6;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.list-header .list-header-uncollapse-left {
|
||||
color: #a6a6a6;
|
||||
/* List header collapse button styling */
|
||||
.list-header .list-header-collapse-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.list-header .list-header-uncollapse-right {
|
||||
|
||||
.list-header .js-collapse {
|
||||
color: #a6a6a6;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 5px 8px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
text-decoration: none;
|
||||
margin: 0;
|
||||
}
|
||||
.list-header .js-collapse:hover {
|
||||
background-color: transparent;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.list-header .list-header-collapse-container > div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
display: inline-block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for collapsed lists */
|
||||
@media (min-width: 768px) {
|
||||
.list.list-collapsed {
|
||||
min-width: 60px;
|
||||
max-width: 80px;
|
||||
width: 60px;
|
||||
min-height: 60vh;
|
||||
height: 60vh;
|
||||
}
|
||||
.list.list-collapsed .list-header {
|
||||
max-width: 60px;
|
||||
margin: 0 auto;
|
||||
min-height: 2.5vh !important;
|
||||
height: auto !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
margin: 20px 0 0 0 !important;
|
||||
position: relative !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
width: 100vh;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
line-height: 1.2;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
top: 50%;
|
||||
transform: translateY(calc(-50% + 120px)) rotate(0deg);
|
||||
text-align: center;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
display: block !important;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ddd;
|
||||
color: #333;
|
||||
z-index: 10;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
margin: 0 auto 20px auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.list.list-collapsed {
|
||||
min-height: 60vh;
|
||||
height: 60vh;
|
||||
}
|
||||
.list.list-collapsed .list-header {
|
||||
min-height: 2.5vh !important;
|
||||
height: auto !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
margin: 20px 0 0 0 !important;
|
||||
position: relative !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
width: 100vh;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
line-height: 1.2;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
top: 50%;
|
||||
transform: translateY(calc(-50% + 120px)) rotate(0deg);
|
||||
text-align: center;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
display: block !important;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ddd;
|
||||
color: #333;
|
||||
z-index: 10;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
margin: 0 auto 20px auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.list.list-collapsed {
|
||||
min-height: 60vh;
|
||||
height: 60vh;
|
||||
}
|
||||
.list.list-collapsed .list-header {
|
||||
min-height: 2.5vh !important;
|
||||
height: auto !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
margin: 20px 0 0 0 !important;
|
||||
position: relative !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
width: 100vh;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
line-height: 1.2;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
top: 50%;
|
||||
transform: translateY(calc(-50% + 40px)) rotate(0deg);
|
||||
text-align: left;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
display: block !important;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ddd;
|
||||
color: #333;
|
||||
z-index: 10;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
margin: 0 auto 20px auto;
|
||||
}
|
||||
}
|
||||
.list-header .list-header-collapse {
|
||||
color: #a6a6a6;
|
||||
@@ -214,19 +663,166 @@
|
||||
#js-list-width-edit .list-width-error {
|
||||
display: none;
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
|
||||
.mini-list.mobile-view {
|
||||
flex: 0 0 60px;
|
||||
height: auto;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
border-left: 0px !important;
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: block !important;
|
||||
}
|
||||
.list.mobile-view {
|
||||
display: block !important;
|
||||
flex-basis: auto;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
border-left: 0px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.list.mobile-view:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
.list.mobile-view.ui-sortable-helper {
|
||||
flex: 0 0 60px;
|
||||
height: 60px;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
border-left: 0px !important;
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: block !important;
|
||||
}
|
||||
.list.mobile-view.ui-sortable-helper .list-header.ui-sortable-handle {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.list.mobile-view.placeholder {
|
||||
flex: 0 0 60px;
|
||||
height: 60px;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
border-left: 0px !important;
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: block !important;
|
||||
}
|
||||
.list.mobile-view .list-body {
|
||||
padding: 15px 19px;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
}
|
||||
.list.mobile-view .list-header {
|
||||
/*Updated padding values for mobile devices, this should fix text grouping issue*/
|
||||
padding: 20px 0px 20px 0px;
|
||||
border-bottom: 0px solid #e4e4e4;
|
||||
min-height: 30px;
|
||||
margin-top: 10px;
|
||||
align-items: center;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
/* Force grid layout for iPhone */
|
||||
display: grid !important;
|
||||
grid-template-columns: 30px 1fr auto auto !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-left-icon {
|
||||
padding: 7px;
|
||||
padding-right: 27px;
|
||||
margin-top: 1px;
|
||||
top: -7px;
|
||||
left: -7px;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-menu-icon {
|
||||
padding: 14px;
|
||||
font-size: 40px !important;
|
||||
text-align: center;
|
||||
/* Force positioning for iPhone */
|
||||
position: absolute !important;
|
||||
right: 60px !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
z-index: 10;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-handle {
|
||||
padding: 14px;
|
||||
font-size: 48px !important;
|
||||
text-align: center;
|
||||
/* Force positioning for iPhone */
|
||||
position: absolute !important;
|
||||
right: 10px !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
z-index: 10;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-left-icon {
|
||||
display: grid;
|
||||
grid-row: 1/3;
|
||||
grid-column: 1;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-name {
|
||||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
align-self: end;
|
||||
font-size: 20px !important;
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.list.mobile-view .list-header .cardCount {
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
align-self: start;
|
||||
text-align: left;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-menu {
|
||||
grid-row: 1/3;
|
||||
grid-column: 3;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-menu-icon {
|
||||
grid-row: 1/3;
|
||||
grid-column: 3;
|
||||
}
|
||||
.list.mobile-view .list-header .list-header-handle {
|
||||
grid-row: 1/3;
|
||||
grid-column: 4;
|
||||
}
|
||||
.list.mobile-view .list-header .inlined-form {
|
||||
grid-row: 1/3;
|
||||
grid-column: 1/4;
|
||||
}
|
||||
.list.mobile-view .list-header .edit-controls {
|
||||
align-items: initial;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||
.mini-list {
|
||||
flex: 0 0 60px;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
border-left: 0px;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
border-left: 0px !important;
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: block !important;
|
||||
}
|
||||
.list {
|
||||
display: contents;
|
||||
display: block !important;
|
||||
flex-basis: auto;
|
||||
width: 100%;
|
||||
border-left: 0px;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
border-left: 0px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.list:first-child {
|
||||
margin-left: 0px;
|
||||
@@ -234,9 +830,11 @@
|
||||
.list.ui-sortable-helper {
|
||||
flex: 0 0 60px;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
border-left: 0px;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
border-left: 0px !important;
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: block !important;
|
||||
}
|
||||
.list.ui-sortable-helper .list-header.ui-sortable-handle {
|
||||
cursor: grabbing;
|
||||
@@ -244,12 +842,17 @@
|
||||
.list.placeholder {
|
||||
flex: 0 0 60px;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
border-left: 0px;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
border-left: 0px !important;
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: block !important;
|
||||
}
|
||||
.list-body {
|
||||
padding: 15px 19px;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
}
|
||||
.list-header {
|
||||
/*Updated padding values for mobile devices, this should fix text grouping issue*/
|
||||
@@ -258,6 +861,9 @@
|
||||
min-height: 30px;
|
||||
margin-top: 10px;
|
||||
align-items: center;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
}
|
||||
.list-header .list-header-left-icon {
|
||||
padding: 7px;
|
||||
@@ -267,24 +873,29 @@
|
||||
left: -7px;
|
||||
}
|
||||
.list-header .list-header-menu-icon {
|
||||
padding: 14px;
|
||||
font-size: 40px;
|
||||
text-align: center;
|
||||
/* iOS Safari fallback positioning */
|
||||
position: absolute;
|
||||
padding: 7px;
|
||||
right: 60px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 47px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.list-header .list-header-handle {
|
||||
padding: 14px;
|
||||
font-size: 48px;
|
||||
text-align: center;
|
||||
/* iOS Safari fallback positioning */
|
||||
position: absolute;
|
||||
padding: 7px;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 10px;
|
||||
font-size: 24px;
|
||||
}
|
||||
.list-header {
|
||||
display: grid;
|
||||
grid-template-columns: 30px 5fr 1fr;
|
||||
grid-template-columns: 30px 1fr auto auto;
|
||||
gap: 10px;
|
||||
}
|
||||
.list-header .list-header-left-icon {
|
||||
display: grid;
|
||||
@@ -295,16 +906,30 @@
|
||||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
align-self: end;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.list-header .cardCount {
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
align-self: start;
|
||||
font-size: 16px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.list-header .list-header-menu {
|
||||
grid-row: 1/3;
|
||||
grid-column: 3;
|
||||
}
|
||||
.list-header .list-header-menu-icon {
|
||||
grid-row: 1/3;
|
||||
grid-column: 3;
|
||||
}
|
||||
.list-header .list-header-handle {
|
||||
grid-row: 1/3;
|
||||
grid-column: 4;
|
||||
}
|
||||
.list-header .inlined-form {
|
||||
grid-row: 1/3;
|
||||
grid-column: 1/4;
|
||||
@@ -313,6 +938,152 @@
|
||||
align-items: initial;
|
||||
}
|
||||
}
|
||||
|
||||
/* iPhone 12 Mini specific - fix icon positioning in stacked lists view */
|
||||
@media screen and (device-width: 375px) and (device-height: 812px), /* iPhone 12 Mini exact */
|
||||
screen and (max-width: 375px) and (max-height: 812px), /* iPhone 12 Mini viewport */
|
||||
screen and (-webkit-min-device-pixel-ratio: 3) and (max-width: 375px) /* iPhone 12 Mini Retina */ {
|
||||
.list.mobile-view .list-header {
|
||||
/* Force grid layout for iPhone 12 Mini */
|
||||
display: grid !important;
|
||||
grid-template-columns: 30px 1fr auto auto !important;
|
||||
gap: 10px !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.list.mobile-view .list-header .list-header-menu-icon {
|
||||
/* Remove absolute positioning for iPhone 12 Mini */
|
||||
position: static !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
/* Use grid positioning */
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 3 !important;
|
||||
padding: 14px !important;
|
||||
font-size: 40px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.list.mobile-view .list-header .list-header-handle {
|
||||
/* Remove absolute positioning for iPhone 12 Mini */
|
||||
position: static !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
/* Use grid positioning */
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 4 !important;
|
||||
padding: 14px !important;
|
||||
font-size: 48px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.list.mobile-view .list-header .list-header-name {
|
||||
grid-row: 1 !important;
|
||||
grid-column: 2 !important;
|
||||
align-self: end !important;
|
||||
font-size: 20px !important;
|
||||
font-weight: bold !important;
|
||||
line-height: 1.2 !important;
|
||||
padding-bottom: 2px !important;
|
||||
}
|
||||
|
||||
.list.mobile-view .list-header .cardCount {
|
||||
grid-row: 2 !important;
|
||||
grid-column: 2 !important;
|
||||
align-self: start !important;
|
||||
text-align: left !important;
|
||||
padding-left: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
|
||||
.list.mobile-view .list-header .list-header-left-icon {
|
||||
display: grid !important;
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* iPhone device JavaScript detection fallback - fix icon positioning */
|
||||
.iphone-device .list.mobile-view .list-header {
|
||||
/* Force grid layout for iPhone devices */
|
||||
display: grid !important;
|
||||
grid-template-columns: 30px 1fr auto auto !important;
|
||||
gap: 10px !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.iphone-device .list.mobile-view .list-header .list-header-menu-icon {
|
||||
/* Remove absolute positioning for iPhone devices */
|
||||
position: static !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
/* Use grid positioning */
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 3 !important;
|
||||
padding: 14px !important;
|
||||
font-size: 40px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.iphone-device .list.mobile-view .list-header .list-header-handle {
|
||||
/* Remove absolute positioning for iPhone devices */
|
||||
position: static !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
transform: none !important;
|
||||
/* Use grid positioning */
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 4 !important;
|
||||
padding: 14px !important;
|
||||
font-size: 48px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.iphone-device .list.mobile-view .list-header .list-header-name {
|
||||
grid-row: 1 !important;
|
||||
grid-column: 2 !important;
|
||||
align-self: end !important;
|
||||
font-size: 20px !important;
|
||||
font-weight: bold !important;
|
||||
line-height: 1.2 !important;
|
||||
padding-bottom: 2px !important;
|
||||
}
|
||||
|
||||
.iphone-device .list.mobile-view .list-header .cardCount {
|
||||
grid-row: 2 !important;
|
||||
grid-column: 2 !important;
|
||||
align-self: start !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
|
||||
.iphone-device .list.mobile-view .list-header .list-header-left-icon {
|
||||
display: grid !important;
|
||||
grid-row: 1/3 !important;
|
||||
grid-column: 1 !important;
|
||||
}
|
||||
|
||||
/* Allow long list titles to expand on desktop (non-mobile, non-collapsed) */
|
||||
.list:not(.mobile-view):not(.list-collapsed) .list-header {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.list:not(.mobile-view):not(.list-collapsed) .list-header .list-header-name {
|
||||
/* Permit wrapping and full visibility */
|
||||
white-space: normal !important;
|
||||
overflow: visible !important;
|
||||
text-overflow: clip !important;
|
||||
display: inline-block !important;
|
||||
/* Reserve space for right-side controls (menu, handle, count) */
|
||||
max-width: calc(100% - 120px) !important;
|
||||
/* Break long words to avoid overflow */
|
||||
word-break: break-word !important;
|
||||
}
|
||||
.link-board-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
template(name='list')
|
||||
.list.js-list(id="js-list-{{_id}}"
|
||||
style="{{#unless collapsed}}min-width:{{listWidth}}px;max-width:{{listConstraint}}px;{{/unless}}"
|
||||
class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}}")
|
||||
class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}} {{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
+listHeader
|
||||
+listBody
|
||||
unless collapsed
|
||||
+listBody
|
||||
.list-resize-handle.js-list-resize-handle.nodragscroll
|
||||
|
||||
template(name='miniList')
|
||||
a.mini-list.js-select-list.js-list(id="js-list-{{_id}}")
|
||||
a.mini-list.js-select-list.js-list(id="js-list-{{_id}}" class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
+listHeader
|
||||
Loaded 100 of 930 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user