Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
479472c1d8 | ||
|
|
c10c10c4a9 | ||
|
|
d8c515c858 | ||
|
|
168a308f5f | ||
|
|
fe40c5fd37 | ||
|
|
c430e36357 | ||
|
|
007bf743a6 | ||
|
|
774f57f3f6 | ||
|
|
044a64b964 | ||
|
|
f6bdb4d694 | ||
|
|
6a6b76e084 | ||
|
|
9357ce8a26 | ||
|
|
0aab2b3085 | ||
|
|
3edafd6c67 | ||
|
|
850ded2e17 | ||
|
|
67a58daaf3 | ||
|
|
9fdafd2008 | ||
|
|
66c37ccd3e | ||
|
|
fd3139201a | ||
|
|
abfedf573b | ||
|
|
a1f9536657 | ||
|
|
dec0563e3c | ||
|
|
9a4136399f | ||
|
|
20b0177105 | ||
|
|
e0957f70e6 | ||
|
|
125c4684bd | ||
|
|
991d4909e1 | ||
|
|
f81fd8084f | ||
|
|
231137617f | ||
|
|
f6b4c0ab25 | ||
|
|
30f2de151b | ||
|
|
20fe8bc09b | ||
|
|
2e715444ed | ||
|
|
1b13beb106 | ||
|
|
88213911fd | ||
|
|
50f3957011 | ||
|
|
bf18792d77 | ||
|
|
ca39fce578 | ||
|
|
f0df8dcdc7 | ||
|
|
7d594705fb | ||
|
|
fe5e124f93 | ||
|
|
41b1c55988 | ||
|
|
cfcbf640d6 | ||
|
|
d344c39d49 | ||
|
|
22d07d7a65 | ||
|
|
44fc7c0771 | ||
|
|
7ec671bb9f | ||
|
|
df35683043 | ||
|
|
03bfd8a734 | ||
|
|
409245e374 | ||
|
|
5870d38e8e | ||
|
|
c105e348d8 | ||
|
|
ccd0f89ae0 | ||
|
|
bdd28444bb | ||
|
|
c96468d76d | ||
|
|
943cd25592 | ||
|
|
7f4d3ff3ec | ||
|
|
5d2daa4a80 | ||
|
|
6372bb82c4 | ||
|
|
03a7da0795 | ||
|
|
bc1165ef3e | ||
|
|
47f34f6d12 | ||
|
|
d887e553ce | ||
|
|
13b5bb6407 | ||
|
|
5863df3323 | ||
|
|
b0cf69c160 | ||
|
|
60b2787c55 | ||
|
|
4d1e4f011f | ||
|
|
ec607596d1 | ||
|
|
7e4b791c29 | ||
|
|
55d8d26b34 | ||
|
|
3f5175778d | ||
|
|
7e80db36cf | ||
|
|
6828a1e07a | ||
|
|
2a51a17ddc | ||
|
|
ce11933dda | ||
|
|
b9f87bf310 | ||
|
|
28eeee3a28 | ||
|
|
e17e8d4154 | ||
|
|
dee7020a5a | ||
|
|
79f4e2ad6d | ||
|
|
25f1256724 | ||
|
|
4d49265b25 | ||
|
|
3d0e91f80e | ||
|
|
533857c961 | ||
|
|
83bc176ebf | ||
|
|
910f0cecbe | ||
|
|
10f0c49007 | ||
|
|
ba2911a645 | ||
|
|
07860bd4de | ||
|
|
42bd03e333 | ||
|
|
97f525fdbc | ||
|
|
1050e04c10 | ||
|
|
1534b22315 | ||
|
|
bcec728c2e | ||
|
|
6f503ca818 | ||
|
|
1b927a9d3a | ||
|
|
045b8a84a2 | ||
|
|
1624fc82f7 | ||
|
|
42d8773e36 | ||
|
|
15dff73b94 | ||
|
|
ec1d8f275f | ||
|
|
68391a943b | ||
|
|
9c48cc9ec0 | ||
|
|
41c06c1374 | ||
|
|
2c6841210c | ||
|
|
eba8fb4c9c | ||
|
|
95522ebcd9 | ||
|
|
8bf13f43e8 | ||
|
|
ca8f2a70a4 | ||
|
|
f6e9f5a5e8 | ||
|
|
be6d1044a0 | ||
|
|
52938b2f1c | ||
|
|
116372e11e | ||
|
|
14cdb5bdf1 | ||
|
|
de28bf8569 | ||
|
|
419615bed4 | ||
|
|
3ee9b13b3b | ||
|
|
0725ce3fb1 | ||
|
|
eb7623e218 | ||
|
|
19acd7861d | ||
|
|
282f0f91fe | ||
|
|
b51094d5cd | ||
|
|
b9a4b0b51d | ||
|
|
1a49d25c90 | ||
|
|
f57ed2990f | ||
|
|
87412ceb55 | ||
|
|
87cb4598f7 | ||
|
|
b35b9d8aa2 | ||
|
|
6646d48ccb | ||
|
|
23ee93ca3d | ||
|
|
0911511d0c | ||
|
|
b7562a7085 | ||
|
|
16a793e672 | ||
|
|
6e4407ed9c | ||
|
|
05cd1247ab | ||
|
|
af240450d6 | ||
|
|
ba24c4e40c | ||
|
|
2cbd7d83f9 | ||
|
|
892008563b | ||
|
|
c7a40784e7 | ||
|
|
c5e0832e00 | ||
|
|
eb4219504d | ||
|
|
a5e5b44330 | ||
|
|
ee6852f8ea | ||
|
|
2575552e11 | ||
|
|
a77cf56fbd | ||
|
|
07dfe33ed8 | ||
|
|
c8b21b2e17 | ||
|
|
0be1c00fcc | ||
|
|
20edac091a | ||
|
|
311c0d23a4 | ||
|
|
032099cfbe | ||
|
|
cd97cca7b8 | ||
|
|
3f0fddf448 | ||
|
|
f1e103b082 | ||
|
|
afe00d02cd | ||
|
|
e21c078521 | ||
|
|
5eb3784527 | ||
|
|
0ff1e63a5d | ||
|
|
b11ae567c9 | ||
|
|
e41256f935 | ||
|
|
b00db983c8 | ||
|
|
3e4a8bca36 | ||
|
|
54b4682258 | ||
|
|
bda49ed609 | ||
|
|
39be9aa58d | ||
|
|
f245b6b7fa | ||
|
|
ad87f9ff4e | ||
|
|
40abff4c1e | ||
|
|
51ad89c3af | ||
|
|
768412ba7c | ||
|
|
670b964e6b | ||
|
|
f6c377eb9f | ||
|
|
6d063a4f64 | ||
|
|
207be3a363 | ||
|
|
e1ffe943c8 | ||
|
|
431d884e83 | ||
|
|
983714cd72 | ||
|
|
e6629779f7 | ||
|
|
f1587753cb | ||
|
|
a6d4f445fb | ||
|
|
8f34cdc279 | ||
|
|
71cbac358b | ||
|
|
3b2b108744 | ||
|
|
100e2d4696 | ||
|
|
d42c4fb4f6 | ||
|
|
ed3e20dce5 | ||
|
|
82b0bd9ddb | ||
|
|
ab6ec59bfb | ||
|
|
b144fab042 | ||
|
|
b1d1c7c45a | ||
|
|
2353f654de | ||
|
|
6e0ae161ad | ||
|
|
f1660e3b9f | ||
|
|
336a22555f | ||
|
|
06b548f12e | ||
|
|
e4de42d487 | ||
|
|
1617577378 | ||
|
|
2add82efc5 | ||
|
|
06cacd7a5a | ||
|
|
2d4f29cb5a | ||
|
|
0b26250b46 | ||
|
|
09677d0bfa | ||
|
|
aa4bdf7efb | ||
|
|
5755ece33e | ||
|
|
2e93a975c4 | ||
|
|
c4df69a05b | ||
|
|
a562ee4ba1 | ||
|
|
781b384fe9 | ||
|
|
b6cc6c440d | ||
|
|
20da91b774 | ||
|
|
e530077fc5 | ||
|
|
444fff47dc | ||
|
|
1aa8f502ae | ||
|
|
2a25318ba8 | ||
|
|
39a8524354 | ||
|
|
573ca73b45 | ||
|
|
07d1a864d8 | ||
|
|
ffcd380523 | ||
|
|
55534b8b2e | ||
|
|
ca23934bde | ||
|
|
8a622ec7c3 | ||
|
|
39519d1cc9 | ||
|
|
dcbf92b5b1 | ||
|
|
7f6d500cbe | ||
|
|
cb1e91fee8 | ||
|
|
96494bacf5 | ||
|
|
99f68f36b0 | ||
|
|
61e682470c | ||
|
|
415e94d187 | ||
|
|
6b22f96313 | ||
|
|
8c3322f9a9 | ||
|
|
f935cf391e | ||
|
|
fb44df9815 | ||
|
|
e127895cf3 | ||
|
|
1f85b25549 | ||
|
|
df9851e2b7 | ||
|
|
232bc746f4 | ||
|
|
ea448cd6c3 | ||
|
|
a7d12ddcaa | ||
|
|
420e9b23d2 | ||
|
|
cd49018306 | ||
|
|
ddbf4f18ff | ||
|
|
24104991b4 | ||
|
|
d9d451a206 | ||
|
|
1f07050979 | ||
|
|
399ddd2dab | ||
|
|
2876ce633c | ||
|
|
333b9c811d | ||
|
|
1df3d56f7c | ||
|
|
d2a7a82870 | ||
|
|
5e49006b6f | ||
|
|
f73ea218ee | ||
|
|
6ac3249272 | ||
|
|
784f960e8f | ||
|
|
04db8d739e | ||
|
|
7226afdc39 | ||
|
|
446a81ddd0 | ||
|
|
5fd167c182 | ||
|
|
7cd57ee023 | ||
|
|
07ab3b83b1 | ||
|
|
3ade9d95a6 | ||
|
|
55c5f07026 | ||
|
|
b4ade0256b | ||
|
|
afcb8aa49b | ||
|
|
a47ca598e7 | ||
|
|
c9a28db3ab | ||
|
|
1cfb6eee4b | ||
|
|
6c68867efe | ||
|
|
0ef84a0d5f | ||
|
|
e0f4758667 | ||
|
|
8c149da9e9 | ||
|
|
18eafe2fec | ||
|
|
5ac636fdd4 | ||
|
|
6db3474cdf | ||
|
|
6a1bc167cf | ||
|
|
191071d295 | ||
|
|
3e4a9471ba | ||
|
|
9b68aa919a | ||
|
|
8732e4b18f | ||
|
|
d26786a628 | ||
|
|
54b9035eb2 | ||
|
|
3cf6ed916f | ||
|
|
351d9d0c95 | ||
|
|
c0923ed3cf | ||
|
|
fc2eb2e935 | ||
|
|
88844034fc | ||
|
|
3d33ea911a | ||
|
|
bb08e40ea5 | ||
|
|
c4879de4b1 | ||
|
|
fd0afd7b05 | ||
|
|
5c02850bc1 | ||
|
|
1f450df1af | ||
|
|
e933890730 | ||
|
|
8eafa1ac66 | ||
|
|
858bb35612 | ||
|
|
f9018fc3a8 | ||
|
|
252c4b19f9 | ||
|
|
05349a5deb | ||
|
|
d52affe658 | ||
|
|
23dcd084a4 | ||
|
|
a853189486 | ||
|
|
82d0a3df85 | ||
|
|
ef189477dd | ||
|
|
4bcc795778 | ||
|
|
01346152b5 | ||
|
|
5b7b5b3e7e | ||
|
|
2df29324e6 | ||
|
|
51f52fee23 | ||
|
|
6e088af3d1 | ||
|
|
fc9f0d8392 | ||
|
|
cfa2bbd3d4 | ||
|
|
52d633ca69 | ||
|
|
b2fee6a6c1 | ||
|
|
66d35a1528 | ||
|
|
b7105d7b57 | ||
|
|
d5fbd50b76 | ||
|
|
06515559a6 | ||
|
|
c61a126c8b | ||
|
|
b737adfcdf | ||
|
|
3b44acd87c | ||
|
|
6fad68b9b9 | ||
|
|
b05f8e5251 | ||
|
|
31c2afb072 | ||
|
|
863f0fc5db | ||
|
|
055b528532 | ||
|
|
fda392e662 | ||
|
|
e5f9d0a4dd | ||
|
|
3f16bf6054 | ||
|
|
6774b9e905 | ||
|
|
00a38b36a4 | ||
|
|
de365ad62a | ||
|
|
fe631ff75b | ||
|
|
eed649fbe6 | ||
|
|
8ddbc622db | ||
|
|
24b21d23ba | ||
|
|
64fa02cdf5 | ||
|
|
3b50b2c59f | ||
|
|
921460db40 | ||
|
|
4156073b1a | ||
|
|
af7c74b2d3 | ||
|
|
4064f3f406 | ||
|
|
94e47401cb | ||
|
|
5ef83ab236 | ||
|
|
c22eaa26cd | ||
|
|
060b9acd86 | ||
|
|
213b7b4819 | ||
|
|
7a576f9ae4 | ||
|
|
62f06a792f | ||
|
|
1ac11d92ba | ||
|
|
0c299e3225 | ||
|
|
bcc3a39c06 | ||
|
|
13dffca055 | ||
|
|
68ee88a041 | ||
|
|
3718dd4d45 | ||
|
|
506a747fce | ||
|
|
98e255ccec | ||
|
|
4879df0244 | ||
|
|
3bdeaaf552 | ||
|
|
45dea737bf | ||
|
|
b80396f627 | ||
|
|
0735981366 | ||
|
|
5d8cca40d2 | ||
|
|
09ce3e464f | ||
|
|
4c5a2fbd1f | ||
|
|
9c6cd51ca7 | ||
|
|
ea74a34d72 | ||
|
|
28cffc4328 | ||
|
|
0a12319002 | ||
|
|
2f33e3a76b | ||
|
|
96507e6777 | ||
|
|
e6d6b5322e | ||
|
|
b8d8081ded | ||
|
|
0e428233f2 | ||
|
|
fdbc1b5c22 | ||
|
|
ea0239538a | ||
|
|
aa084b7a7e | ||
|
|
8a2509007c | ||
|
|
d0208b2112 | ||
|
|
851bfa53e6 | ||
|
|
ba01ebe05d | ||
|
|
e6a2ffc592 | ||
|
|
f90ca00669 | ||
|
|
bbb65ae4e2 | ||
|
|
eece32d51b | ||
|
|
1865bdbee9 | ||
|
|
85f254da38 | ||
|
|
1fe7394d05 | ||
|
|
6b9a7a3fc1 | ||
|
|
8f33517142 | ||
|
|
0a51ea753f | ||
|
|
235bedab00 | ||
|
|
564e1919af | ||
|
|
1c9dd7233e | ||
|
|
6e7a58b204 | ||
|
|
75bdd33fda | ||
|
|
5f915ef966 | ||
|
|
a570c4a861 | ||
|
|
a797abaa36 | ||
|
|
2bb5a31fa4 | ||
|
|
012ca39a8d | ||
|
|
c3458855bd | ||
|
|
cfcc73724f | ||
|
|
4448488767 | ||
|
|
1742bcd9b1 | ||
|
|
7dc0bbd7b2 | ||
|
|
269698ba78 | ||
|
|
0a1bfd37b3 | ||
|
|
ec03bbe260 | ||
|
|
3cc0a93e0e | ||
|
|
079867ff37 | ||
|
|
533bc045d0 | ||
|
|
b5773232d5 | ||
|
|
3042d2ff9a | ||
|
|
8d5adc0464 | ||
|
|
9cba640120 | ||
|
|
b740381a72 | ||
|
|
2691f033cb | ||
|
|
c7d4a90d5c | ||
|
|
92dd6bbba2 | ||
|
|
e42adcaed0 | ||
|
|
301d96f392 | ||
|
|
1c488cb8a7 | ||
|
|
9e33d8cbab | ||
|
|
4c3907bd41 | ||
|
|
153d729544 | ||
|
|
3d5abd60cc | ||
|
|
1d89e96dd1 | ||
|
|
bc3696b64a | ||
|
|
f803520c76 | ||
|
|
6afc9259f0 | ||
|
|
47df8d6737 | ||
|
|
d2dfd9c3b9 | ||
|
|
63f501c574 | ||
|
|
6aa0aa50dc | ||
|
|
9ae20a3f51 | ||
|
|
280e66947e | ||
|
|
9b9e3c9e0c | ||
|
|
1696bd688e | ||
|
|
63ad79a784 | ||
|
|
f7a0d15db7 | ||
|
|
6a7cb809f4 | ||
|
|
3244c3c32b | ||
|
|
09666585e2 | ||
|
|
ee106d1cb4 | ||
|
|
8ef435c83e | ||
|
|
3a74b88294 | ||
|
|
c5079096b5 | ||
|
|
927e43ab3f | ||
|
|
eddcb2260b | ||
|
|
ff73aa4566 | ||
|
|
de993a079d | ||
|
|
7bb0aa7488 | ||
|
|
fe7398faef | ||
|
|
8e709c1b79 | ||
|
|
3908fe0ab8 | ||
|
|
5d2ff89e9b | ||
|
|
7f38bc7b95 | ||
|
|
ed0c64a270 | ||
|
|
e63e7b1131 | ||
|
|
3f7d9fd42c | ||
|
|
867344f070 | ||
|
|
a84195179e | ||
|
|
f1b18d79cd | ||
|
|
6d1cdebfe2 | ||
|
|
7cf8487f57 | ||
|
|
5403027c9d | ||
|
|
daf397a1ba | ||
|
|
6104c1711f | ||
|
|
db41e41d90 | ||
|
|
1235363465 | ||
|
|
7263d7497f | ||
|
|
666186fee8 | ||
|
|
27d1b5acca | ||
|
|
5eb5ca5752 | ||
|
|
16d371b814 | ||
|
|
df9b4db49d | ||
|
|
2e8e58004d | ||
|
|
a83aa6162b | ||
|
|
07c915ccf0 | ||
|
|
405f176bbb | ||
|
|
6476503137 | ||
|
|
981ed546f1 | ||
|
|
c1287248a6 | ||
|
|
52f884f2b1 | ||
|
|
4550e10fbe | ||
|
|
2efe974195 | ||
|
|
6b808dd74b | ||
|
|
8a917e0efc | ||
|
|
d1f98d0c47 | ||
|
|
323de77a83 | ||
|
|
738e7f9ebc | ||
|
|
b5b4af73a9 | ||
|
|
ab1014b7d8 | ||
|
|
12964aa742 | ||
|
|
e4b1106acd | ||
|
|
d0524532a4 | ||
|
|
7de18eccea | ||
|
|
f9747bc908 | ||
|
|
6fd827792a | ||
|
|
9e95c06415 | ||
|
|
e7603298d7 | ||
|
|
8e14459cff | ||
|
|
3ac5dba243 | ||
|
|
9969c89b1c | ||
|
|
5a5b19d4fc | ||
|
|
4d48fa4d95 | ||
|
|
57545f66d2 | ||
|
|
8586c843a4 | ||
|
|
b2acc3ba45 | ||
|
|
9f0273aa44 | ||
|
|
3407b0e65b | ||
|
|
6cd92ab2c6 | ||
|
|
e92ebe5151 | ||
|
|
8cb838c0de | ||
|
|
6cae14d6e9 | ||
|
|
b42d8346cd | ||
|
|
1a065ff351 | ||
|
|
3565ff2700 | ||
|
|
e354715a9d | ||
|
|
e7002f417b | ||
|
|
ef5f38f431 | ||
|
|
b3efa71d13 | ||
|
|
10fcc19b7f | ||
|
|
948f05fecb | ||
|
|
927bb83813 | ||
|
|
9f396e9038 | ||
|
|
2400c91013 | ||
|
|
f583645744 | ||
|
|
42b3cff99f | ||
|
|
960fe5163b | ||
|
|
41665c079d | ||
|
|
8a0defc988 | ||
|
|
d7e0350ecf | ||
|
|
bf15fa988d | ||
|
|
db2d59181c | ||
|
|
dcc9a0d498 | ||
|
|
936dea591a | ||
|
|
cf7e5472ba | ||
|
|
5c61119716 | ||
|
|
3558d711ff | ||
|
|
72c1c49da3 | ||
|
|
93f8ff946f | ||
|
|
61d582ffd9 | ||
|
|
60384be5ff | ||
|
|
8ac305778a | ||
|
|
5be93741ee | ||
|
|
c2f91baaf9 | ||
|
|
9df5c0e2de | ||
|
|
f1a9e30956 | ||
|
|
4b202aaa08 | ||
|
|
5ef1a92366 | ||
|
|
1a836969e1 | ||
|
|
079b3da6ef | ||
|
|
13563e20e7 | ||
|
|
4f1330c777 | ||
|
|
94b558cf43 | ||
|
|
e22e24d917 | ||
|
|
55545a52ca | ||
|
|
e0cb5a0c7a | ||
|
|
efd3faf9ef | ||
|
|
2fd577d2c0 | ||
|
|
659e8418f7 | ||
|
|
18610d2fe6 | ||
|
|
1389ed0fce | ||
|
|
c39364af83 | ||
|
|
3e817257ef | ||
|
|
c5f782976b | ||
|
|
b841f798f8 | ||
|
|
35ae07e2a6 | ||
|
|
2c792363bf | ||
|
|
a68ecc079d | ||
|
|
b2d546579c | ||
|
|
4bc6ad3b4c | ||
|
|
7f558fede0 | ||
|
|
544bc1d6ad | ||
|
|
89d25f52fa | ||
|
|
2fd71d5be8 | ||
|
|
dc643f3d43 | ||
|
|
4469b12203 | ||
|
|
9cacaafb8f | ||
|
|
269382869e | ||
|
|
8648e32108 | ||
|
|
edf52bc438 | ||
|
|
58ce859368 | ||
|
|
e23aacedb3 | ||
|
|
7f02990626 | ||
|
|
c86155f1aa | ||
|
|
3fae5355d4 | ||
|
|
20e88159ae | ||
|
|
a81921382a | ||
|
|
8f28a409c7 | ||
|
|
7566f05b97 | ||
|
|
268d7fcb32 | ||
|
|
70edd44a01 | ||
|
|
6db717b9b3 | ||
|
|
f389fd95fb | ||
|
|
363bcab98b | ||
|
|
c4a7e53661 | ||
|
|
c6afe8d007 | ||
|
|
93eccffd44 | ||
|
|
fe66169415 | ||
|
|
6ced47aa8c | ||
|
|
e661d03e8d | ||
|
|
f09219cbfd | ||
|
|
7c25542976 | ||
|
|
5899b9366c | ||
|
|
4b196d5378 | ||
|
|
b34ed58289 | ||
|
|
617fdaeb74 | ||
|
|
d26bf04bfa | ||
|
|
a365899312 | ||
|
|
6ebd6defe9 | ||
|
|
93337c20f8 | ||
|
|
6cdd464f54 | ||
|
|
3e0bedd8c7 | ||
|
|
4dcdec0084 | ||
|
|
05c53ca01d |
No files matched your search
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:disco
|
||||
FROM ubuntu:rolling
|
||||
LABEL maintainer="sgr"
|
||||
|
||||
ENV BUILD_DEPS="gnupg gosu bsdtar wget curl bzip2 g++ build-essential python git ca-certificates iproute2"
|
||||
@@ -6,8 +6,8 @@ ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ENV \
|
||||
DEBUG=false \
|
||||
NODE_VERSION=8.17.0 \
|
||||
METEOR_RELEASE=1.8.1 \
|
||||
NODE_VERSION=v12.18.3 \
|
||||
METEOR_RELEASE=1.10.2 \
|
||||
USE_EDGE=false \
|
||||
METEOR_EDGE=1.5-beta.17 \
|
||||
NPM_VERSION=latest \
|
||||
@@ -21,10 +21,13 @@ ENV \
|
||||
ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE=3 \
|
||||
ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD=60 \
|
||||
ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15 \
|
||||
RICHER_CARD_COMMENT_EDITOR=true \
|
||||
RICHER_CARD_COMMENT_EDITOR=false \
|
||||
CARD_OPENED_WEBHOOK_ENABLED=false \
|
||||
ATTACHMENTS_STORE_PATH="" \
|
||||
MAX_IMAGE_PIXEL="" \
|
||||
IMAGE_COMPRESS_RATIO="" \
|
||||
BIGEVENTS_PATTERN="" \
|
||||
NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE="" \
|
||||
BIGEVENTS_PATTERN=NONE \
|
||||
NOTIFY_DUE_DAYS_BEFORE_AND_AFTER="" \
|
||||
NOTIFY_DUE_AT_HOUR_OF_DAY="" \
|
||||
EMAIL_NOTIFICATION_TIMEOUT=30000 \
|
||||
@@ -36,6 +39,7 @@ ENV \
|
||||
TRUSTED_URL="" \
|
||||
WEBHOOKS_ATTRIBUTES="" \
|
||||
OAUTH2_ENABLED=false \
|
||||
OAUTH2_ADFS_ENABLED=false \
|
||||
OAUTH2_LOGIN_STYLE=redirect \
|
||||
OAUTH2_CLIENT_ID="" \
|
||||
OAUTH2_SECRET="" \
|
||||
@@ -108,7 +112,8 @@ ENV \
|
||||
CORS="" \
|
||||
CORS_ALLOW_HEADERS="" \
|
||||
CORS_EXPOSE_HEADERS="" \
|
||||
DEFAULT_AUTHENTICATION_METHOD=""
|
||||
DEFAULT_AUTHENTICATION_METHOD="" \
|
||||
PASSWORD_LOGIN_ENABLED=true
|
||||
|
||||
# Install OS
|
||||
RUN set -o xtrace \
|
||||
|
||||
+2
-1
@@ -11,6 +11,7 @@
|
||||
"browser": true,
|
||||
"meteor": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
@@ -44,7 +45,7 @@
|
||||
"no-spaced-func": 2,
|
||||
"no-trailing-spaces": 2,
|
||||
"operator-linebreak": 2,
|
||||
"quotes": [2, "single"],
|
||||
"quotes": [2, "single", { "avoidEscape": true }],
|
||||
"semi-spacing": 2,
|
||||
"space-unary-ops": 2,
|
||||
"arrow-spacing": 2,
|
||||
|
||||
@@ -65,9 +65,9 @@ apps:
|
||||
|
||||
parts:
|
||||
mongodb:
|
||||
source: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.2.22.tgz
|
||||
source: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-4.2.6.tgz
|
||||
plugin: dump
|
||||
stage-packages: [libssl1.0.0]
|
||||
stage-packages: [libssl1.0.0, libcurl3]
|
||||
filesets:
|
||||
mongo:
|
||||
- usr
|
||||
@@ -81,19 +81,20 @@ parts:
|
||||
wekan:
|
||||
source: .
|
||||
plugin: nodejs
|
||||
node-engine: 8.17.0
|
||||
node-engine: 12.18.3
|
||||
node-packages:
|
||||
- node-gyp
|
||||
- node-pre-gyp
|
||||
- fibers@2.0.0
|
||||
- fibers
|
||||
build-packages:
|
||||
- ca-certificates
|
||||
- apt-utils
|
||||
- python
|
||||
# - python3
|
||||
- python3
|
||||
- g++
|
||||
- capnproto
|
||||
- curl
|
||||
- libcurl3
|
||||
- execstack
|
||||
- nodejs
|
||||
- npm
|
||||
@@ -104,6 +105,18 @@ parts:
|
||||
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
|
||||
Executable
+198
@@ -0,0 +1,198 @@
|
||||
#!/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.18.2
|
||||
#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.18.2
|
||||
#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
|
||||
@@ -83,7 +83,7 @@ parts:
|
||||
wekan:
|
||||
source: .
|
||||
plugin: nodejs
|
||||
node-engine: 12.14.1
|
||||
node-engine: 12.18.3
|
||||
node-packages:
|
||||
- node-gyp
|
||||
- node-pre-gyp
|
||||
@@ -0,0 +1,62 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '0 16 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Override automatic language detection by changing the below list
|
||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||
language: ['javascript', 'python']
|
||||
# Learn more...
|
||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
FROM gitpod/workspace-mongodb
|
||||
|
||||
USER gitpod
|
||||
|
||||
# Install custom tools, runtime, etc. using apt-get
|
||||
# For example, the command below would install "bastet" - a command line tetris clone:
|
||||
#
|
||||
# RUN sudo apt-get -q update && # sudo apt-get install -yq bastet && # sudo rm -rf /var/lib/apt/lists/*
|
||||
#
|
||||
# More information: https://www.gitpod.io/docs/config-docker/
|
||||
@@ -0,0 +1,4 @@
|
||||
tasks:
|
||||
- init: npm install
|
||||
image:
|
||||
file: .gitpod.Dockerfile
|
||||
+6
-7
@@ -6,7 +6,7 @@
|
||||
meteor-base@1.4.0
|
||||
|
||||
# Build system
|
||||
ecmascript@0.14.2
|
||||
ecmascript@0.14.3
|
||||
standard-minifier-css@1.6.0
|
||||
standard-minifier-js@2.6.0
|
||||
mquandalle:jade
|
||||
@@ -23,7 +23,7 @@ dburles:collection-helpers
|
||||
idmontie:migrations
|
||||
matb33:collection-hooks
|
||||
matteodem:easy-search
|
||||
mongo@1.9.0
|
||||
mongo@1.10.0
|
||||
mquandalle:collection-mutations
|
||||
|
||||
# Account system
|
||||
@@ -70,20 +70,18 @@ rajit:bootstrap3-datepicker
|
||||
shell-server@0.5.0
|
||||
simple:rest-accounts-password
|
||||
useraccounts:core
|
||||
email@1.2.3
|
||||
email@2.0.0
|
||||
horka:swipebox
|
||||
dynamic-import@0.5.1
|
||||
dynamic-import@0.5.2
|
||||
staringatlights:fast-render
|
||||
|
||||
accounts-password@1.6.0
|
||||
accounts-password@1.6.2
|
||||
cfs:gridfs
|
||||
rzymek:fullcalendar
|
||||
momentjs:moment@2.22.2
|
||||
browser-policy-framing@1.1.0
|
||||
mquandalle:moment
|
||||
msavin:usercache
|
||||
wekan-scrollbar
|
||||
mquandalle:perfect-scrollbar
|
||||
mdg:meteor-apm-agent@3.2.0-rc.0!
|
||||
# Keep stylus in 1.1.0, because building v2 takes extra 52 minutes.
|
||||
coagmano:stylus@1.1.0!
|
||||
@@ -98,3 +96,4 @@ percolate:synced-cron
|
||||
easylogic:summernote
|
||||
cfs:filesystem
|
||||
ostrio:cookies
|
||||
tmeasday:check-npm-versions
|
||||
+1
-1
@@ -1 +1 @@
|
||||
METEOR@1.10.1
|
||||
METEOR@1.11
|
||||
+20
-21
@@ -1,7 +1,7 @@
|
||||
3stack:presence@1.1.2
|
||||
accounts-base@1.6.0
|
||||
accounts-base@1.7.0
|
||||
accounts-oauth@1.2.0
|
||||
accounts-password@1.6.0
|
||||
accounts-password@1.6.2
|
||||
aldeed:collection2@2.10.0
|
||||
aldeed:collection2-core@1.2.0
|
||||
aldeed:schema-deny@1.1.0
|
||||
@@ -17,7 +17,7 @@ base64@1.0.12
|
||||
binary-heap@1.0.11
|
||||
blaze@2.3.4
|
||||
blaze-tools@1.0.10
|
||||
boilerplate-generator@1.7.0
|
||||
boilerplate-generator@1.7.1
|
||||
browser-policy-common@1.0.11
|
||||
browser-policy-framing@1.1.0
|
||||
caching-compiler@1.2.2
|
||||
@@ -51,18 +51,18 @@ dburles:collection-helpers@1.1.0
|
||||
ddp@1.4.0
|
||||
ddp-client@2.3.3
|
||||
ddp-common@1.4.0
|
||||
ddp-rate-limiter@1.0.7
|
||||
ddp-server@2.3.1
|
||||
ddp-rate-limiter@1.0.9
|
||||
ddp-server@2.3.2
|
||||
deps@1.0.12
|
||||
diff-sequence@1.1.1
|
||||
dynamic-import@0.5.2
|
||||
easylogic:summernote@0.8.8
|
||||
ecmascript@0.14.3
|
||||
ecmascript-runtime@0.7.0
|
||||
ecmascript-runtime-client@0.10.0
|
||||
ecmascript-runtime-server@0.9.0
|
||||
ecmascript-runtime-client@0.11.0
|
||||
ecmascript-runtime-server@0.10.0
|
||||
ejson@1.1.1
|
||||
email@1.2.3
|
||||
email@2.0.0
|
||||
es5-shim@4.8.0
|
||||
fastclick@1.0.13
|
||||
fetch@0.1.1
|
||||
@@ -91,7 +91,7 @@ logging@1.1.20
|
||||
lucasantoniassi:accounts-lockout@1.0.0
|
||||
matb33:collection-hooks@0.9.1
|
||||
matteodem:easy-search@1.6.4
|
||||
mdg:meteor-apm-agent@3.2.5
|
||||
mdg:meteor-apm-agent@3.3.0
|
||||
mdg:validation-error@0.5.1
|
||||
meteor@1.9.3
|
||||
meteor-base@1.4.0
|
||||
@@ -101,16 +101,16 @@ meteorhacks:collection-utils@1.2.0
|
||||
meteorhacks:picker@1.0.3
|
||||
meteorhacks:subs-manager@1.6.4
|
||||
meteorspark:util@0.2.0
|
||||
minifier-css@1.5.0
|
||||
minifier-css@1.5.3
|
||||
minifier-js@2.6.0
|
||||
minifiers@1.1.8-faster-rebuild.0
|
||||
minimongo@1.5.0
|
||||
minimongo@1.6.0
|
||||
mobile-status-bar@1.1.0
|
||||
modern-browsers@0.1.5
|
||||
modules@0.15.0
|
||||
modules-runtime@0.12.0
|
||||
momentjs:moment@2.24.0
|
||||
mongo@1.9.1
|
||||
momentjs:moment@2.27.0
|
||||
mongo@1.10.0
|
||||
mongo-decimal@0.1.1
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.7
|
||||
@@ -124,10 +124,9 @@ mquandalle:jquery-textcomplete@0.8.0_1
|
||||
mquandalle:jquery-ui-drag-drop-sort@0.2.0
|
||||
mquandalle:moment@1.0.1
|
||||
mquandalle:mousetrap-bindglobal@0.0.1
|
||||
mquandalle:perfect-scrollbar@0.6.5_2
|
||||
msavin:usercache@1.8.0
|
||||
npm-bcrypt@0.9.3
|
||||
npm-mongo@3.7.0
|
||||
npm-mongo@3.8.1
|
||||
oauth@1.3.0
|
||||
oauth2@1.3.0
|
||||
observe-sequence@1.0.16
|
||||
@@ -162,14 +161,14 @@ simple:json-routes@2.1.0
|
||||
simple:rest-accounts-password@1.1.2
|
||||
simple:rest-bearer-token-parser@1.0.1
|
||||
simple:rest-json-error-handler@1.0.1
|
||||
socket-stream-client@0.2.3
|
||||
socket-stream-client@0.3.1
|
||||
softwarerero:accounts-t9n@1.3.11
|
||||
spacebars@1.0.15
|
||||
spacebars-compiler@1.1.3
|
||||
srp@1.0.12
|
||||
srp@1.1.0
|
||||
standard-minifier-css@1.6.0
|
||||
standard-minifier-js@2.6.0
|
||||
staringatlights:fast-render@3.2.0
|
||||
staringatlights:fast-render@3.3.0
|
||||
staringatlights:inject-data@2.3.0
|
||||
tap:i18n@1.8.2
|
||||
templates:tabs@2.3.0
|
||||
@@ -177,11 +176,12 @@ templating@1.3.2
|
||||
templating-compiler@1.3.3
|
||||
templating-runtime@1.3.2
|
||||
templating-tools@1.1.2
|
||||
tmeasday:check-npm-versions@0.3.2
|
||||
tracker@1.2.0
|
||||
twbs:bootstrap@3.3.6
|
||||
ui@1.0.13
|
||||
underscore@1.0.10
|
||||
url@1.2.0
|
||||
url@1.3.1
|
||||
useraccounts:core@1.14.2
|
||||
useraccounts:flow-routing@1.14.2
|
||||
useraccounts:unstyled@1.14.2
|
||||
@@ -191,8 +191,7 @@ webapp-hashing@1.0.9
|
||||
wekan-accounts-cas@0.1.0
|
||||
wekan-accounts-oidc@1.0.10
|
||||
wekan-ldap@0.0.2
|
||||
wekan-markdown@1.0.7
|
||||
wekan-markdown@1.0.9
|
||||
wekan-oidc@1.0.12
|
||||
wekan-scrollbar@3.1.3
|
||||
yasaricli:slugify@0.0.7
|
||||
zimme:active-route@2.3.2
|
||||
@@ -1,20 +0,0 @@
|
||||
# This file contains information which helps Meteor properly upgrade your
|
||||
# app when you run 'meteor update'. You should check it into version control
|
||||
# with your project.
|
||||
|
||||
notices-for-0.9.0
|
||||
notices-for-0.9.1
|
||||
0.9.4-platform-file
|
||||
notices-for-facebook-graph-api-2
|
||||
1.2.0-standard-minifiers-package
|
||||
1.2.0-meteor-platform-split
|
||||
1.2.0-cordova-changes
|
||||
1.2.0-breaking-changes
|
||||
1.3.0-split-minifiers-package
|
||||
1.3.5-remove-old-dev-bundle-link
|
||||
1.4.0-remove-old-dev-bundle-link
|
||||
1.4.1-add-shell-server-package
|
||||
1.4.3-split-account-service-packages
|
||||
1.5-add-dynamic-import-package
|
||||
1.7-split-underscore-from-meteor-base
|
||||
1.8.3-split-jquery-from-blaze
|
||||
@@ -1,2 +0,0 @@
|
||||
dev_bundle
|
||||
local
|
||||
@@ -1,7 +0,0 @@
|
||||
# This file contains a token that is unique to your project.
|
||||
# Check it into your repository along with the rest of this directory.
|
||||
# It can be used for purposes such as:
|
||||
# - ensuring you don't accidentally deploy one app on top of another
|
||||
# - providing package authors with aggregated statistics
|
||||
|
||||
dvyihgykyzec6y1dpg
|
||||
Whitespace-only changes.
@@ -1,100 +0,0 @@
|
||||
# Meteor packages used by this project, one per line.
|
||||
#
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
meteor-base@1.4.0
|
||||
|
||||
# Build system
|
||||
ecmascript@0.13.2
|
||||
standard-minifier-css@1.5.4
|
||||
standard-minifier-js@2.5.2
|
||||
mquandalle:jade
|
||||
|
||||
# Polyfills
|
||||
es5-shim@4.8.0
|
||||
|
||||
# Collections
|
||||
aldeed:collection2
|
||||
cfs:standard-packages
|
||||
cottz:publish-relations
|
||||
dburles:collection-helpers
|
||||
idmontie:migrations
|
||||
matb33:collection-hooks
|
||||
matteodem:easy-search
|
||||
mongo@1.7.0
|
||||
mquandalle:collection-mutations
|
||||
|
||||
# Account system
|
||||
kenton:accounts-sandstorm
|
||||
service-configuration@1.0.11
|
||||
useraccounts:unstyled
|
||||
useraccounts:flow-routing
|
||||
wekan-ldap
|
||||
wekan-accounts-cas
|
||||
wekan-accounts-oidc
|
||||
|
||||
# Utilities
|
||||
check@1.3.1
|
||||
jquery@1.11.10
|
||||
random@1.1.0
|
||||
reactive-dict@1.3.0
|
||||
session@1.2.0
|
||||
tracker@1.2.0
|
||||
underscore@1.0.10
|
||||
3stack:presence
|
||||
alethes:pages
|
||||
arillo:flow-router-helpers
|
||||
audit-argument-checks@1.0.7
|
||||
kadira:blaze-layout
|
||||
kadira:dochead
|
||||
mquandalle:autofocus
|
||||
ongoworks:speakingurl
|
||||
raix:handlebar-helpers
|
||||
tap:i18n
|
||||
http@1.4.2
|
||||
|
||||
# UI components
|
||||
blaze
|
||||
reactive-var@1.0.11
|
||||
fortawesome:fontawesome
|
||||
mousetrap:mousetrap
|
||||
mquandalle:jquery-textcomplete
|
||||
mquandalle:jquery-ui-drag-drop-sort
|
||||
mquandalle:mousetrap-bindglobal
|
||||
peerlibrary:blaze-components@=0.15.1
|
||||
templates:tabs
|
||||
verron:autosize
|
||||
simple:json-routes
|
||||
rajit:bootstrap3-datepicker
|
||||
shell-server@0.4.0
|
||||
simple:rest-accounts-password
|
||||
useraccounts:core
|
||||
email@1.2.3
|
||||
horka:swipebox
|
||||
dynamic-import@0.5.1
|
||||
staringatlights:fast-render
|
||||
|
||||
accounts-password@1.5.2
|
||||
cfs:gridfs
|
||||
rzymek:fullcalendar
|
||||
momentjs:moment@2.22.2
|
||||
browser-policy-framing@1.1.0
|
||||
mquandalle:moment
|
||||
msavin:usercache
|
||||
wekan-scrollbar
|
||||
mquandalle:perfect-scrollbar
|
||||
mdg:meteor-apm-agent@3.2.0-rc.0!
|
||||
# Keep stylus in 1.1.0, because building v2 takes extra 52 minutes.
|
||||
coagmano:stylus@1.1.0!
|
||||
lucasantoniassi:accounts-lockout
|
||||
meteorhacks:subs-manager
|
||||
meteorhacks:picker
|
||||
lamhieu:unblock
|
||||
meteorhacks:aggregate@1.3.0
|
||||
wekan-markdown
|
||||
konecty:mongo-counter
|
||||
percolate:synced-cron
|
||||
easylogic:summernote
|
||||
cfs:filesystem
|
||||
ostrio:cookies
|
||||
@@ -1,2 +0,0 @@
|
||||
server
|
||||
browser
|
||||
@@ -1 +0,0 @@
|
||||
METEOR@1.8.3
|
||||
@@ -1,198 +0,0 @@
|
||||
3stack:presence@1.1.2
|
||||
accounts-base@1.4.5
|
||||
accounts-oauth@1.1.16
|
||||
accounts-password@1.5.2
|
||||
aldeed:collection2@2.10.0
|
||||
aldeed:collection2-core@1.2.0
|
||||
aldeed:schema-deny@1.1.0
|
||||
aldeed:schema-index@1.1.1
|
||||
aldeed:simple-schema@1.5.4
|
||||
alethes:pages@1.8.6
|
||||
allow-deny@1.1.0
|
||||
arillo:flow-router-helpers@0.5.2
|
||||
audit-argument-checks@1.0.7
|
||||
autoupdate@1.6.0
|
||||
babel-compiler@7.4.2
|
||||
babel-runtime@1.4.0
|
||||
base64@1.0.12
|
||||
binary-heap@1.0.11
|
||||
blaze@2.3.4
|
||||
blaze-tools@1.0.10
|
||||
boilerplate-generator@1.6.0
|
||||
browser-policy-common@1.0.11
|
||||
browser-policy-framing@1.1.0
|
||||
caching-compiler@1.2.1
|
||||
caching-html-compiler@1.1.3
|
||||
callback-hook@1.2.0
|
||||
cfs:access-point@0.1.49
|
||||
cfs:base-package@0.0.30
|
||||
cfs:collection@0.5.5
|
||||
cfs:collection-filters@0.2.4
|
||||
cfs:data-man@0.0.6
|
||||
cfs:file@0.1.17
|
||||
cfs:filesystem@0.1.2
|
||||
cfs:gridfs@0.0.34
|
||||
cfs:http-methods@0.0.32
|
||||
cfs:http-publish@0.0.13
|
||||
cfs:power-queue@0.9.11
|
||||
cfs:reactive-list@0.0.9
|
||||
cfs:reactive-property@0.0.4
|
||||
cfs:standard-packages@0.5.10
|
||||
cfs:storage-adapter@0.2.4
|
||||
cfs:tempstore@0.1.6
|
||||
cfs:upload-http@0.0.20
|
||||
cfs:worker@0.1.5
|
||||
check@1.3.1
|
||||
chuangbo:cookie@1.1.0
|
||||
coagmano:stylus@1.1.0
|
||||
coffeescript@1.0.17
|
||||
cottz:publish-relations@2.0.8
|
||||
dburles:collection-helpers@1.1.0
|
||||
ddp@1.4.0
|
||||
ddp-client@2.3.3
|
||||
ddp-common@1.4.0
|
||||
ddp-rate-limiter@1.0.7
|
||||
ddp-server@2.3.0
|
||||
deps@1.0.12
|
||||
diff-sequence@1.1.1
|
||||
dynamic-import@0.5.1
|
||||
easylogic:summernote@0.8.8
|
||||
ecmascript@0.13.2
|
||||
ecmascript-runtime@0.7.0
|
||||
ecmascript-runtime-client@0.9.0
|
||||
ecmascript-runtime-server@0.8.0
|
||||
ejson@1.1.1
|
||||
email@1.2.3
|
||||
es5-shim@4.8.0
|
||||
fastclick@1.0.13
|
||||
fetch@0.1.1
|
||||
fortawesome:fontawesome@4.7.0
|
||||
geojson-utils@1.0.10
|
||||
horka:swipebox@1.0.2
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.0.11
|
||||
htmljs@1.0.11
|
||||
http@1.4.2
|
||||
id-map@1.1.0
|
||||
idmontie:migrations@1.0.3
|
||||
inter-process-messaging@0.1.0
|
||||
jquery@1.11.11
|
||||
kadira:blaze-layout@2.3.0
|
||||
kadira:dochead@1.5.0
|
||||
kadira:flow-router@2.12.1
|
||||
kenton:accounts-sandstorm@0.7.0
|
||||
konecty:mongo-counter@0.0.5_3
|
||||
lamhieu:meteorx@2.1.1
|
||||
lamhieu:unblock@1.0.0
|
||||
launch-screen@1.1.1
|
||||
livedata@1.0.18
|
||||
localstorage@1.2.0
|
||||
logging@1.1.20
|
||||
lucasantoniassi:accounts-lockout@1.0.0
|
||||
matb33:collection-hooks@0.9.1
|
||||
matteodem:easy-search@1.6.4
|
||||
mdg:meteor-apm-agent@3.2.5
|
||||
mdg:validation-error@0.5.1
|
||||
meteor@1.9.3
|
||||
meteor-base@1.4.0
|
||||
meteor-platform@1.2.6
|
||||
meteorhacks:aggregate@1.3.0
|
||||
meteorhacks:collection-utils@1.2.0
|
||||
meteorhacks:picker@1.0.3
|
||||
meteorhacks:subs-manager@1.6.4
|
||||
meteorspark:util@0.2.0
|
||||
minifier-css@1.4.3
|
||||
minifier-js@2.5.1
|
||||
minifiers@1.1.8-faster-rebuild.0
|
||||
minimongo@1.4.5
|
||||
mobile-status-bar@1.0.14
|
||||
modern-browsers@0.1.4
|
||||
modules@0.14.0
|
||||
modules-runtime@0.11.0
|
||||
momentjs:moment@2.24.0
|
||||
mongo@1.7.0
|
||||
mongo-decimal@0.1.1
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.7
|
||||
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:jquery-ui-drag-drop-sort@0.2.0
|
||||
mquandalle:moment@1.0.1
|
||||
mquandalle:mousetrap-bindglobal@0.0.1
|
||||
mquandalle:perfect-scrollbar@0.6.5_2
|
||||
msavin:usercache@1.8.0
|
||||
npm-bcrypt@0.9.3
|
||||
npm-mongo@3.2.0
|
||||
oauth@1.2.8
|
||||
oauth2@1.2.1
|
||||
observe-sequence@1.0.16
|
||||
ongoworks:speakingurl@1.1.0
|
||||
ordered-dict@1.1.0
|
||||
ostrio:cookies@2.5.0
|
||||
peerlibrary:assert@0.3.0
|
||||
peerlibrary:base-component@0.16.0
|
||||
peerlibrary:blaze-components@0.15.1
|
||||
peerlibrary:computed-field@0.10.0
|
||||
peerlibrary:reactive-field@0.6.0
|
||||
percolate:synced-cron@1.3.2
|
||||
promise@0.11.2
|
||||
raix:eventemitter@0.1.3
|
||||
raix:handlebar-helpers@0.2.5
|
||||
rajit:bootstrap3-datepicker@1.7.1_1
|
||||
random@1.1.0
|
||||
rate-limit@1.0.9
|
||||
reactive-dict@1.3.0
|
||||
reactive-var@1.0.11
|
||||
reload@1.3.0
|
||||
retry@1.1.0
|
||||
routepolicy@1.1.0
|
||||
rzymek:fullcalendar@3.8.0
|
||||
server-render@0.3.1
|
||||
service-configuration@1.0.11
|
||||
session@1.2.0
|
||||
sha@1.0.9
|
||||
shell-server@0.4.0
|
||||
simple:authenticate-user-by-token@1.0.1
|
||||
simple:json-routes@2.1.0
|
||||
simple:rest-accounts-password@1.1.2
|
||||
simple:rest-bearer-token-parser@1.0.1
|
||||
simple:rest-json-error-handler@1.0.1
|
||||
socket-stream-client@0.2.2
|
||||
softwarerero:accounts-t9n@1.3.11
|
||||
spacebars@1.0.15
|
||||
spacebars-compiler@1.1.3
|
||||
srp@1.0.12
|
||||
standard-minifier-css@1.5.4
|
||||
standard-minifier-js@2.5.2
|
||||
staringatlights:fast-render@3.2.0
|
||||
staringatlights:inject-data@2.3.0
|
||||
tap:i18n@1.8.2
|
||||
templates:tabs@2.3.0
|
||||
templating@1.3.2
|
||||
templating-compiler@1.3.3
|
||||
templating-runtime@1.3.2
|
||||
templating-tools@1.1.2
|
||||
tracker@1.2.0
|
||||
twbs:bootstrap@3.3.6
|
||||
ui@1.0.13
|
||||
underscore@1.0.10
|
||||
url@1.2.0
|
||||
useraccounts:core@1.14.2
|
||||
useraccounts:flow-routing@1.14.2
|
||||
useraccounts:unstyled@1.14.2
|
||||
verron:autosize@3.0.8
|
||||
webapp@1.7.5
|
||||
webapp-hashing@1.0.9
|
||||
wekan-accounts-cas@0.1.0
|
||||
wekan-accounts-oidc@1.0.10
|
||||
wekan-ldap@0.0.2
|
||||
wekan-markdown@1.0.7
|
||||
wekan-oidc@1.0.12
|
||||
wekan-scrollbar@3.1.3
|
||||
yasaricli:slugify@0.0.7
|
||||
zimme:active-route@2.3.2
|
||||
@@ -1,914 +0,0 @@
|
||||
(function () {
|
||||
|
||||
/* Imports */
|
||||
var Meteor = Package.meteor.Meteor;
|
||||
var global = Package.meteor.global;
|
||||
var meteorEnv = Package.meteor.meteorEnv;
|
||||
var FS = Package['cfs:base-package'].FS;
|
||||
var check = Package.check.check;
|
||||
var Match = Package.check.Match;
|
||||
var EJSON = Package.ejson.EJSON;
|
||||
var HTTP = Package['cfs:http-methods'].HTTP;
|
||||
|
||||
/* Package-scope variables */
|
||||
var rootUrlPathPrefix, baseUrl, getHeaders, getHeadersByCollection, _existingMountPoints, mountUrls;
|
||||
|
||||
(function(){
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// //
|
||||
// packages/cfs_access-point/packages/cfs_access-point.js //
|
||||
// //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
(function () {
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// //
|
||||
// packages/cfs:access-point/access-point-common.js //
|
||||
// //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; // 1
|
||||
// Adjust the rootUrlPathPrefix if necessary // 2
|
||||
if (rootUrlPathPrefix.length > 0) { // 3
|
||||
if (rootUrlPathPrefix.slice(0, 1) !== '/') { // 4
|
||||
rootUrlPathPrefix = '/' + rootUrlPathPrefix; // 5
|
||||
} // 6
|
||||
if (rootUrlPathPrefix.slice(-1) === '/') { // 7
|
||||
rootUrlPathPrefix = rootUrlPathPrefix.slice(0, -1); // 8
|
||||
} // 9
|
||||
} // 10
|
||||
// 11
|
||||
// prepend ROOT_URL when isCordova // 12
|
||||
if (Meteor.isCordova) { // 13
|
||||
rootUrlPathPrefix = Meteor.absoluteUrl(rootUrlPathPrefix.replace(/^\/+/, '')).replace(/\/+$/, ''); // 14
|
||||
} // 15
|
||||
// 16
|
||||
baseUrl = '/cfs'; // 17
|
||||
FS.HTTP = FS.HTTP || {}; // 18
|
||||
// 19
|
||||
// Note the upload URL so that client uploader packages know what it is // 20
|
||||
FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 21
|
||||
// 22
|
||||
/** // 23
|
||||
* @method FS.HTTP.setBaseUrl // 24
|
||||
* @public // 25
|
||||
* @param {String} newBaseUrl - Change the base URL for the HTTP GET and DELETE endpoints. // 26
|
||||
* @returns {undefined} // 27
|
||||
*/ // 28
|
||||
FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) { // 29
|
||||
// 30
|
||||
// Adjust the baseUrl if necessary // 31
|
||||
if (newBaseUrl.slice(0, 1) !== '/') { // 32
|
||||
newBaseUrl = '/' + newBaseUrl; // 33
|
||||
} // 34
|
||||
if (newBaseUrl.slice(-1) === '/') { // 35
|
||||
newBaseUrl = newBaseUrl.slice(0, -1); // 36
|
||||
} // 37
|
||||
// 38
|
||||
// Update the base URL // 39
|
||||
baseUrl = newBaseUrl; // 40
|
||||
// 41
|
||||
// Change the upload URL so that client uploader packages know what it is // 42
|
||||
FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 43
|
||||
// 44
|
||||
// Remount URLs with the new baseUrl, unmounting the old, on the server only. // 45
|
||||
// If existingMountPoints is empty, then we haven't run the server startup // 46
|
||||
// code yet, so this new URL will be used at that point for the initial mount. // 47
|
||||
if (Meteor.isServer && !FS.Utility.isEmpty(_existingMountPoints)) { // 48
|
||||
mountUrls(); // 49
|
||||
} // 50
|
||||
}; // 51
|
||||
// 52
|
||||
/* // 53
|
||||
* FS.File extensions // 54
|
||||
*/ // 55
|
||||
// 56
|
||||
/** // 57
|
||||
* @method FS.File.prototype.url Construct the file url // 58
|
||||
* @public // 59
|
||||
* @param {Object} [options] // 60
|
||||
* @param {String} [options.store] Name of the store to get from. If not defined, the first store defined in `options.stores` for the collection on the client is used.
|
||||
* @param {Boolean} [options.auth=null] Add authentication token to the URL query string? By default, a token for the current logged in user is added on the client. Set this to `false` to omit the token. Set this to a string to provide your own token. Set this to a number to specify an expiration time for the token in seconds.
|
||||
* @param {Boolean} [options.download=false] Should headers be set to force a download? Typically this means that clicking the link with this URL will download the file to the user's Downloads folder instead of displaying the file in the browser.
|
||||
* @param {Boolean} [options.brokenIsFine=false] Return the URL even if we know it's currently a broken link because the file hasn't been saved in the requested store yet.
|
||||
* @param {Boolean} [options.metadata=false] Return the URL for the file metadata access point rather than the file itself.
|
||||
* @param {String} [options.uploading=null] A URL to return while the file is being uploaded. // 66
|
||||
* @param {String} [options.storing=null] A URL to return while the file is being stored. // 67
|
||||
* @param {String} [options.filename=null] Override the filename that should appear at the end of the URL. By default it is the name of the file in the requested store.
|
||||
* // 69
|
||||
* Returns the HTTP URL for getting the file or its metadata. // 70
|
||||
*/ // 71
|
||||
FS.File.prototype.url = function(options) { // 72
|
||||
var self = this; // 73
|
||||
options = options || {}; // 74
|
||||
options = FS.Utility.extend({ // 75
|
||||
store: null, // 76
|
||||
auth: null, // 77
|
||||
download: false, // 78
|
||||
metadata: false, // 79
|
||||
brokenIsFine: false, // 80
|
||||
uploading: null, // return this URL while uploading // 81
|
||||
storing: null, // return this URL while storing // 82
|
||||
filename: null // override the filename that is shown to the user // 83
|
||||
}, options.hash || options); // check for "hash" prop if called as helper // 84
|
||||
// 85
|
||||
// Primarily useful for displaying a temporary image while uploading an image // 86
|
||||
if (options.uploading && !self.isUploaded()) { // 87
|
||||
return options.uploading; // 88
|
||||
} // 89
|
||||
// 90
|
||||
if (self.isMounted()) { // 91
|
||||
// See if we've stored in the requested store yet // 92
|
||||
var storeName = options.store || self.collection.primaryStore.name; // 93
|
||||
if (!self.hasStored(storeName)) { // 94
|
||||
if (options.storing) { // 95
|
||||
return options.storing; // 96
|
||||
} else if (!options.brokenIsFine) { // 97
|
||||
// We want to return null if we know the URL will be a broken // 98
|
||||
// link because then we can avoid rendering broken links, broken // 99
|
||||
// images, etc. // 100
|
||||
return null; // 101
|
||||
} // 102
|
||||
} // 103
|
||||
// 104
|
||||
// Add filename to end of URL if we can determine one // 105
|
||||
var filename = options.filename || self.name({store: storeName}); // 106
|
||||
if (typeof filename === "string" && filename.length) { // 107
|
||||
filename = '/' + filename; // 108
|
||||
} else { // 109
|
||||
filename = ''; // 110
|
||||
} // 111
|
||||
// 112
|
||||
// TODO: Could we somehow figure out if the collection requires login? // 113
|
||||
var authToken = ''; // 114
|
||||
if (Meteor.isClient && typeof Accounts !== "undefined" && typeof Accounts._storedLoginToken === "function") { // 115
|
||||
if (options.auth !== false) { // 116
|
||||
// Add reactive deps on the user // 117
|
||||
Meteor.userId(); // 118
|
||||
// 119
|
||||
var authObject = { // 120
|
||||
authToken: Accounts._storedLoginToken() || '' // 121
|
||||
}; // 122
|
||||
// 123
|
||||
// If it's a number, we use that as the expiration time (in seconds) // 124
|
||||
if (options.auth === +options.auth) { // 125
|
||||
authObject.expiration = FS.HTTP.now() + options.auth * 1000; // 126
|
||||
} // 127
|
||||
// 128
|
||||
// Set the authToken // 129
|
||||
var authString = JSON.stringify(authObject); // 130
|
||||
authToken = FS.Utility.btoa(authString); // 131
|
||||
} // 132
|
||||
} else if (typeof options.auth === "string") { // 133
|
||||
// If the user supplies auth token the user will be responsible for // 134
|
||||
// updating // 135
|
||||
authToken = options.auth; // 136
|
||||
} // 137
|
||||
// 138
|
||||
// Construct query string // 139
|
||||
var params = {}; // 140
|
||||
if (authToken !== '') { // 141
|
||||
params.token = authToken; // 142
|
||||
} // 143
|
||||
if (options.download) { // 144
|
||||
params.download = true; // 145
|
||||
} // 146
|
||||
if (options.store) { // 147
|
||||
// We use options.store here instead of storeName because we want to omit the queryString // 148
|
||||
// whenever possible, allowing users to have "clean" URLs if they want. The server will // 149
|
||||
// assume the first store defined on the server, which means that we are assuming that // 150
|
||||
// the first on the client is also the first on the server. If that's not the case, the // 151
|
||||
// store option should be supplied. // 152
|
||||
params.store = options.store; // 153
|
||||
} // 154
|
||||
var queryString = FS.Utility.encodeParams(params); // 155
|
||||
if (queryString.length) { // 156
|
||||
queryString = '?' + queryString; // 157
|
||||
} // 158
|
||||
// 159
|
||||
// Determine which URL to use // 160
|
||||
var area; // 161
|
||||
if (options.metadata) { // 162
|
||||
area = '/record'; // 163
|
||||
} else { // 164
|
||||
area = '/files'; // 165
|
||||
} // 166
|
||||
// 167
|
||||
// Construct and return the http method url // 168
|
||||
return rootUrlPathPrefix + baseUrl + area + '/' + self.collection.name + '/' + self._id + filename + queryString; // 169
|
||||
} // 170
|
||||
// 171
|
||||
}; // 172
|
||||
// 173
|
||||
// 174
|
||||
// 175
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
}).call(this);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(function () {
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// //
|
||||
// packages/cfs:access-point/access-point-handlers.js //
|
||||
// //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
getHeaders = []; // 1
|
||||
getHeadersByCollection = {}; // 2
|
||||
// 3
|
||||
FS.HTTP.Handlers = {}; // 4
|
||||
// 5
|
||||
/** // 6
|
||||
* @method FS.HTTP.Handlers.Del // 7
|
||||
* @public // 8
|
||||
* @returns {any} response // 9
|
||||
* // 10
|
||||
* HTTP DEL request handler // 11
|
||||
*/ // 12
|
||||
FS.HTTP.Handlers.Del = function httpDelHandler(ref) { // 13
|
||||
var self = this; // 14
|
||||
var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 15
|
||||
// 16
|
||||
// If DELETE request, validate with 'remove' allow/deny, delete the file, and return // 17
|
||||
FS.Utility.validateAction(ref.collection.files._validators['remove'], ref.file, self.userId); // 18
|
||||
// 19
|
||||
/* // 20
|
||||
* From the DELETE spec: // 21
|
||||
* A successful response SHOULD be 200 (OK) if the response includes an // 22
|
||||
* entity describing the status, 202 (Accepted) if the action has not // 23
|
||||
* yet been enacted, or 204 (No Content) if the action has been enacted // 24
|
||||
* but the response does not include an entity. // 25
|
||||
*/ // 26
|
||||
self.setStatusCode(200); // 27
|
||||
// 28
|
||||
return { // 29
|
||||
deleted: !!ref.file.remove() // 30
|
||||
}; // 31
|
||||
}; // 32
|
||||
// 33
|
||||
/** // 34
|
||||
* @method FS.HTTP.Handlers.GetList // 35
|
||||
* @public // 36
|
||||
* @returns {Object} response // 37
|
||||
* // 38
|
||||
* HTTP GET file list request handler // 39
|
||||
*/ // 40
|
||||
FS.HTTP.Handlers.GetList = function httpGetListHandler() { // 41
|
||||
// Not Yet Implemented // 42
|
||||
// Need to check publications and return file list based on // 43
|
||||
// what user is allowed to see // 44
|
||||
}; // 45
|
||||
// 46
|
||||
/* // 47
|
||||
requestRange will parse the range set in request header - if not possible it // 48
|
||||
will throw fitting errors and autofill range for both partial and full ranges // 49
|
||||
// 50
|
||||
throws error or returns the object: // 51
|
||||
{ // 52
|
||||
start // 53
|
||||
end // 54
|
||||
length // 55
|
||||
unit // 56
|
||||
partial // 57
|
||||
} // 58
|
||||
*/ // 59
|
||||
var requestRange = function(req, fileSize) { // 60
|
||||
if (req) { // 61
|
||||
if (req.headers) { // 62
|
||||
var rangeString = req.headers.range; // 63
|
||||
// 64
|
||||
// Make sure range is a string // 65
|
||||
if (rangeString === ''+rangeString) { // 66
|
||||
// 67
|
||||
// range will be in the format "bytes=0-32767" // 68
|
||||
var parts = rangeString.split('='); // 69
|
||||
var unit = parts[0]; // 70
|
||||
// 71
|
||||
// Make sure parts consists of two strings and range is of type "byte" // 72
|
||||
if (parts.length == 2 && unit == 'bytes') { // 73
|
||||
// Parse the range // 74
|
||||
var range = parts[1].split('-'); // 75
|
||||
var start = Number(range[0]); // 76
|
||||
var end = Number(range[1]); // 77
|
||||
// 78
|
||||
// Fix invalid ranges? // 79
|
||||
if (range[0] != start) start = 0; // 80
|
||||
if (range[1] != end || !end) end = fileSize - 1; // 81
|
||||
// 82
|
||||
// Make sure range consists of a start and end point of numbers and start is less than end // 83
|
||||
if (start < end) { // 84
|
||||
// 85
|
||||
var partSize = 0 - start + end + 1; // 86
|
||||
// 87
|
||||
// Return the parsed range // 88
|
||||
return { // 89
|
||||
start: start, // 90
|
||||
end: end, // 91
|
||||
length: partSize, // 92
|
||||
size: fileSize, // 93
|
||||
unit: unit, // 94
|
||||
partial: (partSize < fileSize) // 95
|
||||
}; // 96
|
||||
// 97
|
||||
} else { // 98
|
||||
throw new Meteor.Error(416, "Requested Range Not Satisfiable"); // 99
|
||||
} // 100
|
||||
// 101
|
||||
} else { // 102
|
||||
// The first part should be bytes // 103
|
||||
throw new Meteor.Error(416, "Requested Range Unit Not Satisfiable"); // 104
|
||||
} // 105
|
||||
// 106
|
||||
} else { // 107
|
||||
// No range found // 108
|
||||
} // 109
|
||||
// 110
|
||||
} else { // 111
|
||||
// throw new Error('No request headers set for _parseRange function'); // 112
|
||||
} // 113
|
||||
} else { // 114
|
||||
throw new Error('No request object passed to _parseRange function'); // 115
|
||||
} // 116
|
||||
// 117
|
||||
return { // 118
|
||||
start: 0, // 119
|
||||
end: fileSize - 1, // 120
|
||||
length: fileSize, // 121
|
||||
size: fileSize, // 122
|
||||
unit: 'bytes', // 123
|
||||
partial: false // 124
|
||||
}; // 125
|
||||
}; // 126
|
||||
// 127
|
||||
/** // 128
|
||||
* @method FS.HTTP.Handlers.Get // 129
|
||||
* @public // 130
|
||||
* @returns {any} response // 131
|
||||
* // 132
|
||||
* HTTP GET request handler // 133
|
||||
*/ // 134
|
||||
FS.HTTP.Handlers.Get = function httpGetHandler(ref) { // 135
|
||||
var self = this; // 136
|
||||
// Once we have the file, we can test allow/deny validators // 137
|
||||
// XXX: pass on the "share" query eg. ?share=342hkjh23ggj for shared url access? // 138
|
||||
FS.Utility.validateAction(ref.collection._validators['download'], ref.file, self.userId /*, self.query.shareId*/); // 139
|
||||
// 140
|
||||
var storeName = ref.storeName; // 141
|
||||
// 142
|
||||
// If no storeName was specified, use the first defined storeName // 143
|
||||
if (typeof storeName !== "string") { // 144
|
||||
// No store handed, we default to primary store // 145
|
||||
storeName = ref.collection.primaryStore.name; // 146
|
||||
} // 147
|
||||
// 148
|
||||
// Get the storage reference // 149
|
||||
var storage = ref.collection.storesLookup[storeName]; // 150
|
||||
// 151
|
||||
if (!storage) { // 152
|
||||
throw new Meteor.Error(404, "Not Found", 'There is no store "' + storeName + '"'); // 153
|
||||
} // 154
|
||||
// 155
|
||||
// Get the file // 156
|
||||
var copyInfo = ref.file.copies[storeName]; // 157
|
||||
// 158
|
||||
if (!copyInfo) { // 159
|
||||
throw new Meteor.Error(404, "Not Found", 'This file was not stored in the ' + storeName + ' store'); // 160
|
||||
} // 161
|
||||
// 162
|
||||
// Set the content type for file // 163
|
||||
if (typeof copyInfo.type === "string") { // 164
|
||||
self.setContentType(copyInfo.type); // 165
|
||||
} else { // 166
|
||||
self.setContentType('application/octet-stream'); // 167
|
||||
} // 168
|
||||
// 169
|
||||
// Add 'Content-Disposition' header if requested a download/attachment URL // 170
|
||||
if (typeof ref.download !== "undefined") { // 171
|
||||
var filename = ref.filename || copyInfo.name; // 172
|
||||
self.addHeader('Content-Disposition', 'attachment; filename="' + filename + '"'); // 173
|
||||
} else { // 174
|
||||
self.addHeader('Content-Disposition', 'inline'); // 175
|
||||
} // 176
|
||||
// 177
|
||||
// Get the contents range from request // 178
|
||||
var range = requestRange(self.request, copyInfo.size); // 179
|
||||
// 180
|
||||
// Some browsers cope better if the content-range header is // 181
|
||||
// still included even for the full file being returned. // 182
|
||||
self.addHeader('Content-Range', range.unit + ' ' + range.start + '-' + range.end + '/' + range.size); // 183
|
||||
// 184
|
||||
// If a chunk/range was requested instead of the whole file, serve that' // 185
|
||||
if (range.partial) { // 186
|
||||
self.setStatusCode(206, 'Partial Content'); // 187
|
||||
} else { // 188
|
||||
self.setStatusCode(200, 'OK'); // 189
|
||||
} // 190
|
||||
// 191
|
||||
// Add any other global custom headers and collection-specific custom headers // 192
|
||||
FS.Utility.each(getHeaders.concat(getHeadersByCollection[ref.collection.name] || []), function(header) { // 193
|
||||
self.addHeader(header[0], header[1]); // 194
|
||||
}); // 195
|
||||
// 196
|
||||
// Inform clients about length (or chunk length in case of ranges) // 197
|
||||
self.addHeader('Content-Length', range.length); // 198
|
||||
// 199
|
||||
// Last modified header (updatedAt from file info) // 200
|
||||
self.addHeader('Last-Modified', copyInfo.updatedAt.toUTCString()); // 201
|
||||
// 202
|
||||
// Inform clients that we accept ranges for resumable chunked downloads // 203
|
||||
self.addHeader('Accept-Ranges', range.unit); // 204
|
||||
// 205
|
||||
if (FS.debug) console.log('Read file "' + (ref.filename || copyInfo.name) + '" ' + range.unit + ' ' + range.start + '-' + range.end + '/' + range.size);
|
||||
// 207
|
||||
var readStream = storage.adapter.createReadStream(ref.file, {start: range.start, end: range.end}); // 208
|
||||
// 209
|
||||
readStream.on('error', function(err) { // 210
|
||||
// Send proper error message on get error // 211
|
||||
if (err.message && err.statusCode) { // 212
|
||||
self.Error(new Meteor.Error(err.statusCode, err.message)); // 213
|
||||
} else { // 214
|
||||
self.Error(new Meteor.Error(503, 'Service unavailable')); // 215
|
||||
} // 216
|
||||
}); // 217
|
||||
// 218
|
||||
readStream.pipe(self.createWriteStream()); // 219
|
||||
}; // 220
|
||||
|
||||
const originalHandler = FS.HTTP.Handlers.Get;
|
||||
FS.HTTP.Handlers.Get = function (ref) {
|
||||
//console.log(ref.filename);
|
||||
try {
|
||||
var userAgent = (this.requestHeaders['user-agent']||'').toLowerCase();
|
||||
|
||||
if(userAgent.indexOf('msie') >= 0 || userAgent.indexOf('trident') >= 0 || userAgent.indexOf('chrome') >= 0) {
|
||||
ref.filename = encodeURIComponent(ref.filename);
|
||||
} else if(userAgent.indexOf('firefox') >= 0) {
|
||||
ref.filename = new Buffer(ref.filename).toString('binary');
|
||||
} else {
|
||||
/* safari*/
|
||||
ref.filename = new Buffer(ref.filename).toString('binary');
|
||||
}
|
||||
} catch (ex){
|
||||
ref.filename = 'tempfix';
|
||||
}
|
||||
return originalHandler.call(this, ref);
|
||||
};
|
||||
// 221
|
||||
/** // 222
|
||||
* @method FS.HTTP.Handlers.PutInsert // 223
|
||||
* @public // 224
|
||||
* @returns {Object} response object with _id property // 225
|
||||
* // 226
|
||||
* HTTP PUT file insert request handler // 227
|
||||
*/ // 228
|
||||
FS.HTTP.Handlers.PutInsert = function httpPutInsertHandler(ref) { // 229
|
||||
var self = this; // 230
|
||||
var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 231
|
||||
// 232
|
||||
FS.debug && console.log("HTTP PUT (insert) handler"); // 233
|
||||
// 234
|
||||
// Create the nice FS.File // 235
|
||||
var fileObj = new FS.File(); // 236
|
||||
// 237
|
||||
// Set its name // 238
|
||||
fileObj.name(opts.filename || null); // 239
|
||||
// 240
|
||||
// Attach the readstream as the file's data // 241
|
||||
fileObj.attachData(self.createReadStream(), {type: self.requestHeaders['content-type'] || 'application/octet-stream'});
|
||||
// 243
|
||||
// Validate with insert allow/deny // 244
|
||||
FS.Utility.validateAction(ref.collection.files._validators['insert'], fileObj, self.userId); // 245
|
||||
// 246
|
||||
// Insert file into collection, triggering readStream storage // 247
|
||||
ref.collection.insert(fileObj); // 248
|
||||
// 249
|
||||
// Send response // 250
|
||||
self.setStatusCode(200); // 251
|
||||
// 252
|
||||
// Return the new file id // 253
|
||||
return {_id: fileObj._id}; // 254
|
||||
}; // 255
|
||||
// 256
|
||||
/** // 257
|
||||
* @method FS.HTTP.Handlers.PutUpdate // 258
|
||||
* @public // 259
|
||||
* @returns {Object} response object with _id and chunk properties // 260
|
||||
* // 261
|
||||
* HTTP PUT file update chunk request handler // 262
|
||||
*/ // 263
|
||||
FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) { // 264
|
||||
var self = this; // 265
|
||||
var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 266
|
||||
// 267
|
||||
var chunk = parseInt(opts.chunk, 10); // 268
|
||||
if (isNaN(chunk)) chunk = 0; // 269
|
||||
// 270
|
||||
FS.debug && console.log("HTTP PUT (update) handler received chunk: ", chunk); // 271
|
||||
// 272
|
||||
// Validate with insert allow/deny; also mounts and retrieves the file // 273
|
||||
FS.Utility.validateAction(ref.collection.files._validators['insert'], ref.file, self.userId); // 274
|
||||
// 275
|
||||
self.createReadStream().pipe( FS.TempStore.createWriteStream(ref.file, chunk) ); // 276
|
||||
// 277
|
||||
// Send response // 278
|
||||
self.setStatusCode(200); // 279
|
||||
// 280
|
||||
return { _id: ref.file._id, chunk: chunk }; // 281
|
||||
}; // 282
|
||||
// 283
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
}).call(this);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(function () {
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// //
|
||||
// packages/cfs:access-point/access-point-server.js //
|
||||
// //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
var path = Npm.require("path"); // 1
|
||||
// 2
|
||||
HTTP.publishFormats({ // 3
|
||||
fileRecordFormat: function (input) { // 4
|
||||
// Set the method scope content type to json // 5
|
||||
this.setContentType('application/json'); // 6
|
||||
if (FS.Utility.isArray(input)) { // 7
|
||||
return EJSON.stringify(FS.Utility.map(input, function (obj) { // 8
|
||||
return FS.Utility.cloneFileRecord(obj); // 9
|
||||
})); // 10
|
||||
} else { // 11
|
||||
return EJSON.stringify(FS.Utility.cloneFileRecord(input)); // 12
|
||||
} // 13
|
||||
} // 14
|
||||
}); // 15
|
||||
// 16
|
||||
/** // 17
|
||||
* @method FS.HTTP.setHeadersForGet // 18
|
||||
* @public // 19
|
||||
* @param {Array} headers - List of headers, where each is a two-item array in which item 1 is the header name and item 2 is the header value.
|
||||
* @param {Array|String} [collections] - Which collections the headers should be added for. Omit this argument to add the header for all collections.
|
||||
* @returns {undefined} // 22
|
||||
*/ // 23
|
||||
FS.HTTP.setHeadersForGet = function setHeadersForGet(headers, collections) { // 24
|
||||
if (typeof collections === "string") { // 25
|
||||
collections = [collections]; // 26
|
||||
} // 27
|
||||
if (collections) { // 28
|
||||
FS.Utility.each(collections, function(collectionName) { // 29
|
||||
getHeadersByCollection[collectionName] = headers || []; // 30
|
||||
}); // 31
|
||||
} else { // 32
|
||||
getHeaders = headers || []; // 33
|
||||
} // 34
|
||||
}; // 35
|
||||
// 36
|
||||
/** // 37
|
||||
* @method FS.HTTP.publish // 38
|
||||
* @public // 39
|
||||
* @param {FS.Collection} collection // 40
|
||||
* @param {Function} func - Publish function that returns a cursor. // 41
|
||||
* @returns {undefined} // 42
|
||||
* // 43
|
||||
* Publishes all documents returned by the cursor at a GET URL // 44
|
||||
* with the format baseUrl/record/collectionName. The publish // 45
|
||||
* function `this` is similar to normal `Meteor.publish`. // 46
|
||||
*/ // 47
|
||||
FS.HTTP.publish = function fsHttpPublish(collection, func) { // 48
|
||||
var name = baseUrl + '/record/' + collection.name; // 49
|
||||
// Mount collection listing URL using http-publish package // 50
|
||||
HTTP.publish({ // 51
|
||||
name: name, // 52
|
||||
defaultFormat: 'fileRecordFormat', // 53
|
||||
collection: collection, // 54
|
||||
collectionGet: true, // 55
|
||||
collectionPost: false, // 56
|
||||
documentGet: true, // 57
|
||||
documentPut: false, // 58
|
||||
documentDelete: false // 59
|
||||
}, func); // 60
|
||||
// 61
|
||||
FS.debug && console.log("Registered HTTP method GET URLs:\n\n" + name + '\n' + name + '/:id\n'); // 62
|
||||
}; // 63
|
||||
// 64
|
||||
/** // 65
|
||||
* @method FS.HTTP.unpublish // 66
|
||||
* @public // 67
|
||||
* @param {FS.Collection} collection // 68
|
||||
* @returns {undefined} // 69
|
||||
* // 70
|
||||
* Unpublishes a restpoint created by a call to `FS.HTTP.publish` // 71
|
||||
*/ // 72
|
||||
FS.HTTP.unpublish = function fsHttpUnpublish(collection) { // 73
|
||||
// Mount collection listing URL using http-publish package // 74
|
||||
HTTP.unpublish(baseUrl + '/record/' + collection.name); // 75
|
||||
}; // 76
|
||||
// 77
|
||||
_existingMountPoints = {}; // 78
|
||||
// 79
|
||||
/** // 80
|
||||
* @method defaultSelectorFunction // 81
|
||||
* @private // 82
|
||||
* @returns { collection, file } // 83
|
||||
* // 84
|
||||
* This is the default selector function // 85
|
||||
*/ // 86
|
||||
var defaultSelectorFunction = function() { // 87
|
||||
var self = this; // 88
|
||||
// Selector function // 89
|
||||
// // 90
|
||||
// This function will have to return the collection and the // 91
|
||||
// file. If file not found undefined is returned - if null is returned the // 92
|
||||
// search was not possible // 93
|
||||
var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 94
|
||||
// 95
|
||||
// Get the collection name from the url // 96
|
||||
var collectionName = opts.collectionName; // 97
|
||||
// 98
|
||||
// Get the id from the url // 99
|
||||
var id = opts.id; // 100
|
||||
// 101
|
||||
// Get the collection // 102
|
||||
var collection = FS._collections[collectionName]; // 103
|
||||
// 104
|
||||
// Get the file if possible else return null // 105
|
||||
var file = (id && collection)? collection.findOne({ _id: id }): null; // 106
|
||||
// 107
|
||||
// Return the collection and the file // 108
|
||||
return { // 109
|
||||
collection: collection, // 110
|
||||
file: file, // 111
|
||||
storeName: opts.store, // 112
|
||||
download: opts.download, // 113
|
||||
filename: opts.filename // 114
|
||||
}; // 115
|
||||
}; // 116
|
||||
// 117
|
||||
/* // 118
|
||||
* @method FS.HTTP.mount // 119
|
||||
* @public // 120
|
||||
* @param {array of string} mountPoints mount points to map rest functinality on // 121
|
||||
* @param {function} selector_f [selector] function returns `{ collection, file }` for mount points to work with // 122
|
||||
* // 123
|
||||
*/ // 124
|
||||
FS.HTTP.mount = function(mountPoints, selector_f) { // 125
|
||||
// We take mount points as an array and we get a selector function // 126
|
||||
var selectorFunction = selector_f || defaultSelectorFunction; // 127
|
||||
// 128
|
||||
var accessPoint = { // 129
|
||||
'stream': true, // 130
|
||||
'auth': expirationAuth, // 131
|
||||
'post': function(data) { // 132
|
||||
// Use the selector for finding the collection and file reference // 133
|
||||
var ref = selectorFunction.call(this); // 134
|
||||
// 135
|
||||
// We dont support post - this would be normal insert eg. of filerecord? // 136
|
||||
throw new Meteor.Error(501, "Not implemented", "Post is not supported"); // 137
|
||||
}, // 138
|
||||
'put': function(data) { // 139
|
||||
// Use the selector for finding the collection and file reference // 140
|
||||
var ref = selectorFunction.call(this); // 141
|
||||
// 142
|
||||
// Make sure we have a collection reference // 143
|
||||
if (!ref.collection) // 144
|
||||
throw new Meteor.Error(404, "Not Found", "No collection found"); // 145
|
||||
// 146
|
||||
// Make sure we have a file reference // 147
|
||||
if (ref.file === null) { // 148
|
||||
// No id supplied so we will create a new FS.File instance and // 149
|
||||
// insert the supplied data. // 150
|
||||
return FS.HTTP.Handlers.PutInsert.apply(this, [ref]); // 151
|
||||
} else { // 152
|
||||
if (ref.file) { // 153
|
||||
return FS.HTTP.Handlers.PutUpdate.apply(this, [ref]); // 154
|
||||
} else { // 155
|
||||
throw new Meteor.Error(404, "Not Found", 'No file found'); // 156
|
||||
} // 157
|
||||
} // 158
|
||||
}, // 159
|
||||
'get': function(data) { // 160
|
||||
// Use the selector for finding the collection and file reference // 161
|
||||
var ref = selectorFunction.call(this); // 162
|
||||
// 163
|
||||
// Make sure we have a collection reference // 164
|
||||
if (!ref.collection) // 165
|
||||
throw new Meteor.Error(404, "Not Found", "No collection found"); // 166
|
||||
// 167
|
||||
// Make sure we have a file reference // 168
|
||||
if (ref.file === null) { // 169
|
||||
// No id supplied so we will return the published list of files ala // 170
|
||||
// http.publish in json format // 171
|
||||
return FS.HTTP.Handlers.GetList.apply(this, [ref]); // 172
|
||||
} else { // 173
|
||||
if (ref.file) { // 174
|
||||
return FS.HTTP.Handlers.Get.apply(this, [ref]); // 175
|
||||
} else { // 176
|
||||
throw new Meteor.Error(404, "Not Found", 'No file found'); // 177
|
||||
} // 178
|
||||
} // 179
|
||||
}, // 180
|
||||
'delete': function(data) { // 181
|
||||
// Use the selector for finding the collection and file reference // 182
|
||||
var ref = selectorFunction.call(this); // 183
|
||||
// 184
|
||||
// Make sure we have a collection reference // 185
|
||||
if (!ref.collection) // 186
|
||||
throw new Meteor.Error(404, "Not Found", "No collection found"); // 187
|
||||
// 188
|
||||
// Make sure we have a file reference // 189
|
||||
if (ref.file) { // 190
|
||||
return FS.HTTP.Handlers.Del.apply(this, [ref]); // 191
|
||||
} else { // 192
|
||||
throw new Meteor.Error(404, "Not Found", 'No file found'); // 193
|
||||
} // 194
|
||||
} // 195
|
||||
}; // 196
|
||||
// 197
|
||||
var accessPoints = {}; // 198
|
||||
// 199
|
||||
// Add debug message // 200
|
||||
FS.debug && console.log('Registered HTTP method URLs:'); // 201
|
||||
// 202
|
||||
FS.Utility.each(mountPoints, function(mountPoint) { // 203
|
||||
// Couple mountpoint and accesspoint // 204
|
||||
accessPoints[mountPoint] = accessPoint; // 205
|
||||
// Remember our mountpoints // 206
|
||||
_existingMountPoints[mountPoint] = mountPoint; // 207
|
||||
// Add debug message // 208
|
||||
FS.debug && console.log(mountPoint); // 209
|
||||
}); // 210
|
||||
// 211
|
||||
// XXX: HTTP:methods should unmount existing mounts in case of overwriting? // 212
|
||||
HTTP.methods(accessPoints); // 213
|
||||
// 214
|
||||
}; // 215
|
||||
// 216
|
||||
/** // 217
|
||||
* @method FS.HTTP.unmount // 218
|
||||
* @public // 219
|
||||
* @param {string | array of string} [mountPoints] Optional, if not specified all mountpoints are unmounted // 220
|
||||
* // 221
|
||||
*/ // 222
|
||||
FS.HTTP.unmount = function(mountPoints) { // 223
|
||||
// The mountPoints is optional, can be string or array if undefined then // 224
|
||||
// _existingMountPoints will be used // 225
|
||||
var unmountList; // 226
|
||||
// Container for the mount points to unmount // 227
|
||||
var unmountPoints = {}; // 228
|
||||
// 229
|
||||
if (typeof mountPoints === 'undefined') { // 230
|
||||
// Use existing mount points - unmount all // 231
|
||||
unmountList = _existingMountPoints; // 232
|
||||
} else if (mountPoints === ''+mountPoints) { // 233
|
||||
// Got a string // 234
|
||||
unmountList = [mountPoints]; // 235
|
||||
} else if (mountPoints.length) { // 236
|
||||
// Got an array // 237
|
||||
unmountList = mountPoints; // 238
|
||||
} // 239
|
||||
// 240
|
||||
// If we have a list to unmount // 241
|
||||
if (unmountList) { // 242
|
||||
// Iterate over each item // 243
|
||||
FS.Utility.each(unmountList, function(mountPoint) { // 244
|
||||
// Check _existingMountPoints to make sure the mount point exists in our // 245
|
||||
// context / was created by the FS.HTTP.mount // 246
|
||||
if (_existingMountPoints[mountPoint]) { // 247
|
||||
// Mark as unmount // 248
|
||||
unmountPoints[mountPoint] = false; // 249
|
||||
// Release // 250
|
||||
delete _existingMountPoints[mountPoint]; // 251
|
||||
} // 252
|
||||
}); // 253
|
||||
FS.debug && console.log('FS.HTTP.unmount:'); // 254
|
||||
FS.debug && console.log(unmountPoints); // 255
|
||||
// Complete unmount // 256
|
||||
HTTP.methods(unmountPoints); // 257
|
||||
} // 258
|
||||
}; // 259
|
||||
// 260
|
||||
// ### FS.Collection maps on HTTP pr. default on the following restpoints: // 261
|
||||
// * // 262
|
||||
// baseUrl + '/files/:collectionName/:id/:filename', // 263
|
||||
// baseUrl + '/files/:collectionName/:id', // 264
|
||||
// baseUrl + '/files/:collectionName' // 265
|
||||
// // 266
|
||||
// Change/ replace the existing mount point by: // 267
|
||||
// ```js // 268
|
||||
// // unmount all existing // 269
|
||||
// FS.HTTP.unmount(); // 270
|
||||
// // Create new mount point // 271
|
||||
// FS.HTTP.mount([ // 272
|
||||
// '/cfs/files/:collectionName/:id/:filename', // 273
|
||||
// '/cfs/files/:collectionName/:id', // 274
|
||||
// '/cfs/files/:collectionName' // 275
|
||||
// ]); // 276
|
||||
// ``` // 277
|
||||
// // 278
|
||||
mountUrls = function mountUrls() { // 279
|
||||
// We unmount first in case we are calling this a second time // 280
|
||||
FS.HTTP.unmount(); // 281
|
||||
// 282
|
||||
FS.HTTP.mount([ // 283
|
||||
baseUrl + '/files/:collectionName/:id/:filename', // 284
|
||||
baseUrl + '/files/:collectionName/:id', // 285
|
||||
baseUrl + '/files/:collectionName' // 286
|
||||
]); // 287
|
||||
}; // 288
|
||||
// 289
|
||||
// Returns the userId from URL token // 290
|
||||
var expirationAuth = function expirationAuth() { // 291
|
||||
var self = this; // 292
|
||||
// 293
|
||||
// Read the token from '/hello?token=base64' // 294
|
||||
var encodedToken = self.query.token; // 295
|
||||
// 296
|
||||
FS.debug && console.log("token: "+encodedToken); // 297
|
||||
// 298
|
||||
if (!encodedToken || !Meteor.users) return false; // 299
|
||||
// 300
|
||||
// Check the userToken before adding it to the db query // 301
|
||||
// Set the this.userId // 302
|
||||
var tokenString = FS.Utility.atob(encodedToken); // 303
|
||||
// 304
|
||||
var tokenObject; // 305
|
||||
try { // 306
|
||||
tokenObject = JSON.parse(tokenString); // 307
|
||||
} catch(err) { // 308
|
||||
throw new Meteor.Error(400, 'Bad Request'); // 309
|
||||
} // 310
|
||||
// 311
|
||||
// XXX: Do some check here of the object // 312
|
||||
var userToken = tokenObject.authToken; // 313
|
||||
if (userToken !== ''+userToken) { // 314
|
||||
throw new Meteor.Error(400, 'Bad Request'); // 315
|
||||
} // 316
|
||||
// 317
|
||||
// If we have an expiration token we should check that it's still valid // 318
|
||||
if (tokenObject.expiration != null) { // 319
|
||||
// check if its too old // 320
|
||||
var now = Date.now(); // 321
|
||||
if (tokenObject.expiration < now) { // 322
|
||||
FS.debug && console.log('Expired token: ' + tokenObject.expiration + ' is less than ' + now); // 323
|
||||
throw new Meteor.Error(500, 'Expired token'); // 324
|
||||
} // 325
|
||||
} // 326
|
||||
// 327
|
||||
// We are not on a secure line - so we have to look up the user... // 328
|
||||
var user = Meteor.users.findOne({ // 329
|
||||
$or: [ // 330
|
||||
{'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(userToken)}, // 331
|
||||
{'services.resume.loginTokens.token': userToken} // 332
|
||||
] // 333
|
||||
}); // 334
|
||||
// 335
|
||||
// Set the userId in the scope // 336
|
||||
return user && user._id; // 337
|
||||
}; // 338
|
||||
// 339
|
||||
HTTP.methods( // 340
|
||||
{'/cfs/servertime': { // 341
|
||||
get: function(data) { // 342
|
||||
return Date.now().toString(); // 343
|
||||
} // 344
|
||||
} // 345
|
||||
}); // 346
|
||||
// 347
|
||||
// Unify client / server api // 348
|
||||
FS.HTTP.now = function() { // 349
|
||||
return Date.now(); // 350
|
||||
}; // 351
|
||||
// 352
|
||||
// Start up the basic mount points // 353
|
||||
Meteor.startup(function () { // 354
|
||||
mountUrls(); // 355
|
||||
}); // 356
|
||||
// 357
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
}).call(this);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
|
||||
}).call(this);
|
||||
|
||||
|
||||
/* Exports */
|
||||
if (typeof Package === 'undefined') Package = {};
|
||||
Package['cfs:access-point'] = {};
|
||||
|
||||
})();
|
||||
@@ -1,238 +0,0 @@
|
||||
/* global JsonRoutes */
|
||||
if (Meteor.isServer) {
|
||||
// todo XXX once we have a real API in place, move that route there
|
||||
// todo XXX also share the route definition between the client and the server
|
||||
// so that we could use something like
|
||||
// `ApiRoutes.path('boards/export', boardId)``
|
||||
// on the client instead of copy/pasting the route path manually between the
|
||||
// client and the server.
|
||||
/**
|
||||
* @operation export
|
||||
* @tag Boards
|
||||
*
|
||||
* @summary This route is used to export the board.
|
||||
*
|
||||
* @description If user is already logged-in, pass loginToken as param
|
||||
* "authToken": '/api/boards/:boardId/export?authToken=:token'
|
||||
*
|
||||
* See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/
|
||||
* for detailed explanations
|
||||
*
|
||||
* @param {string} boardId the ID of the board we are exporting
|
||||
* @param {string} authToken the loginToken
|
||||
*/
|
||||
JsonRoutes.add('get', '/api/boards/:boardId/export', function(req, res) {
|
||||
const boardId = req.params.boardId;
|
||||
let user = null;
|
||||
|
||||
const loginToken = req.query.authToken;
|
||||
if (loginToken) {
|
||||
const hashToken = Accounts._hashLoginToken(loginToken);
|
||||
user = Meteor.users.findOne({
|
||||
'services.resume.loginTokens.hashedToken': hashToken,
|
||||
});
|
||||
} else if (!Meteor.settings.public.sandstorm) {
|
||||
Authentication.checkUserId(req.userId);
|
||||
user = Users.findOne({ _id: req.userId, isAdmin: true });
|
||||
}
|
||||
|
||||
const exporter = new Exporter(boardId);
|
||||
if (exporter.canExport(user)) {
|
||||
JsonRoutes.sendResult(res, {
|
||||
code: 200,
|
||||
data: exporter.build(),
|
||||
});
|
||||
} else {
|
||||
// we could send an explicit error message, but on the other hand the only
|
||||
// way to get there is by hacking the UI so let's keep it raw.
|
||||
JsonRoutes.sendResult(res, 403);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// exporter maybe is broken since Gridfs introduced, add fs and path
|
||||
|
||||
export class Exporter {
|
||||
constructor(boardId) {
|
||||
this._boardId = boardId;
|
||||
}
|
||||
|
||||
build() {
|
||||
const fs = Npm.require('fs');
|
||||
const os = Npm.require('os');
|
||||
const path = Npm.require('path');
|
||||
|
||||
const byBoard = { boardId: this._boardId };
|
||||
const byBoardNoLinked = {
|
||||
boardId: this._boardId,
|
||||
linkedId: { $in: ['', null] },
|
||||
};
|
||||
// we do not want to retrieve boardId in related elements
|
||||
const noBoardId = {
|
||||
fields: {
|
||||
boardId: 0,
|
||||
},
|
||||
};
|
||||
const result = {
|
||||
_format: 'wekan-board-1.0.0',
|
||||
};
|
||||
_.extend(
|
||||
result,
|
||||
Boards.findOne(this._boardId, {
|
||||
fields: {
|
||||
stars: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
result.lists = Lists.find(byBoard, noBoardId).fetch();
|
||||
result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
|
||||
result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();
|
||||
result.customFields = CustomFields.find(
|
||||
{ boardIds: { $in: [this.boardId] } },
|
||||
{ fields: { boardId: 0 } },
|
||||
).fetch();
|
||||
result.comments = CardComments.find(byBoard, noBoardId).fetch();
|
||||
result.activities = Activities.find(byBoard, noBoardId).fetch();
|
||||
result.rules = Rules.find(byBoard, noBoardId).fetch();
|
||||
result.checklists = [];
|
||||
result.checklistItems = [];
|
||||
result.subtaskItems = [];
|
||||
result.triggers = [];
|
||||
result.actions = [];
|
||||
result.cards.forEach(card => {
|
||||
result.checklists.push(
|
||||
...Checklists.find({
|
||||
cardId: card._id,
|
||||
}).fetch(),
|
||||
);
|
||||
result.checklistItems.push(
|
||||
...ChecklistItems.find({
|
||||
cardId: card._id,
|
||||
}).fetch(),
|
||||
);
|
||||
result.subtaskItems.push(
|
||||
...Cards.find({
|
||||
parentId: card._id,
|
||||
}).fetch(),
|
||||
);
|
||||
});
|
||||
result.rules.forEach(rule => {
|
||||
result.triggers.push(
|
||||
...Triggers.find(
|
||||
{
|
||||
_id: rule.triggerId,
|
||||
},
|
||||
noBoardId,
|
||||
).fetch(),
|
||||
);
|
||||
result.actions.push(
|
||||
...Actions.find(
|
||||
{
|
||||
_id: rule.actionId,
|
||||
},
|
||||
noBoardId,
|
||||
).fetch(),
|
||||
);
|
||||
});
|
||||
|
||||
// [Old] for attachments we only export IDs and absolute url to original doc
|
||||
// [New] Encode attachment to base64
|
||||
const getBase64Data = function(doc, callback) {
|
||||
let buffer = new Buffer(0);
|
||||
// callback has the form function (err, res) {}
|
||||
const tmpFile = path.join(
|
||||
os.tmpdir(),
|
||||
`tmpexport${process.pid}${Math.random()}`,
|
||||
);
|
||||
const tmpWriteable = fs.createWriteStream(tmpFile);
|
||||
const readStream = doc.createReadStream();
|
||||
readStream.on('data', function(chunk) {
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
});
|
||||
readStream.on('error', function(err) {
|
||||
callback(err, null);
|
||||
});
|
||||
readStream.on('end', function() {
|
||||
// done
|
||||
fs.unlink(tmpFile, () => {
|
||||
//ignored
|
||||
});
|
||||
callback(null, buffer.toString('base64'));
|
||||
});
|
||||
readStream.pipe(tmpWriteable);
|
||||
};
|
||||
const getBase64DataSync = Meteor.wrapAsync(getBase64Data);
|
||||
result.attachments = Attachments.find(byBoard)
|
||||
.fetch()
|
||||
.map(attachment => {
|
||||
return {
|
||||
_id: attachment._id,
|
||||
cardId: attachment.cardId,
|
||||
// url: FlowRouter.url(attachment.url()),
|
||||
file: getBase64DataSync(attachment),
|
||||
name: attachment.original.name,
|
||||
type: attachment.original.type,
|
||||
};
|
||||
});
|
||||
|
||||
// we also have to export some user data - as the other elements only
|
||||
// include id but we have to be careful:
|
||||
// 1- only exports users that are linked somehow to that board
|
||||
// 2- do not export any sensitive information
|
||||
const users = {};
|
||||
result.members.forEach(member => {
|
||||
users[member.userId] = true;
|
||||
});
|
||||
result.lists.forEach(list => {
|
||||
users[list.userId] = true;
|
||||
});
|
||||
result.cards.forEach(card => {
|
||||
users[card.userId] = true;
|
||||
if (card.members) {
|
||||
card.members.forEach(memberId => {
|
||||
users[memberId] = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
result.comments.forEach(comment => {
|
||||
users[comment.userId] = true;
|
||||
});
|
||||
result.activities.forEach(activity => {
|
||||
users[activity.userId] = true;
|
||||
});
|
||||
result.checklists.forEach(checklist => {
|
||||
users[checklist.userId] = true;
|
||||
});
|
||||
const byUserIds = {
|
||||
_id: {
|
||||
$in: Object.getOwnPropertyNames(users),
|
||||
},
|
||||
};
|
||||
// we use whitelist to be sure we do not expose inadvertently
|
||||
// some secret fields that gets added to User later.
|
||||
const userFields = {
|
||||
fields: {
|
||||
_id: 1,
|
||||
username: 1,
|
||||
'profile.fullname': 1,
|
||||
'profile.initials': 1,
|
||||
'profile.avatarUrl': 1,
|
||||
},
|
||||
};
|
||||
result.users = Users.find(byUserIds, userFields)
|
||||
.fetch()
|
||||
.map(user => {
|
||||
// user avatar is stored as a relative url, we export absolute
|
||||
if ((user.profile || {}).avatarUrl) {
|
||||
user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl);
|
||||
}
|
||||
return user;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
canExport(user) {
|
||||
const board = Boards.findOne(this._boardId);
|
||||
return board && board.isVisibleBy(user);
|
||||
}
|
||||
}
|
||||
@@ -1,640 +0,0 @@
|
||||
import ldapjs from 'ldapjs';
|
||||
import util from 'util';
|
||||
import Bunyan from 'bunyan';
|
||||
import { log_debug, log_info, log_warn, log_error } from './logger';
|
||||
|
||||
export default class LDAP {
|
||||
constructor() {
|
||||
this.ldapjs = ldapjs;
|
||||
|
||||
this.connected = false;
|
||||
|
||||
this.options = {
|
||||
host: this.constructor.settings_get('LDAP_HOST'),
|
||||
port: this.constructor.settings_get('LDAP_PORT'),
|
||||
Reconnect: this.constructor.settings_get('LDAP_RECONNECT'),
|
||||
timeout: this.constructor.settings_get('LDAP_TIMEOUT'),
|
||||
connect_timeout: this.constructor.settings_get('LDAP_CONNECT_TIMEOUT'),
|
||||
idle_timeout: this.constructor.settings_get('LDAP_IDLE_TIMEOUT'),
|
||||
encryption: this.constructor.settings_get('LDAP_ENCRYPTION'),
|
||||
ca_cert: this.constructor.settings_get('LDAP_CA_CERT'),
|
||||
reject_unauthorized:
|
||||
this.constructor.settings_get('LDAP_REJECT_UNAUTHORIZED') || false,
|
||||
Authentication: this.constructor.settings_get('LDAP_AUTHENTIFICATION'),
|
||||
Authentication_UserDN: this.constructor.settings_get(
|
||||
'LDAP_AUTHENTIFICATION_USERDN',
|
||||
),
|
||||
Authentication_Password: this.constructor.settings_get(
|
||||
'LDAP_AUTHENTIFICATION_PASSWORD',
|
||||
),
|
||||
Authentication_Fallback: this.constructor.settings_get(
|
||||
'LDAP_LOGIN_FALLBACK',
|
||||
),
|
||||
BaseDN: this.constructor.settings_get('LDAP_BASEDN'),
|
||||
Internal_Log_Level: this.constructor.settings_get('INTERNAL_LOG_LEVEL'),
|
||||
User_Authentication: this.constructor.settings_get(
|
||||
'LDAP_USER_AUTHENTICATION',
|
||||
),
|
||||
User_Authentication_Field: this.constructor.settings_get(
|
||||
'LDAP_USER_AUTHENTICATION_FIELD',
|
||||
),
|
||||
User_Attributes: this.constructor.settings_get('LDAP_USER_ATTRIBUTES'),
|
||||
User_Search_Filter: this.constructor.settings_get(
|
||||
'LDAP_USER_SEARCH_FILTER',
|
||||
),
|
||||
User_Search_Scope: this.constructor.settings_get(
|
||||
'LDAP_USER_SEARCH_SCOPE',
|
||||
),
|
||||
User_Search_Field: this.constructor.settings_get(
|
||||
'LDAP_USER_SEARCH_FIELD',
|
||||
),
|
||||
Search_Page_Size: this.constructor.settings_get('LDAP_SEARCH_PAGE_SIZE'),
|
||||
Search_Size_Limit: this.constructor.settings_get(
|
||||
'LDAP_SEARCH_SIZE_LIMIT',
|
||||
),
|
||||
group_filter_enabled: this.constructor.settings_get(
|
||||
'LDAP_GROUP_FILTER_ENABLE',
|
||||
),
|
||||
group_filter_object_class: this.constructor.settings_get(
|
||||
'LDAP_GROUP_FILTER_OBJECTCLASS',
|
||||
),
|
||||
group_filter_group_id_attribute: this.constructor.settings_get(
|
||||
'LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE',
|
||||
),
|
||||
group_filter_group_member_attribute: this.constructor.settings_get(
|
||||
'LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE',
|
||||
),
|
||||
group_filter_group_member_format: this.constructor.settings_get(
|
||||
'LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT',
|
||||
),
|
||||
group_filter_group_name: this.constructor.settings_get(
|
||||
'LDAP_GROUP_FILTER_GROUP_NAME',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
static settings_get(name, ...args) {
|
||||
let value = process.env[name];
|
||||
if (value !== undefined) {
|
||||
if (value === 'true' || value === 'false') {
|
||||
value = JSON.parse(value);
|
||||
} else if (value !== '' && !isNaN(value)) {
|
||||
value = Number(value);
|
||||
}
|
||||
return value;
|
||||
} else {
|
||||
log_warn(`Lookup for unset variable: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
connectSync(...args) {
|
||||
if (!this._connectSync) {
|
||||
this._connectSync = Meteor.wrapAsync(this.connectAsync, this);
|
||||
}
|
||||
return this._connectSync(...args);
|
||||
}
|
||||
|
||||
searchAllSync(...args) {
|
||||
if (!this._searchAllSync) {
|
||||
this._searchAllSync = Meteor.wrapAsync(this.searchAllAsync, this);
|
||||
}
|
||||
return this._searchAllSync(...args);
|
||||
}
|
||||
|
||||
connectAsync(callback) {
|
||||
log_info('Init setup');
|
||||
|
||||
let replied = false;
|
||||
|
||||
const connectionOptions = {
|
||||
url: `${this.options.host}:${this.options.port}`,
|
||||
timeout: this.options.timeout,
|
||||
connectTimeout: this.options.connect_timeout,
|
||||
idleTimeout: this.options.idle_timeout,
|
||||
reconnect: this.options.Reconnect,
|
||||
};
|
||||
|
||||
if (this.options.Internal_Log_Level !== 'disabled') {
|
||||
connectionOptions.log = new Bunyan({
|
||||
name: 'ldapjs',
|
||||
component: 'client',
|
||||
stream: process.stderr,
|
||||
level: this.options.Internal_Log_Level,
|
||||
});
|
||||
}
|
||||
|
||||
const tlsOptions = {
|
||||
rejectUnauthorized: this.options.reject_unauthorized,
|
||||
};
|
||||
|
||||
if (this.options.ca_cert && this.options.ca_cert !== '') {
|
||||
// Split CA cert into array of strings
|
||||
const chainLines = this.constructor
|
||||
.settings_get('LDAP_CA_CERT')
|
||||
.split('\n');
|
||||
let cert = [];
|
||||
const ca = [];
|
||||
chainLines.forEach(line => {
|
||||
cert.push(line);
|
||||
if (line.match(/-END CERTIFICATE-/)) {
|
||||
ca.push(cert.join('\n'));
|
||||
cert = [];
|
||||
}
|
||||
});
|
||||
tlsOptions.ca = ca;
|
||||
}
|
||||
|
||||
if (this.options.encryption === 'ssl') {
|
||||
connectionOptions.url = `ldaps://${connectionOptions.url}`;
|
||||
connectionOptions.tlsOptions = tlsOptions;
|
||||
} else {
|
||||
connectionOptions.url = `ldap://${connectionOptions.url}`;
|
||||
}
|
||||
|
||||
log_info('Connecting', connectionOptions.url);
|
||||
log_debug(`connectionOptions${util.inspect(connectionOptions)}`);
|
||||
|
||||
this.client = ldapjs.createClient(connectionOptions);
|
||||
|
||||
this.bindSync = Meteor.wrapAsync(this.client.bind, this.client);
|
||||
|
||||
this.client.on('error', error => {
|
||||
log_error('connection', error);
|
||||
if (replied === false) {
|
||||
replied = true;
|
||||
callback(error, null);
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on('idle', () => {
|
||||
log_info('Idle');
|
||||
this.disconnect();
|
||||
});
|
||||
|
||||
this.client.on('close', () => {
|
||||
log_info('Closed');
|
||||
});
|
||||
|
||||
if (this.options.encryption === 'tls') {
|
||||
// Set host parameter for tls.connect which is used by ldapjs starttls. This shouldn't be needed in newer nodejs versions (e.g v5.6.0).
|
||||
// https://github.com/RocketChat/Rocket.Chat/issues/2035
|
||||
// https://github.com/mcavage/node-ldapjs/issues/349
|
||||
tlsOptions.host = this.options.host;
|
||||
|
||||
log_info('Starting TLS');
|
||||
log_debug('tlsOptions', tlsOptions);
|
||||
|
||||
this.client.starttls(tlsOptions, null, (error, response) => {
|
||||
if (error) {
|
||||
log_error('TLS connection', error);
|
||||
if (replied === false) {
|
||||
replied = true;
|
||||
callback(error, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
log_info('TLS connected');
|
||||
this.connected = true;
|
||||
if (replied === false) {
|
||||
replied = true;
|
||||
callback(null, response);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.client.on('connect', response => {
|
||||
log_info('LDAP connected');
|
||||
this.connected = true;
|
||||
if (replied === false) {
|
||||
replied = true;
|
||||
callback(null, response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (replied === false) {
|
||||
log_error('connection time out', connectionOptions.connectTimeout);
|
||||
replied = true;
|
||||
callback(new Error('Timeout'));
|
||||
}
|
||||
}, connectionOptions.connectTimeout);
|
||||
}
|
||||
|
||||
getUserFilter(username) {
|
||||
const filter = [];
|
||||
|
||||
if (this.options.User_Search_Filter !== '') {
|
||||
if (this.options.User_Search_Filter[0] === '(') {
|
||||
filter.push(`${this.options.User_Search_Filter}`);
|
||||
} else {
|
||||
filter.push(`(${this.options.User_Search_Filter})`);
|
||||
}
|
||||
}
|
||||
|
||||
const usernameFilter = this.options.User_Search_Field.split(',').map(
|
||||
item => `(${item}=${username})`,
|
||||
);
|
||||
|
||||
if (usernameFilter.length === 0) {
|
||||
log_error('LDAP_LDAP_User_Search_Field not defined');
|
||||
} else if (usernameFilter.length === 1) {
|
||||
filter.push(`${usernameFilter[0]}`);
|
||||
} else {
|
||||
filter.push(`(|${usernameFilter.join('')})`);
|
||||
}
|
||||
|
||||
return `(&${filter.join('')})`;
|
||||
}
|
||||
|
||||
bindUserIfNecessary(username, password) {
|
||||
if (this.domainBinded === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.options.User_Authentication) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.options.BaseDN) throw new Error('BaseDN is not provided');
|
||||
|
||||
const userDn = `${this.options.User_Authentication_Field}=${username},${this.options.BaseDN}`;
|
||||
|
||||
this.bindSync(userDn, password);
|
||||
this.domainBinded = true;
|
||||
}
|
||||
|
||||
bindIfNecessary() {
|
||||
if (this.domainBinded === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.Authentication !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_info('Binding UserDN', this.options.Authentication_UserDN);
|
||||
|
||||
this.bindSync(
|
||||
this.options.Authentication_UserDN,
|
||||
this.options.Authentication_Password,
|
||||
);
|
||||
this.domainBinded = true;
|
||||
}
|
||||
|
||||
searchUsersSync(username, page) {
|
||||
this.bindIfNecessary();
|
||||
const searchOptions = {
|
||||
filter: this.getUserFilter(username),
|
||||
scope: this.options.User_Search_Scope || 'sub',
|
||||
sizeLimit: this.options.Search_Size_Limit,
|
||||
};
|
||||
|
||||
if (!!this.options.User_Attributes)
|
||||
searchOptions.attributes = this.options.User_Attributes.split(',');
|
||||
|
||||
if (this.options.Search_Page_Size > 0) {
|
||||
searchOptions.paged = {
|
||||
pageSize: this.options.Search_Page_Size,
|
||||
pagePause: !!page,
|
||||
};
|
||||
}
|
||||
|
||||
log_info('Searching user', username);
|
||||
log_debug('searchOptions', searchOptions);
|
||||
log_debug('BaseDN', this.options.BaseDN);
|
||||
|
||||
if (page) {
|
||||
return this.searchAllPaged(this.options.BaseDN, searchOptions, page);
|
||||
}
|
||||
|
||||
return this.searchAllSync(this.options.BaseDN, searchOptions);
|
||||
}
|
||||
|
||||
getUserByIdSync(id, attribute) {
|
||||
this.bindIfNecessary();
|
||||
|
||||
const Unique_Identifier_Field = this.constructor
|
||||
.settings_get('LDAP_UNIQUE_IDENTIFIER_FIELD')
|
||||
.split(',');
|
||||
|
||||
let filter;
|
||||
|
||||
if (attribute) {
|
||||
filter = new this.ldapjs.filters.EqualityFilter({
|
||||
attribute,
|
||||
value: new Buffer(id, 'hex'),
|
||||
});
|
||||
} else {
|
||||
const filters = [];
|
||||
Unique_Identifier_Field.forEach(item => {
|
||||
filters.push(
|
||||
new this.ldapjs.filters.EqualityFilter({
|
||||
attribute: item,
|
||||
value: new Buffer(id, 'hex'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
filter = new this.ldapjs.filters.OrFilter({ filters });
|
||||
}
|
||||
|
||||
const searchOptions = {
|
||||
filter,
|
||||
scope: 'sub',
|
||||
};
|
||||
|
||||
log_info('Searching by id', id);
|
||||
log_debug('search filter', searchOptions.filter.toString());
|
||||
log_debug('BaseDN', this.options.BaseDN);
|
||||
|
||||
const result = this.searchAllSync(this.options.BaseDN, searchOptions);
|
||||
|
||||
if (!Array.isArray(result) || result.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.length > 1) {
|
||||
log_error('Search by id', id, 'returned', result.length, 'records');
|
||||
}
|
||||
|
||||
return result[0];
|
||||
}
|
||||
|
||||
getUserByUsernameSync(username) {
|
||||
this.bindIfNecessary();
|
||||
|
||||
const searchOptions = {
|
||||
filter: this.getUserFilter(username),
|
||||
scope: this.options.User_Search_Scope || 'sub',
|
||||
};
|
||||
|
||||
log_info('Searching user', username);
|
||||
log_debug('searchOptions', searchOptions);
|
||||
log_debug('BaseDN', this.options.BaseDN);
|
||||
|
||||
const result = this.searchAllSync(this.options.BaseDN, searchOptions);
|
||||
|
||||
if (!Array.isArray(result) || result.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.length > 1) {
|
||||
log_error(
|
||||
'Search by username',
|
||||
username,
|
||||
'returned',
|
||||
result.length,
|
||||
'records',
|
||||
);
|
||||
}
|
||||
|
||||
return result[0];
|
||||
}
|
||||
|
||||
getUserGroups(username, ldapUser) {
|
||||
if (!this.options.group_filter_enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const filter = ['(&'];
|
||||
|
||||
if (this.options.group_filter_object_class !== '') {
|
||||
filter.push(`(objectclass=${this.options.group_filter_object_class})`);
|
||||
}
|
||||
|
||||
if (this.options.group_filter_group_member_attribute !== '') {
|
||||
const format_value =
|
||||
ldapUser[this.options.group_filter_group_member_format];
|
||||
if (format_value) {
|
||||
filter.push(
|
||||
`(${this.options.group_filter_group_member_attribute}=${format_value})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
filter.push(')');
|
||||
|
||||
const searchOptions = {
|
||||
filter: filter.join('').replace(/#{username}/g, username),
|
||||
scope: 'sub',
|
||||
};
|
||||
|
||||
log_debug('Group list filter LDAP:', searchOptions.filter);
|
||||
|
||||
const result = this.searchAllSync(this.options.BaseDN, searchOptions);
|
||||
|
||||
if (!Array.isArray(result) || result.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const grp_identifier = this.options.group_filter_group_id_attribute || 'cn';
|
||||
const groups = [];
|
||||
result.map(item => {
|
||||
groups.push(item[grp_identifier]);
|
||||
});
|
||||
log_debug(`Groups: ${groups.join(', ')}`);
|
||||
return groups;
|
||||
}
|
||||
|
||||
isUserInGroup(username, ldapUser) {
|
||||
if (!this.options.group_filter_enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const grps = this.getUserGroups(username, ldapUser);
|
||||
|
||||
const filter = ['(&'];
|
||||
|
||||
if (this.options.group_filter_object_class !== '') {
|
||||
filter.push(`(objectclass=${this.options.group_filter_object_class})`);
|
||||
}
|
||||
|
||||
if (this.options.group_filter_group_member_attribute !== '') {
|
||||
const format_value =
|
||||
ldapUser[this.options.group_filter_group_member_format];
|
||||
if (format_value) {
|
||||
filter.push(
|
||||
`(${this.options.group_filter_group_member_attribute}=${format_value})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.group_filter_group_id_attribute !== '') {
|
||||
filter.push(
|
||||
`(${this.options.group_filter_group_id_attribute}=${this.options.group_filter_group_name})`,
|
||||
);
|
||||
}
|
||||
filter.push(')');
|
||||
|
||||
const searchOptions = {
|
||||
filter: filter.join('').replace(/#{username}/g, username),
|
||||
scope: 'sub',
|
||||
};
|
||||
|
||||
log_debug('Group filter LDAP:', searchOptions.filter);
|
||||
|
||||
const result = this.searchAllSync(this.options.BaseDN, searchOptions);
|
||||
|
||||
if (!Array.isArray(result) || result.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
extractLdapEntryData(entry) {
|
||||
const values = {
|
||||
_raw: entry.raw,
|
||||
};
|
||||
|
||||
Object.keys(values._raw).forEach(key => {
|
||||
const value = values._raw[key];
|
||||
|
||||
if (!['thumbnailPhoto', 'jpegPhoto'].includes(key)) {
|
||||
if (value instanceof Buffer) {
|
||||
values[key] = value.toString();
|
||||
} else {
|
||||
values[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
searchAllPaged(BaseDN, options, page) {
|
||||
this.bindIfNecessary();
|
||||
|
||||
const processPage = ({ entries, title, end, next }) => {
|
||||
log_info(title);
|
||||
// Force LDAP idle to wait the record processing
|
||||
this.client._updateIdle(true);
|
||||
page(null, entries, {
|
||||
end,
|
||||
next: () => {
|
||||
// Reset idle timer
|
||||
this.client._updateIdle();
|
||||
next && next();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
this.client.search(BaseDN, options, (error, res) => {
|
||||
if (error) {
|
||||
log_error(error);
|
||||
page(error);
|
||||
return;
|
||||
}
|
||||
|
||||
res.on('error', error => {
|
||||
log_error(error);
|
||||
page(error);
|
||||
return;
|
||||
});
|
||||
|
||||
let entries = [];
|
||||
|
||||
const internalPageSize =
|
||||
options.paged && options.paged.pageSize > 0
|
||||
? options.paged.pageSize * 2
|
||||
: 500;
|
||||
|
||||
res.on('searchEntry', entry => {
|
||||
entries.push(this.extractLdapEntryData(entry));
|
||||
|
||||
if (entries.length >= internalPageSize) {
|
||||
processPage({
|
||||
entries,
|
||||
title: 'Internal Page',
|
||||
end: false,
|
||||
});
|
||||
entries = [];
|
||||
}
|
||||
});
|
||||
|
||||
res.on('page', (result, next) => {
|
||||
if (!next) {
|
||||
this.client._updateIdle(true);
|
||||
processPage({
|
||||
entries,
|
||||
title: 'Final Page',
|
||||
end: true,
|
||||
});
|
||||
} else if (entries.length) {
|
||||
log_info('Page');
|
||||
processPage({
|
||||
entries,
|
||||
title: 'Page',
|
||||
end: false,
|
||||
next,
|
||||
});
|
||||
entries = [];
|
||||
}
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
if (entries.length) {
|
||||
processPage({
|
||||
entries,
|
||||
title: 'Final Page',
|
||||
end: true,
|
||||
});
|
||||
entries = [];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
searchAllAsync(BaseDN, options, callback) {
|
||||
this.bindIfNecessary();
|
||||
|
||||
this.client.search(BaseDN, options, (error, res) => {
|
||||
if (error) {
|
||||
log_error(error);
|
||||
callback(error);
|
||||
return;
|
||||
}
|
||||
|
||||
res.on('error', error => {
|
||||
log_error(error);
|
||||
callback(error);
|
||||
return;
|
||||
});
|
||||
|
||||
const entries = [];
|
||||
|
||||
res.on('searchEntry', entry => {
|
||||
entries.push(this.extractLdapEntryData(entry));
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
log_info('Search result count', entries.length);
|
||||
callback(null, entries);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
authSync(dn, password) {
|
||||
log_info('Authenticating', dn);
|
||||
|
||||
try {
|
||||
if (password === '') {
|
||||
throw new Error('Password is not provided');
|
||||
}
|
||||
this.bindSync(dn, password);
|
||||
log_info('Authenticated', dn);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log_info('Not authenticated', dn);
|
||||
log_debug('error', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.connected = false;
|
||||
this.domainBinded = false;
|
||||
log_info('Disconecting');
|
||||
this.client.unbind();
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
Oidc = {};
|
||||
|
||||
OAuth.registerService('oidc', 2, null, function(query) {
|
||||
var debug = process.env.DEBUG || false;
|
||||
var token = getToken(query);
|
||||
if (debug) console.log('XXX: register token:', token);
|
||||
|
||||
var accessToken = token.access_token || token.id_token;
|
||||
var expiresAt = +new Date() + 1000 * parseInt(token.expires_in, 10);
|
||||
|
||||
var userinfo = getUserInfo(accessToken);
|
||||
if (debug) console.log('XXX: userinfo:', userinfo);
|
||||
|
||||
var serviceData = {};
|
||||
serviceData.id = userinfo[process.env.OAUTH2_ID_MAP]; // || userinfo["id"];
|
||||
serviceData.username = userinfo[process.env.OAUTH2_USERNAME_MAP]; // || userinfo["uid"];
|
||||
serviceData.fullname = userinfo[process.env.OAUTH2_FULLNAME_MAP]; // || userinfo["displayName"];
|
||||
serviceData.accessToken = accessToken;
|
||||
serviceData.expiresAt = expiresAt;
|
||||
serviceData.email = userinfo[process.env.OAUTH2_EMAIL_MAP]; // || userinfo["email"];
|
||||
|
||||
if (accessToken) {
|
||||
var tokenContent = getTokenContent(accessToken);
|
||||
var fields = _.pick(
|
||||
tokenContent,
|
||||
getConfiguration().idTokenWhitelistFields,
|
||||
);
|
||||
_.extend(serviceData, fields);
|
||||
}
|
||||
|
||||
if (token.refresh_token) serviceData.refreshToken = token.refresh_token;
|
||||
if (debug) console.log('XXX: serviceData:', serviceData);
|
||||
|
||||
var profile = {};
|
||||
profile.name = userinfo[process.env.OAUTH2_FULLNAME_MAP]; // || userinfo["displayName"];
|
||||
profile.email = userinfo[process.env.OAUTH2_EMAIL_MAP]; // || userinfo["email"];
|
||||
if (debug) console.log('XXX: profile:', profile);
|
||||
|
||||
return {
|
||||
serviceData: serviceData,
|
||||
options: { profile: profile },
|
||||
};
|
||||
});
|
||||
|
||||
var userAgent = 'Meteor';
|
||||
if (Meteor.release) {
|
||||
userAgent += '/' + Meteor.release;
|
||||
}
|
||||
|
||||
var getToken = function(query) {
|
||||
var debug = process.env.DEBUG || false;
|
||||
var config = getConfiguration();
|
||||
if (config.tokenEndpoint.includes('https://')) {
|
||||
var serverTokenEndpoint = config.tokenEndpoint;
|
||||
} else {
|
||||
var serverTokenEndpoint = config.serverUrl + config.tokenEndpoint;
|
||||
}
|
||||
var requestPermissions = config.requestPermissions;
|
||||
var response;
|
||||
|
||||
try {
|
||||
response = HTTP.post(serverTokenEndpoint, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'User-Agent': userAgent,
|
||||
},
|
||||
params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: OAuth.openSecret(config.secret),
|
||||
redirect_uri: OAuth._redirectUri('oidc', config),
|
||||
grant_type: 'authorization_code',
|
||||
scope: requestPermissions,
|
||||
state: query.state,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
throw _.extend(
|
||||
new Error(
|
||||
'Failed to get token from OIDC ' +
|
||||
serverTokenEndpoint +
|
||||
': ' +
|
||||
err.message,
|
||||
),
|
||||
{ response: err.response },
|
||||
);
|
||||
}
|
||||
if (response.data.error) {
|
||||
// if the http response was a json object with an error attribute
|
||||
throw new Error(
|
||||
'Failed to complete handshake with OIDC ' +
|
||||
serverTokenEndpoint +
|
||||
': ' +
|
||||
response.data.error,
|
||||
);
|
||||
} else {
|
||||
if (debug) console.log('XXX: getToken response: ', response.data);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
var getUserInfo = function(accessToken) {
|
||||
var debug = process.env.DEBUG || false;
|
||||
var config = getConfiguration();
|
||||
// Some userinfo endpoints use a different base URL than the authorization or token endpoints.
|
||||
// This logic allows the end user to override the setting by providing the full URL to userinfo in their config.
|
||||
if (config.userinfoEndpoint.includes('https://')) {
|
||||
var serverUserinfoEndpoint = config.userinfoEndpoint;
|
||||
} else {
|
||||
var serverUserinfoEndpoint = config.serverUrl + config.userinfoEndpoint;
|
||||
}
|
||||
var response;
|
||||
try {
|
||||
response = HTTP.get(serverUserinfoEndpoint, {
|
||||
headers: {
|
||||
'User-Agent': userAgent,
|
||||
Authorization: 'Bearer ' + accessToken,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
throw _.extend(
|
||||
new Error(
|
||||
'Failed to fetch userinfo from OIDC ' +
|
||||
serverUserinfoEndpoint +
|
||||
': ' +
|
||||
err.message,
|
||||
),
|
||||
{ response: err.response },
|
||||
);
|
||||
}
|
||||
if (debug) console.log('XXX: getUserInfo response: ', response.data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
var getConfiguration = function() {
|
||||
var config = ServiceConfiguration.configurations.findOne({ service: 'oidc' });
|
||||
if (!config) {
|
||||
throw new ServiceConfiguration.ConfigError('Service oidc not configured.');
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
var getTokenContent = function(token) {
|
||||
var content = null;
|
||||
if (token) {
|
||||
try {
|
||||
var parts = token.split('.');
|
||||
var header = JSON.parse(new Buffer(parts[0], 'base64').toString());
|
||||
content = JSON.parse(new Buffer(parts[1], 'base64').toString());
|
||||
var signature = new Buffer(parts[2], 'base64');
|
||||
var signed = parts[0] + '.' + parts[1];
|
||||
} catch (err) {
|
||||
this.content = {
|
||||
exp: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
Oidc.retrieveCredential = function(credentialToken, credentialSecret) {
|
||||
return OAuth.retrieveCredential(credentialToken, credentialSecret);
|
||||
};
|
||||
Generated
-4361
File diff suppressed because it is too large.
Load diff
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"name": "wekan",
|
||||
"version": "v3.92.0",
|
||||
"description": "Open-Source kanban",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint --cache --ext .js --ignore-path .eslintignore .",
|
||||
"lint:eslint:fix": "eslint --ext .js --ignore-path .eslintignore --fix .",
|
||||
"lint:staged": "lint-staged",
|
||||
"prettify": "prettier --write '**/*.js' '**/*.jsx'",
|
||||
"test": "npm run lint"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
"meteor npm run prettify",
|
||||
"meteor npm run lint:eslint:fix",
|
||||
"git add --force"
|
||||
],
|
||||
"*.jsx": [
|
||||
"meteor npm run prettify",
|
||||
"meteor npm run lint:eslint:fix",
|
||||
"git add --force"
|
||||
],
|
||||
"*.json": [
|
||||
"prettier --write",
|
||||
"git add --force"
|
||||
]
|
||||
},
|
||||
"pre-commit": "lint:staged",
|
||||
"eslintConfig": {
|
||||
"extends": "@meteorjs/eslint-config-meteor"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wekan/wekan.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wekan/wekan/issues"
|
||||
},
|
||||
"homepage": "https://wekan.github.io",
|
||||
"devDependencies": {
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-meteor": "^0.1.1",
|
||||
"eslint-config-prettier": "^6.10.0",
|
||||
"eslint-import-resolver-meteor": "^0.4.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-meteor": "^6.0.0",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"lint-staged": "^10.0.8",
|
||||
"pre-commit": "^1.2.2",
|
||||
"prettier": "^1.19.1",
|
||||
"prettier-eslint": "^9.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"ajv": "^6.12.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"bcrypt": "^4.0.1",
|
||||
"bson": "^4.0.3",
|
||||
"bunyan": "^1.8.12",
|
||||
"es6-promise": "^4.2.8",
|
||||
"gridfs-stream": "^1.1.1",
|
||||
"ldapjs": "^1.0.2",
|
||||
"meteor-node-stubs": "^1.0.0",
|
||||
"mongodb": "^3.5.5",
|
||||
"os": "^0.1.1",
|
||||
"page": "^1.11.5",
|
||||
"qs": "^6.9.1",
|
||||
"source-map-support": "^0.5.16",
|
||||
"xss": "^1.0.6"
|
||||
}
|
||||
}
|
||||
@@ -1,853 +0,0 @@
|
||||
const DateString = Match.Where(function(dateAsString) {
|
||||
check(dateAsString, String);
|
||||
return moment(dateAsString, moment.ISO_8601).isValid();
|
||||
});
|
||||
|
||||
export class WekanCreator {
|
||||
constructor(data) {
|
||||
// we log current date, to use the same timestamp for all our actions.
|
||||
// this helps to retrieve all elements performed by the same import.
|
||||
this._nowDate = new Date();
|
||||
// The object creation dates, indexed by Wekan id
|
||||
// (so we only parse actions once!)
|
||||
this.createdAt = {
|
||||
board: null,
|
||||
cards: {},
|
||||
lists: {},
|
||||
swimlanes: {},
|
||||
};
|
||||
// The object creator Wekan Id, indexed by the object Wekan id
|
||||
// (so we only parse actions once!)
|
||||
this.createdBy = {
|
||||
cards: {}, // only cards have a field for that
|
||||
};
|
||||
|
||||
// Map of labels Wekan ID => Wekan ID
|
||||
this.labels = {};
|
||||
// Map of swimlanes Wekan ID => Wekan ID
|
||||
this.swimlanes = {};
|
||||
// Map of lists Wekan ID => Wekan ID
|
||||
this.lists = {};
|
||||
// Map of cards Wekan ID => Wekan ID
|
||||
this.cards = {};
|
||||
// Map of comments Wekan ID => Wekan ID
|
||||
this.commentIds = {};
|
||||
// Map of attachments Wekan ID => Wekan ID
|
||||
this.attachmentIds = {};
|
||||
// Map of checklists Wekan ID => Wekan ID
|
||||
this.checklists = {};
|
||||
// Map of checklistItems Wekan ID => Wekan ID
|
||||
this.checklistItems = {};
|
||||
// The comments, indexed by Wekan card id (to map when importing cards)
|
||||
this.comments = {};
|
||||
// Map of rules Wekan ID => Wekan ID
|
||||
this.rules = {};
|
||||
// the members, indexed by Wekan member id => Wekan user ID
|
||||
this.members = data.membersMapping ? data.membersMapping : {};
|
||||
// Map of triggers Wekan ID => Wekan ID
|
||||
this.triggers = {};
|
||||
// Map of actions Wekan ID => Wekan ID
|
||||
this.actions = {};
|
||||
|
||||
// maps a wekanCardId to an array of wekanAttachments
|
||||
this.attachments = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* If dateString is provided,
|
||||
* return the Date it represents.
|
||||
* If not, will return the date when it was first called.
|
||||
* This is useful for us, as we want all import operations to
|
||||
* have the exact same date for easier later retrieval.
|
||||
*
|
||||
* @param {String} dateString a properly formatted Date
|
||||
*/
|
||||
_now(dateString) {
|
||||
if (dateString) {
|
||||
return new Date(dateString);
|
||||
}
|
||||
if (!this._nowDate) {
|
||||
this._nowDate = new Date();
|
||||
}
|
||||
return this._nowDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* if wekanUserId is provided and we have a mapping,
|
||||
* return it.
|
||||
* Otherwise return current logged user.
|
||||
* @param wekanUserId
|
||||
* @private
|
||||
*/
|
||||
_user(wekanUserId) {
|
||||
if (wekanUserId && this.members[wekanUserId]) {
|
||||
return this.members[wekanUserId];
|
||||
}
|
||||
return Meteor.userId();
|
||||
}
|
||||
|
||||
checkActivities(wekanActivities) {
|
||||
check(wekanActivities, [
|
||||
Match.ObjectIncluding({
|
||||
activityType: String,
|
||||
createdAt: DateString,
|
||||
}),
|
||||
]);
|
||||
// XXX we could perform more thorough checks based on action type
|
||||
}
|
||||
|
||||
checkBoard(wekanBoard) {
|
||||
check(
|
||||
wekanBoard,
|
||||
Match.ObjectIncluding({
|
||||
archived: Boolean,
|
||||
title: String,
|
||||
// XXX refine control by validating 'color' against a list of
|
||||
// allowed values (is it worth the maintenance?)
|
||||
color: String,
|
||||
permission: Match.Where(value => {
|
||||
return ['private', 'public'].indexOf(value) >= 0;
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
checkCards(wekanCards) {
|
||||
check(wekanCards, [
|
||||
Match.ObjectIncluding({
|
||||
archived: Boolean,
|
||||
dateLastActivity: DateString,
|
||||
labelIds: [String],
|
||||
title: String,
|
||||
sort: Number,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
checkLabels(wekanLabels) {
|
||||
check(wekanLabels, [
|
||||
Match.ObjectIncluding({
|
||||
// XXX refine control by validating 'color' against a list of allowed
|
||||
// values (is it worth the maintenance?)
|
||||
color: String,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
checkLists(wekanLists) {
|
||||
check(wekanLists, [
|
||||
Match.ObjectIncluding({
|
||||
archived: Boolean,
|
||||
title: String,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
checkSwimlanes(wekanSwimlanes) {
|
||||
check(wekanSwimlanes, [
|
||||
Match.ObjectIncluding({
|
||||
archived: Boolean,
|
||||
title: String,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
checkChecklists(wekanChecklists) {
|
||||
check(wekanChecklists, [
|
||||
Match.ObjectIncluding({
|
||||
cardId: String,
|
||||
title: String,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
checkChecklistItems(wekanChecklistItems) {
|
||||
check(wekanChecklistItems, [
|
||||
Match.ObjectIncluding({
|
||||
cardId: String,
|
||||
title: String,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
checkRules(wekanRules) {
|
||||
check(wekanRules, [
|
||||
Match.ObjectIncluding({
|
||||
triggerId: String,
|
||||
actionId: String,
|
||||
title: String,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
checkTriggers(wekanTriggers) {
|
||||
// XXX More check based on trigger type
|
||||
check(wekanTriggers, [
|
||||
Match.ObjectIncluding({
|
||||
activityType: String,
|
||||
desc: String,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
getMembersToMap(data) {
|
||||
// we will work on the list itself (an ordered array of objects) when a
|
||||
// mapping is done, we add a 'wekan' field to the object representing the
|
||||
// imported member
|
||||
const membersToMap = data.members;
|
||||
const users = data.users;
|
||||
// auto-map based on username
|
||||
membersToMap.forEach(importedMember => {
|
||||
importedMember.id = importedMember.userId;
|
||||
delete importedMember.userId;
|
||||
const user = users.filter(user => {
|
||||
return user._id === importedMember.id;
|
||||
})[0];
|
||||
if (user.profile && user.profile.fullname) {
|
||||
importedMember.fullName = user.profile.fullname;
|
||||
}
|
||||
importedMember.username = user.username;
|
||||
const wekanUser = Users.findOne({ username: importedMember.username });
|
||||
if (wekanUser) {
|
||||
importedMember.wekanId = wekanUser._id;
|
||||
}
|
||||
});
|
||||
return membersToMap;
|
||||
}
|
||||
|
||||
checkActions(wekanActions) {
|
||||
// XXX More check based on action type
|
||||
check(wekanActions, [
|
||||
Match.ObjectIncluding({
|
||||
actionType: String,
|
||||
desc: String,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
// You must call parseActions before calling this one.
|
||||
createBoardAndLabels(boardToImport) {
|
||||
const boardToCreate = {
|
||||
archived: boardToImport.archived,
|
||||
color: boardToImport.color,
|
||||
// very old boards won't have a creation activity so no creation date
|
||||
createdAt: this._now(boardToImport.createdAt),
|
||||
labels: [],
|
||||
members: [
|
||||
{
|
||||
userId: Meteor.userId(),
|
||||
wekanId: Meteor.userId(),
|
||||
isActive: true,
|
||||
isAdmin: true,
|
||||
isNoComments: false,
|
||||
isCommentOnly: false,
|
||||
swimlaneId: false,
|
||||
},
|
||||
],
|
||||
// Standalone Export has modifiedAt missing, adding modifiedAt to fix it
|
||||
modifiedAt: this._now(boardToImport.modifiedAt),
|
||||
permission: boardToImport.permission,
|
||||
slug: getSlug(boardToImport.title) || 'board',
|
||||
stars: 0,
|
||||
title: boardToImport.title,
|
||||
};
|
||||
// now add other members
|
||||
if (boardToImport.members) {
|
||||
boardToImport.members.forEach(wekanMember => {
|
||||
// do we already have it in our list?
|
||||
if (
|
||||
!boardToCreate.members.some(
|
||||
member => member.wekanId === wekanMember.wekanId,
|
||||
)
|
||||
)
|
||||
boardToCreate.members.push({
|
||||
...wekanMember,
|
||||
userId: wekanMember.wekanId,
|
||||
});
|
||||
});
|
||||
}
|
||||
boardToImport.labels.forEach(label => {
|
||||
const labelToCreate = {
|
||||
_id: Random.id(6),
|
||||
color: label.color,
|
||||
name: label.name,
|
||||
};
|
||||
// We need to remember them by Wekan ID, as this is the only ref we have
|
||||
// when importing cards.
|
||||
this.labels[label._id] = labelToCreate._id;
|
||||
boardToCreate.labels.push(labelToCreate);
|
||||
});
|
||||
const boardId = Boards.direct.insert(boardToCreate);
|
||||
Boards.direct.update(boardId, {
|
||||
$set: {
|
||||
modifiedAt: this._now(),
|
||||
},
|
||||
});
|
||||
// log activity
|
||||
Activities.direct.insert({
|
||||
activityType: 'importBoard',
|
||||
boardId,
|
||||
createdAt: this._now(),
|
||||
source: {
|
||||
id: boardToImport.id,
|
||||
system: 'Wekan',
|
||||
},
|
||||
// We attribute the import to current user,
|
||||
// not the author from the original object.
|
||||
userId: this._user(),
|
||||
});
|
||||
return boardId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the Wekan cards corresponding to the supplied Wekan cards,
|
||||
* as well as all linked data: activities, comments, and attachments
|
||||
* @param wekanCards
|
||||
* @param boardId
|
||||
* @returns {Array}
|
||||
*/
|
||||
createCards(wekanCards, boardId) {
|
||||
const result = [];
|
||||
wekanCards.forEach(card => {
|
||||
const cardToCreate = {
|
||||
archived: card.archived,
|
||||
boardId,
|
||||
// very old boards won't have a creation activity so no creation date
|
||||
createdAt: this._now(this.createdAt.cards[card._id]),
|
||||
dateLastActivity: this._now(),
|
||||
description: card.description,
|
||||
listId: this.lists[card.listId],
|
||||
swimlaneId: this.swimlanes[card.swimlaneId],
|
||||
sort: card.sort,
|
||||
title: card.title,
|
||||
// we attribute the card to its creator if available
|
||||
userId: this._user(this.createdBy.cards[card._id]),
|
||||
isOvertime: card.isOvertime || false,
|
||||
startAt: card.startAt ? this._now(card.startAt) : null,
|
||||
dueAt: card.dueAt ? this._now(card.dueAt) : null,
|
||||
spentTime: card.spentTime || null,
|
||||
};
|
||||
// add labels
|
||||
if (card.labelIds) {
|
||||
cardToCreate.labelIds = card.labelIds.map(wekanId => {
|
||||
return this.labels[wekanId];
|
||||
});
|
||||
}
|
||||
// add members {
|
||||
if (card.members) {
|
||||
const wekanMembers = [];
|
||||
// we can't just map, as some members may not have been mapped
|
||||
card.members.forEach(sourceMemberId => {
|
||||
if (this.members[sourceMemberId]) {
|
||||
const wekanId = this.members[sourceMemberId];
|
||||
// we may map multiple Wekan members to the same wekan user
|
||||
// in which case we risk adding the same user multiple times
|
||||
if (!wekanMembers.find(wId => wId === wekanId)) {
|
||||
wekanMembers.push(wekanId);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (wekanMembers.length > 0) {
|
||||
cardToCreate.members = wekanMembers;
|
||||
}
|
||||
}
|
||||
// set color
|
||||
if (card.color) {
|
||||
cardToCreate.color = card.color;
|
||||
}
|
||||
// insert card
|
||||
const cardId = Cards.direct.insert(cardToCreate);
|
||||
// keep track of Wekan id => Wekan id
|
||||
this.cards[card._id] = cardId;
|
||||
// // log activity
|
||||
// Activities.direct.insert({
|
||||
// activityType: 'importCard',
|
||||
// boardId,
|
||||
// cardId,
|
||||
// createdAt: this._now(),
|
||||
// listId: cardToCreate.listId,
|
||||
// source: {
|
||||
// id: card._id,
|
||||
// system: 'Wekan',
|
||||
// },
|
||||
// // we attribute the import to current user,
|
||||
// // not the author of the original card
|
||||
// userId: this._user(),
|
||||
// });
|
||||
// add comments
|
||||
const comments = this.comments[card._id];
|
||||
if (comments) {
|
||||
comments.forEach(comment => {
|
||||
const commentToCreate = {
|
||||
boardId,
|
||||
cardId,
|
||||
createdAt: this._now(comment.createdAt),
|
||||
text: comment.text,
|
||||
// we attribute the comment to the original author, default to current user
|
||||
userId: this._user(comment.userId),
|
||||
};
|
||||
// dateLastActivity will be set from activity insert, no need to
|
||||
// update it ourselves
|
||||
const commentId = CardComments.direct.insert(commentToCreate);
|
||||
this.commentIds[comment._id] = commentId;
|
||||
// Activities.direct.insert({
|
||||
// activityType: 'addComment',
|
||||
// boardId: commentToCreate.boardId,
|
||||
// cardId: commentToCreate.cardId,
|
||||
// commentId,
|
||||
// createdAt: this._now(commentToCreate.createdAt),
|
||||
// // we attribute the addComment (not the import)
|
||||
// // to the original author - it is needed by some UI elements.
|
||||
// userId: commentToCreate.userId,
|
||||
// });
|
||||
});
|
||||
}
|
||||
const attachments = this.attachments[card._id];
|
||||
const wekanCoverId = card.coverId;
|
||||
if (attachments) {
|
||||
attachments.forEach(att => {
|
||||
const file = new FS.File();
|
||||
// Simulating file.attachData on the client generates multiple errors
|
||||
// - HEAD returns null, which causes exception down the line
|
||||
// - the template then tries to display the url to the attachment which causes other errors
|
||||
// so we make it server only, and let UI catch up once it is done, forget about latency comp.
|
||||
const self = this;
|
||||
if (Meteor.isServer) {
|
||||
if (att.url) {
|
||||
file.attachData(att.url, function(error) {
|
||||
file.boardId = boardId;
|
||||
file.cardId = cardId;
|
||||
file.userId = self._user(att.userId);
|
||||
// The field source will only be used to prevent adding
|
||||
// attachments' related activities automatically
|
||||
file.source = 'import';
|
||||
if (error) {
|
||||
throw error;
|
||||
} else {
|
||||
const wekanAtt = Attachments.insert(file, () => {
|
||||
// we do nothing
|
||||
});
|
||||
self.attachmentIds[att._id] = wekanAtt._id;
|
||||
//
|
||||
if (wekanCoverId === att._id) {
|
||||
Cards.direct.update(cardId, {
|
||||
$set: {
|
||||
coverId: wekanAtt._id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (att.file) {
|
||||
file.attachData(
|
||||
new Buffer(att.file, 'base64'),
|
||||
{
|
||||
type: att.type,
|
||||
},
|
||||
error => {
|
||||
file.name(att.name);
|
||||
file.boardId = boardId;
|
||||
file.cardId = cardId;
|
||||
file.userId = self._user(att.userId);
|
||||
// The field source will only be used to prevent adding
|
||||
// attachments' related activities automatically
|
||||
file.source = 'import';
|
||||
if (error) {
|
||||
throw error;
|
||||
} else {
|
||||
const wekanAtt = Attachments.insert(file, () => {
|
||||
// we do nothing
|
||||
});
|
||||
this.attachmentIds[att._id] = wekanAtt._id;
|
||||
//
|
||||
if (wekanCoverId === att._id) {
|
||||
Cards.direct.update(cardId, {
|
||||
$set: {
|
||||
coverId: wekanAtt._id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
// todo XXX set cover - if need be
|
||||
});
|
||||
}
|
||||
result.push(cardId);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Create labels if they do not exist and load this.labels.
|
||||
createLabels(wekanLabels, board) {
|
||||
wekanLabels.forEach(label => {
|
||||
const color = label.color;
|
||||
const name = label.name;
|
||||
const existingLabel = board.getLabel(name, color);
|
||||
if (existingLabel) {
|
||||
this.labels[label.id] = existingLabel._id;
|
||||
} else {
|
||||
const idLabelCreated = board.pushLabel(name, color);
|
||||
this.labels[label.id] = idLabelCreated;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createLists(wekanLists, boardId) {
|
||||
wekanLists.forEach((list, listIndex) => {
|
||||
const listToCreate = {
|
||||
archived: list.archived,
|
||||
boardId,
|
||||
// We are being defensing here by providing a default date (now) if the
|
||||
// creation date wasn't found on the action log. This happen on old
|
||||
// Wekan boards (eg from 2013) that didn't log the 'createList' action
|
||||
// we require.
|
||||
createdAt: this._now(this.createdAt.lists[list.id]),
|
||||
title: list.title,
|
||||
sort: list.sort ? list.sort : listIndex,
|
||||
};
|
||||
const listId = Lists.direct.insert(listToCreate);
|
||||
Lists.direct.update(listId, {
|
||||
$set: {
|
||||
updatedAt: this._now(),
|
||||
},
|
||||
});
|
||||
this.lists[list._id] = listId;
|
||||
// // log activity
|
||||
// Activities.direct.insert({
|
||||
// activityType: 'importList',
|
||||
// boardId,
|
||||
// createdAt: this._now(),
|
||||
// listId,
|
||||
// source: {
|
||||
// id: list._id,
|
||||
// system: 'Wekan',
|
||||
// },
|
||||
// // We attribute the import to current user,
|
||||
// // not the creator of the original object
|
||||
// userId: this._user(),
|
||||
// });
|
||||
});
|
||||
}
|
||||
|
||||
createSwimlanes(wekanSwimlanes, boardId) {
|
||||
wekanSwimlanes.forEach((swimlane, swimlaneIndex) => {
|
||||
const swimlaneToCreate = {
|
||||
archived: swimlane.archived,
|
||||
boardId,
|
||||
// We are being defensing here by providing a default date (now) if the
|
||||
// creation date wasn't found on the action log. This happen on old
|
||||
// Wekan boards (eg from 2013) that didn't log the 'createList' action
|
||||
// we require.
|
||||
createdAt: this._now(this.createdAt.swimlanes[swimlane._id]),
|
||||
title: swimlane.title,
|
||||
sort: swimlane.sort ? swimlane.sort : swimlaneIndex,
|
||||
};
|
||||
// set color
|
||||
if (swimlane.color) {
|
||||
swimlaneToCreate.color = swimlane.color;
|
||||
}
|
||||
const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate);
|
||||
Swimlanes.direct.update(swimlaneId, {
|
||||
$set: {
|
||||
updatedAt: this._now(),
|
||||
},
|
||||
});
|
||||
this.swimlanes[swimlane._id] = swimlaneId;
|
||||
});
|
||||
}
|
||||
|
||||
createChecklists(wekanChecklists) {
|
||||
const result = [];
|
||||
wekanChecklists.forEach((checklist, checklistIndex) => {
|
||||
// Create the checklist
|
||||
const checklistToCreate = {
|
||||
cardId: this.cards[checklist.cardId],
|
||||
title: checklist.title,
|
||||
createdAt: checklist.createdAt,
|
||||
sort: checklist.sort ? checklist.sort : checklistIndex,
|
||||
};
|
||||
const checklistId = Checklists.direct.insert(checklistToCreate);
|
||||
this.checklists[checklist._id] = checklistId;
|
||||
result.push(checklistId);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
createTriggers(wekanTriggers, boardId) {
|
||||
wekanTriggers.forEach(trigger => {
|
||||
if (trigger.hasOwnProperty('labelId')) {
|
||||
trigger.labelId = this.labels[trigger.labelId];
|
||||
}
|
||||
if (trigger.hasOwnProperty('memberId')) {
|
||||
trigger.memberId = this.members[trigger.memberId];
|
||||
}
|
||||
trigger.boardId = boardId;
|
||||
const oldId = trigger._id;
|
||||
delete trigger._id;
|
||||
this.triggers[oldId] = Triggers.direct.insert(trigger);
|
||||
});
|
||||
}
|
||||
|
||||
createActions(wekanActions, boardId) {
|
||||
wekanActions.forEach(action => {
|
||||
if (action.hasOwnProperty('labelId')) {
|
||||
action.labelId = this.labels[action.labelId];
|
||||
}
|
||||
if (action.hasOwnProperty('memberId')) {
|
||||
action.memberId = this.members[action.memberId];
|
||||
}
|
||||
action.boardId = boardId;
|
||||
const oldId = action._id;
|
||||
delete action._id;
|
||||
this.actions[oldId] = Actions.direct.insert(action);
|
||||
});
|
||||
}
|
||||
|
||||
createRules(wekanRules, boardId) {
|
||||
wekanRules.forEach(rule => {
|
||||
// Create the rule
|
||||
rule.boardId = boardId;
|
||||
rule.triggerId = this.triggers[rule.triggerId];
|
||||
rule.actionId = this.actions[rule.actionId];
|
||||
delete rule._id;
|
||||
Rules.direct.insert(rule);
|
||||
});
|
||||
}
|
||||
|
||||
createChecklistItems(wekanChecklistItems) {
|
||||
wekanChecklistItems.forEach((checklistitem, checklistitemIndex) => {
|
||||
// Create the checklistItem
|
||||
const checklistItemTocreate = {
|
||||
title: checklistitem.title,
|
||||
checklistId: this.checklists[checklistitem.checklistId],
|
||||
cardId: this.cards[checklistitem.cardId],
|
||||
sort: checklistitem.sort ? checklistitem.sort : checklistitemIndex,
|
||||
isFinished: checklistitem.isFinished,
|
||||
};
|
||||
const checklistItemId = ChecklistItems.direct.insert(
|
||||
checklistItemTocreate,
|
||||
);
|
||||
this.checklistItems[checklistitem._id] = checklistItemId;
|
||||
});
|
||||
}
|
||||
|
||||
parseActivities(wekanBoard) {
|
||||
wekanBoard.activities.forEach(activity => {
|
||||
switch (activity.activityType) {
|
||||
case 'addAttachment': {
|
||||
// We have to be cautious, because the attachment could have been removed later.
|
||||
// In that case Wekan still reports its addition, but removes its 'url' field.
|
||||
// So we test for that
|
||||
const wekanAttachment = wekanBoard.attachments.filter(attachment => {
|
||||
return attachment._id === activity.attachmentId;
|
||||
})[0];
|
||||
|
||||
if (typeof wekanAttachment !== 'undefined' && wekanAttachment) {
|
||||
if (wekanAttachment.url || wekanAttachment.file) {
|
||||
// we cannot actually create the Wekan attachment, because we don't yet
|
||||
// have the cards to attach it to, so we store it in the instance variable.
|
||||
const wekanCardId = activity.cardId;
|
||||
if (!this.attachments[wekanCardId]) {
|
||||
this.attachments[wekanCardId] = [];
|
||||
}
|
||||
this.attachments[wekanCardId].push(wekanAttachment);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'addComment': {
|
||||
const wekanComment = wekanBoard.comments.filter(comment => {
|
||||
return comment._id === activity.commentId;
|
||||
})[0];
|
||||
const id = activity.cardId;
|
||||
if (!this.comments[id]) {
|
||||
this.comments[id] = [];
|
||||
}
|
||||
this.comments[id].push(wekanComment);
|
||||
break;
|
||||
}
|
||||
case 'createBoard': {
|
||||
this.createdAt.board = activity.createdAt;
|
||||
break;
|
||||
}
|
||||
case 'createCard': {
|
||||
const cardId = activity.cardId;
|
||||
this.createdAt.cards[cardId] = activity.createdAt;
|
||||
this.createdBy.cards[cardId] = activity.userId;
|
||||
break;
|
||||
}
|
||||
case 'createList': {
|
||||
const listId = activity.listId;
|
||||
this.createdAt.lists[listId] = activity.createdAt;
|
||||
break;
|
||||
}
|
||||
case 'createSwimlane': {
|
||||
const swimlaneId = activity.swimlaneId;
|
||||
this.createdAt.swimlanes[swimlaneId] = activity.createdAt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
importActivities(activities, boardId) {
|
||||
activities.forEach(activity => {
|
||||
switch (activity.activityType) {
|
||||
// Board related activities
|
||||
// TODO: addBoardMember, removeBoardMember
|
||||
case 'createBoard': {
|
||||
Activities.direct.insert({
|
||||
userId: this._user(activity.userId),
|
||||
type: 'board',
|
||||
activityTypeId: boardId,
|
||||
activityType: activity.activityType,
|
||||
boardId,
|
||||
createdAt: this._now(activity.createdAt),
|
||||
});
|
||||
break;
|
||||
}
|
||||
// List related activities
|
||||
// TODO: removeList, archivedList
|
||||
case 'createList': {
|
||||
Activities.direct.insert({
|
||||
userId: this._user(activity.userId),
|
||||
type: 'list',
|
||||
activityType: activity.activityType,
|
||||
listId: this.lists[activity.listId],
|
||||
boardId,
|
||||
createdAt: this._now(activity.createdAt),
|
||||
});
|
||||
break;
|
||||
}
|
||||
// Card related activities
|
||||
// TODO: archivedCard, restoredCard, joinMember, unjoinMember
|
||||
case 'createCard': {
|
||||
Activities.direct.insert({
|
||||
userId: this._user(activity.userId),
|
||||
activityType: activity.activityType,
|
||||
listId: this.lists[activity.listId],
|
||||
cardId: this.cards[activity.cardId],
|
||||
boardId,
|
||||
createdAt: this._now(activity.createdAt),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'moveCard': {
|
||||
Activities.direct.insert({
|
||||
userId: this._user(activity.userId),
|
||||
oldListId: this.lists[activity.oldListId],
|
||||
activityType: activity.activityType,
|
||||
listId: this.lists[activity.listId],
|
||||
cardId: this.cards[activity.cardId],
|
||||
boardId,
|
||||
createdAt: this._now(activity.createdAt),
|
||||
});
|
||||
break;
|
||||
}
|
||||
// Comment related activities
|
||||
case 'addComment': {
|
||||
Activities.direct.insert({
|
||||
userId: this._user(activity.userId),
|
||||
activityType: activity.activityType,
|
||||
cardId: this.cards[activity.cardId],
|
||||
commentId: this.commentIds[activity.commentId],
|
||||
boardId,
|
||||
createdAt: this._now(activity.createdAt),
|
||||
});
|
||||
break;
|
||||
}
|
||||
// Attachment related activities
|
||||
case 'addAttachment': {
|
||||
Activities.direct.insert({
|
||||
userId: this._user(activity.userId),
|
||||
type: 'card',
|
||||
activityType: activity.activityType,
|
||||
attachmentId: this.attachmentIds[activity.attachmentId],
|
||||
cardId: this.cards[activity.cardId],
|
||||
boardId,
|
||||
createdAt: this._now(activity.createdAt),
|
||||
});
|
||||
break;
|
||||
}
|
||||
// Checklist related activities
|
||||
case 'addChecklist': {
|
||||
Activities.direct.insert({
|
||||
userId: this._user(activity.userId),
|
||||
activityType: activity.activityType,
|
||||
cardId: this.cards[activity.cardId],
|
||||
checklistId: this.checklists[activity.checklistId],
|
||||
boardId,
|
||||
createdAt: this._now(activity.createdAt),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'addChecklistItem': {
|
||||
Activities.direct.insert({
|
||||
userId: this._user(activity.userId),
|
||||
activityType: activity.activityType,
|
||||
cardId: this.cards[activity.cardId],
|
||||
checklistId: this.checklists[activity.checklistId],
|
||||
checklistItemId: activity.checklistItemId.replace(
|
||||
activity.checklistId,
|
||||
this.checklists[activity.checklistId],
|
||||
),
|
||||
boardId,
|
||||
createdAt: this._now(activity.createdAt),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//check(board) {
|
||||
check() {
|
||||
//try {
|
||||
// check(data, {
|
||||
// membersMapping: Match.Optional(Object),
|
||||
// });
|
||||
// this.checkActivities(board.activities);
|
||||
// this.checkBoard(board);
|
||||
// this.checkLabels(board.labels);
|
||||
// this.checkLists(board.lists);
|
||||
// this.checkSwimlanes(board.swimlanes);
|
||||
// this.checkCards(board.cards);
|
||||
//this.checkChecklists(board.checklists);
|
||||
// this.checkRules(board.rules);
|
||||
// this.checkActions(board.actions);
|
||||
//this.checkTriggers(board.triggers);
|
||||
//this.checkChecklistItems(board.checklistItems);
|
||||
//} catch (e) {
|
||||
// throw new Meteor.Error('error-json-schema');
|
||||
// }
|
||||
}
|
||||
|
||||
create(board, currentBoardId) {
|
||||
// TODO : Make isSandstorm variable global
|
||||
const isSandstorm =
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.sandstorm;
|
||||
if (isSandstorm && currentBoardId) {
|
||||
const currentBoard = Boards.findOne(currentBoardId);
|
||||
currentBoard.archive();
|
||||
}
|
||||
this.parseActivities(board);
|
||||
const boardId = this.createBoardAndLabels(board);
|
||||
this.createLists(board.lists, boardId);
|
||||
this.createSwimlanes(board.swimlanes, boardId);
|
||||
this.createCards(board.cards, boardId);
|
||||
this.createChecklists(board.checklists);
|
||||
this.createChecklistItems(board.checklistItems);
|
||||
this.importActivities(board.activities, boardId);
|
||||
this.createTriggers(board.triggers, boardId);
|
||||
this.createActions(board.actions, boardId);
|
||||
this.createRules(board.rules, boardId);
|
||||
// XXX add members
|
||||
return boardId;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
dist: eoan
|
||||
dist: focal
|
||||
sudo: required
|
||||
|
||||
env:
|
||||
TRAVIS_DOCKER_COMPOSE_VERSION: 1.24.0
|
||||
TRAVIS_NODE_VERSION: 12.15.0
|
||||
TRAVIS_NODE_VERSION: 12.18.3
|
||||
TRAVIS_NPM_VERSION: latest
|
||||
|
||||
before_install:
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ host = https://www.transifex.com
|
||||
# tap:i18n requires us to use `-` separator in the language identifiers whereas
|
||||
# Transifex uses a `_` separator, without an option to customize it on one side
|
||||
# or the other, so we need to do a Manual mapping.
|
||||
lang_map = bg_BG:bg, en_GB:en-GB, es_AR:es-AR, el_GR:el, fi_FI:fi, hu_HU:hu, id_ID:id, mn_MN:mn, no:nb, lv_LV:lv, pt_BR:pt-BR, ro_RO:ro, sl_SI:sl, zh_CN:zh-CN, zh_TW:zh-TW, zh_HK:zh-HK
|
||||
lang_map = bg_BG:bg, en_GB:en-GB, es_AR:es-AR, es_CL:es-CL, es_PE:es-PE, el_GR:el, fi_FI:fi, hu_HU:hu, id_ID:id, mn_MN:mn, no:nb, lv_LV:lv, pt_BR:pt-BR, ro_RO:ro, sl_SI:sl, zh_CN:zh-CN, zh_TW:zh-TW, zh_HK:zh-HK
|
||||
|
||||
[wekan.application]
|
||||
file_filter = i18n/<lang>.i18n.json
|
||||
|
||||
+819
-2
@@ -1,3 +1,820 @@
|
||||
# v4.30 2020-09-13 Wekan release
|
||||
|
||||
This release adds the following new features and improvements:
|
||||
|
||||
- [Add setting for OAUTH2_ADFS_ENABLED=true](https://github.com/wekan/wekan/pull/3269)
|
||||
for [SSO Integration with ADFS 4.0 using OAuth 2 and OpenID](https://github.com/wekan/wekan/issues/3184).
|
||||
Thanks to phaseshift3r.
|
||||
- [Add setting OAUTH2_ADFS_ENABLED=false for most platforms. Remove mouse scroll settings of already removed custom scrollbar.
|
||||
Add testing for both string and boolean version of true](https://github.com/wekan/wekan/commit/f6bdb4d694453d73f4bfa6a75814833594cf5000).
|
||||
Thanks to xet7.
|
||||
- [Design improvements in templates, card details and custom fields](https://github.com/wekan/wekan/pull/3271)
|
||||
and [related change to translation](https://github.com/wekan/wekan/commit/fe40c5fd37a7c54240c080caf98b6130229f5d31).
|
||||
Thanks to helioguardabaxo and xet7.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Use forked & updated version of gridfs-stream](https://github.com/wekan/wekan/pull/3270).
|
||||
Thanks to blaggacao.
|
||||
- [Update dependencies](https://github.com/wekan/wekan/pull/3268).
|
||||
Thanks to blaggacao.
|
||||
- [Update npm-mongo dependency](https://github.com/wekan/wekan/commit/9fdafd20081b20302af3d1a6397fb840348f1209).
|
||||
Thanks to filipenevola.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.29 2020-09-11
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Changed markdown from marked to markdown-it](https://github.com/wekan/wekan/commit/20b01771055ca4d8871d13abb559ab92ecee10f4) and
|
||||
added emoji support https://github.com/wekan/wekan/wiki/Emoji .
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.28 2020-09-11 Wekan release
|
||||
|
||||
This release updates some dependencies:
|
||||
|
||||
- [Update some dependencies](https://github.com/wekan/wekan/commit/125c4684bd6815a8f49241bc2663e82112afe67b).
|
||||
Thanks to developers of dependencies.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix card scrollbar on Windows](https://github.com/wekan/wekan/pull/3264).
|
||||
Thanks to tborychowski.
|
||||
- [Try to fix language names](https://github.com/wekan/wekan/commit/f81fd8084fd6cd1ad57daefcc22ed1fb0acaaeca).
|
||||
Thanks to buplet, xoas and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.27 2020-09-09 Wekan release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Reverted incomplete fix for "Checklist + card title with starting number and point", because it disabled some markdown.
|
||||
Also more fixes to GFM checklist not displayed properly](https://github.com/wekan/wekan/commit/bf18792d7733d6e6cfb61a8d6db4caafdcc19b34).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.26 2020-09-05 Wekan release
|
||||
|
||||
This release adds the following quality checks:
|
||||
|
||||
- [Added GitHub automatic code quality analysis](https://github.com/wekan/wekan/commit/df35683043603f6ecb9bd4f2a4b357e374397ad1).
|
||||
Thanks to xet7.
|
||||
|
||||
and updates the following dependencies:
|
||||
|
||||
- [bl](https://github.com/wekan/wekan/commit/7ec671bb9f8a33c5eb28c26b98143f9b4cd9b958).
|
||||
Thanks to developers of dependencies.
|
||||
- [Delete markdown demo that is not in use](https://github.com/wekan/wekan/commit/d344c39d497cc291ee7927fdda900dc8bac22bc2).
|
||||
Thanks to xet7.
|
||||
- [Update markdown and xss](https://github.com/wekan/wekan/commit/cfcbf640d64bdfc4f3a482c32e35f396e1a22191)
|
||||
Thanks to developers of dependencies.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Disable list formatting and converting to HTML. This fixes markdown numbering and viewing bugs](https://github.com/wekan/wekan/commit/41b1c55988a9a65005ac0b9e1ddcc0596c047a49).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.25 2020-08-31 Wekan release
|
||||
|
||||
This release adds the following docker-compose.yml changes:
|
||||
|
||||
- [Mongo 4.4 does not work. Mongo 4.2 and 3.x works](https://github.com/wekan/wekan/commit/5d2daa4a80c819f0610ff2f17589de1e1085836c).
|
||||
Thanks to GuidoDr for info. Related https://github.com/wekan/wekan/issues/3247
|
||||
|
||||
and adds the following Nextcloud documentation:
|
||||
|
||||
- [Improving documentation for Nextcloud integration](https://github.com/wekan/wekan/pull/3248).
|
||||
Thanks to relikd.
|
||||
|
||||
and removes the following code and allows double quotes in code:
|
||||
|
||||
- Removed custom scrollbar [1](https://github.com/wekan/wekan/pull/3246) and [2](https://github.com/wekan/wekan/commit/5870d38e8e63159ede8c18d1766a4f9f6ba8987c).
|
||||
Also in eslint settings allowed double quotes in code. Thanks to tborychowski and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.24 2020-08-27 Wekan release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- [Upgrade to Meteor 1.11](https://github.com/wekan/wekan/commit/4d49265b25595444553e1c2d6e48c7a699949654).
|
||||
Thanks to Meteor developers.
|
||||
- [Update bcrypt](https://github.com/wekan/wekan/commit/dee7020a5aaa90c8580ef42fa73aff0ca4ae3e12).
|
||||
Thanks to bcrypt developers.
|
||||
- [Update dependencies](https://github.com/wekan/wekan/commit/60b2787c559b9966d6040a622c5b971fa95241c3).
|
||||
Thanks to developers of dependencies.
|
||||
|
||||
and adds the following translations:
|
||||
|
||||
- [Add Trigger and Action header words to Rule Details](https://github.com/wekan/wekan/pull/3244).
|
||||
Thanks to helioguardabaxo.
|
||||
- [Add Spanish (Peru) (es_PE)](https://github.com/wekan/wekan/commit/b9f87bf310b4f071c8219bb7511b15a7fa27340d).
|
||||
Thanks to translators.
|
||||
|
||||
and adds the following mouse scroll settings:
|
||||
|
||||
- [Add setting for mouse scroll deltafactor. Fix snap setting for mouse scroll amount](https://github.com/wekan/wekan/commit/7e4b791c2964f4b130abbaee62ffdff1536450c4).
|
||||
Thanks to danger89 and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.23 2020-08-13 Wekan release
|
||||
|
||||
This release fixes following CRITICAL VULNERABILITIES:
|
||||
|
||||
- [Update vulnerable dependency elliptic that is dependency of meteor-node-stubs that is dependency of
|
||||
Wekan](https://github.com/wekan/wekan/commit/910f0cecbe7a4b3fdff603e5e74c2cb1c40b660b).
|
||||
Thanks to filipenevola, neeldug, L25inux and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.22 2020-07-23 Wekan release
|
||||
|
||||
This release adds back these features:
|
||||
|
||||
- [Export to JSON and HTML .zip file](https://github.com/commit/1624fc82f7c319e84a78f29445c7867f7da15c32)
|
||||
that also fixes #3216 Clone Boards not working.
|
||||
Thanks to xet7.
|
||||
|
||||
and hides these features temporarily:
|
||||
|
||||
- [Hide CSV export until it's fixed in EdgeHTML compatible way](https://github.com/wekan/wekan/commit/045b8a84a29dde09201dd5108c757719d00e6f55).
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Upgrade to Node 12.18.3](https://github.com/wekan/wekan/commit/6f503ca818abff17a20b6612aeea1f9e2c4a8234).
|
||||
Thanks to Node developers.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix Snap](https://github.com/wekan/wekan/commit/68391a943bd37d9f98819ffb7b7a29692d0bd380).
|
||||
This fix was already included to Wekan v4.21 to get it released.
|
||||
TODO: Sometime migrate from Caddy v1 to Caddy v2.
|
||||
Thanks to xet7.
|
||||
- [Fix detecting current IP address on rebuild-wekan.sh](https://github.com/wekan/wekan/commit/ec1d8f275ff4cd720a8cd3bc918b32f9c5f5d099).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.21 2020-07-21 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [REST API: Changed edit_card and get_card_by_customfields to return full documents](https://github.com/wekan/wekan/pull/3215).
|
||||
Thanks to gvespignani70.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.20 2020-07-20 Wekan release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- Update dependencies [Part1](https://github.com/wekan/wekan/commit/419615bed43b6e9de4030193c47137a066b85bde) and
|
||||
[Part2](https://github.com/wekan/wekan/commit/116372e11e09ce9b8376a8694553add595e02815).
|
||||
Thanks to developers of dependencies and xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Change slug on card rename](https://github.com/wekan/wekan/pull/3214).
|
||||
Thanks to NicoP-S.
|
||||
- [Add missing Wekan logo sizes for PWAs and Apps](https://github.com/wekan/wekan/commit/de28bf8569a7373a5d6fd60a4f413e76673adc26).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.19 2020-07-18 Wekan release
|
||||
|
||||
This release adds the following features:
|
||||
|
||||
- [Add support for EdgeHTML browser (Microsoft Legacy Edge, not based on Chromium) by removing incompatible csv-stringify package.
|
||||
CSV export will be fixed later](https://github.com/wekan/wekan/commit/b9a4b0b51d3692fcbb715b1afc875f21cd204ae5).
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- Update dependencies [Part1](https://github.com/wekan/wekan/commit/23ee93ca3d4ea161a93627a8e28e1ce93eea1bab),
|
||||
[Part2](https://github.com/wekan/wekan/commit/6646d48ccbaf04c4935de35fe037eff3bd7fd469),
|
||||
[Part3](https://github.com/wekan/wekan/commit/87cb4598f745a362aaac06b8b457198c40aaf61e),
|
||||
[Part4](https://github.com/wekan/wekan/commit/f57ed2990f5c6e1af10d270b24c7092805711afe).
|
||||
Thanks to developers of dependencies and xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Checklist Item PUT API: boolean cast on isFinished](https://github.com/wekan/wekan/pull/3211).
|
||||
Thanks to Robert-Lebedeu.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.18 2020-07-10 Wekan release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- [Upgrade to Node 12.18.2](https://github.com/wekan/wekan/commit/6e4407ed9cb3c95a99e0dbbb4383324dd57d6be1).
|
||||
Thanks to Node developers and xet7.
|
||||
- [Update dependencies](https://github.com/wekan/wekan/commit/05cd1247ab935f586d747743bb9cd79d23e0b1e6).
|
||||
Thanks to dependency developers and xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [All logged in users are now allowed to reorder boards by dragging at All Boards page and Public Boards page](https://github.com/wekan/wekan/commit/ba24c4e40c728d030504ed21ccf79247d0449e1b).
|
||||
Thanks to xet7.
|
||||
- [Fix running meteor for dev in rebuild-wekan.sh](https://github.com/wekan/wekan/commit/a77cf56fbdaf0b74d8b97aa41b0a88fee85e3ee1).
|
||||
Thanks to xet7.
|
||||
- [Fix start-wekan.bat](https://github.com/wekan/wekan/commit/0be1c00fccef8797a1b3612593a6623a9b465e0d) and
|
||||
[Windows bundle install](https://github.com/wekan/wekan/wiki/Windows#a-bundle-with-windows-nodemongodb).
|
||||
Thanks to xet7.
|
||||
- [Fix typo](https://github.com/wekan/wekan/pull/3197).
|
||||
Thanks to Lua00808.
|
||||
- [Fix creating user misbehaving in some special case](https://github.com/wekan/wekan/pull/3206).
|
||||
Thanks to salleman33.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.17 2020-06-18 Wekan release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Revert finding correct user changes that were made at Wekan v4.16](https://github.com/wekan/wekan/commit/5eb378452761ad1d6d67a491316007fdf6dfd689).
|
||||
Thanks to xet7.
|
||||
- [Fix activities view on mobile devices](https://github.com/wekan/wekan/pull/3183).
|
||||
Thanks to marc1006.
|
||||
- [Add back checks about can user export CSV/TSV](https://github.com/wekan/wekan/commit/afe00d02cddf016a3ccc1ed9a98a7f10d3339f26).
|
||||
Thanks to marc1006 and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.16 2020-06-17 Wekan release
|
||||
|
||||
This release adds the following features:
|
||||
|
||||
- [Add find-replace.sh script for development](https://github.com/wekan/wekan/commit/bda49ed60947e0438206b2f55119f5c5c132c734).
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Upgrade to Node 12.18.1](https://github.com/wekan/wekan/commit/b11ae567c9b2d16a115ea4f3f7f88e67d076f326).
|
||||
Thanks to Node developers and xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [OpenAPI: Fix jsdoc/operation matching](https://github.com/wekan/wekan/pull/3171).
|
||||
Thanks to bentiss.
|
||||
- Fix finding corrent user [Part1](https://github.com/wekan/wekan/pull/3180) and
|
||||
[Part2](https://github.com/wekan/wekan/commit/f245b6b7faa29b4f276527daca48c305fe9689c1).
|
||||
Thanks to salleman33 and xet7.
|
||||
- [Try to prevent errors on CSV/TSV export](https://github.com/wekan/wekan/commit/b00db983c8506e0cdc9968e452c3c8025fc10776).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.15 2020-06-16 Wekan release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- Fix lint errors [Part1](https://github.com/wekan/wekan/commit/f1587753cb0bba38e4b1df2e0300d3dc2826da72) and
|
||||
[Part2](https://github.com/wekan/wekan/commit/e6629779f77676eadfe4465c407f0bee0ec64061).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.14 2020-06-16 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Add user option to hide finished checklist items. Strikethrough checked items](https://github.com/wekan/wekan/pull/3167).
|
||||
Thanks to marc1006.
|
||||
- [Added the possibility to start a vote via API edit_card. And added some better visibility to see what was voted](https://github.com/wekan/wekan/pull/3170).
|
||||
Thanks to NicoP-S.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Update dependencies](https://github.com/wekan/wekan/commit/8f34cdc279602e97085e0a504f7716495349f83c).
|
||||
Thanks to xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix infinite scrolling for activities](https://github.com/wekan/wekan/pull/3168).
|
||||
Thanks to marc1006.
|
||||
- [Remove top and bottom margin for hidden checklist items](https://github.com/wekan/wekan/pull/3172).
|
||||
Thanks to marc1006.
|
||||
- [Alignment and spacing of minicard labels](https://github.com/wekan/wekan/pull/3174).
|
||||
Thanks to hgustafsson.
|
||||
- [Fix: Unable to delete a custom field in a board](https://github.com/wekan/wekan/commit/3b2b1087447bc8613baa8254bfec55e3d485bdc4).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.13 2020-06-09 Wekan release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- [OpenShift template updates](https://github.com/wekan/wekan/pull/3158), Thanks to jimmyjones2:
|
||||
1) Remove status fields (this is created by Kubernetes at run time)
|
||||
2) The latest MongoDB by default available with OpenShift is 3.6
|
||||
3) Change MongoDB service name to contain wekan to avoid potentially conflicting with other mongodb instances in the same project.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Copy the labels only if the target board is different](https://github.com/wekan/wekan/pull/3154).
|
||||
Thanks to marc1006.
|
||||
- [Fix condition whether a card is in list](https://github.com/wekan/wekan/pull/3165).
|
||||
Thanks to marc1006.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.12 2020-06-08 Wekan release
|
||||
|
||||
This release fixes the following CRITICAL SECURITY VULNERABILITIES:
|
||||
|
||||
- Fix XSS bug reported 2020-05-24 by [swsjona](https://twitter.com/swsjona):
|
||||
[Part 1](https://github.com/wekan/wekan/commit/1f85b25549b50602380f1745f19e5fe44fe36d6f),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/fb44df981581354bf23a6928427ad2bf73c4550f),
|
||||
[Part 3](https://github.com/wekan/wekan/commit/99f68f36b028d6c75acf2e5b83585b1acee65f97),
|
||||
[Part 4](https://github.com/wekan/wekan/commit/8a622ec7c3043bf8f34399ef34563e6a9a19dcd8).
|
||||
Logged in users could run javascript in input fields. This was partially fixed at v3.85,
|
||||
but at some fields XSS was still possible. This affects at least Wekan versions v3.12-v4.12.
|
||||
After this fix, Javascript in input fields is not executed.
|
||||
Thanks to swsjona, marc1006 and xet7.
|
||||
|
||||
and adds the following new features:
|
||||
|
||||
- Change default view to Swimlanes:
|
||||
[Part 1](https://github.com/wekan/wekan/commit/8c3322f9a93c321e8a2cc5cfcd4b1d6316a5fb7c),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/61e682470cdaef42cce2d74b41fb752cfc61848b),
|
||||
[Part 3 Change dropdown order to Swimlanes/Lists/Calendar](https://github.com/wekan/wekan/commit/7f6d500cbec15496ae357b05b9df3f10e51ed1f1),
|
||||
[Part 4.1. Public board default view to Swimlane. Part 4.2. When changing Public board
|
||||
view (sets view cookie), also reload page so view is changed
|
||||
immediately](https://github.com/wekan/wekan/commit/39519d1cc944c567837be0f88ab4a037e2144c61).
|
||||
Thanks to xet7.
|
||||
- [Use markdown in Swimlane titles](https://github.com/wekan/wekan/commit/6b22f96313354b45b851b93c25aa392bbe346bdb).
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Update minifier-css](https://github.com/wekan/wekan/commit/cb1e91fee83eaad1e926c288c0abfc1e4f2a8bd4).
|
||||
Thanks to xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- Fix indent [Part1](https://github.com/wekan/wekan/commit/415e94d187ffcb9a4afaecc5c6960a50a87ca7eb),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/96494bacf550cde65598e6d59199517f311aa33d).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.11 2020-06-04 Wekan release
|
||||
|
||||
This release adds the following new platforms:
|
||||
|
||||
- [Using arm64 bundle on Raspberry Pi OS arm64 with MongoDB 4.2.x for RasPi3 and
|
||||
RasPi4](https://github.com/wekan/wekan/wiki/Raspberry-Pi#raspberry-pi-os-arm64-with-mongodb-42x).
|
||||
Thanks to Raspberry Pi OS devs, MongoDB devs and xet7.
|
||||
- [s390x RHEL 8](https://github.com/wekan/wekan/wiki/s390x#rhel-8).
|
||||
Thanks to IBM, Red Hat Linux, Linux Foundation and xet7.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Upgrade to Node v12.18.0](https://github.com/wekan/wekan/commit/d9d451a206cabe7f6ca8ad5d35eb76443198e4c1).
|
||||
Thanks to Node developers and xet7.
|
||||
- [Update `markedjs` package](https://github.com/wekan/wekan/pull/3149).
|
||||
Thanks to marc1006.
|
||||
- [Add fibers](https://github.com/wekan/wekan/commit/cd49018306f826fff37b7024dfde9de05d88b620).
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following new features:
|
||||
|
||||
- [Add Calendar Month Event List view](https://github.com/wekan/wekan/commit/f73ea218eefba3f0d6c642849dfede9e03052d25).
|
||||
Thanks to xet7.
|
||||
- [Added dates & assignees to REST API calls](https://github.com/wekan/wekan/pull/3146).
|
||||
Thanks to GitGramm.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.10 2020-05-30 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Added an API to get the cards for a specific custom field value](https://github.com/wekan/wekan/pulls/3131).
|
||||
Thanks to gvespignani70.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Upgrade to Node v12.17.0](https://github.com/wekan/wekan/commit/3ade9d95a69b269c345127e1755e1b539dc07263).
|
||||
Thanks to Node developers and xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix email verification in `sendSMTPTestEmail`](https://github.com/wekan/wekan/pull/3135).
|
||||
Thanks to marc1006.
|
||||
- [Try to Fix Registration broken "Templates board id is required" with ugly hack. If it works, ugly becomes
|
||||
beautiful](https://github.com/wekan/wekan/pull/3140).
|
||||
Thanks to marc1006.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.09 2020-05-27 Wekan release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Fix vote export & export/import currency custom field to CSV/TSV](https://github.com/wekan/wekan/pull/3128).
|
||||
Thanks to brymut.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.08 2020-05-26 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Add the 'Currency' Custom Field type](https://github.com/wekan/wekan/pull/3123).
|
||||
Thanks to habenamare.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Add some changes to Modern theme](https://github.com/wekan/wekan/commit/6a1bc167cf10e75d61b3196db9eac2978d70ad8e).
|
||||
Thanks to jeroenstoker and xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix typo that caused parse error](https://github.com/wekan/wekan/commit/351d9d0c9577c9d543d543bc12a51388b0141324).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.07 2020-05-26 Wekan release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Fix move selection](https://github.com/wekan/wekan/pull/3120).
|
||||
Thanks to marc1006.
|
||||
- [Fix Python API generation](https://github.com/wekan/wekan/pull/3121).
|
||||
Thanks to marc1006.
|
||||
- [Fix default value of `sort`](https://github.com/wekan/wekan/pull/3122).
|
||||
Thanks to marc1006.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.06 2020-05-25 Wekan release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Fix Card export CSV, check for vote
|
||||
undefined](https://github.com/wekan/wekan/commit/8eafa1ac66fdcf5fb5f0a95aa6cfee454ddad67f).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.05 2020-05-25 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Import/Export Custom Fields in CSV/TSV](https://github.com/wekan/wekan/pull/3115).
|
||||
Thanks to brymut.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Update packages](https://github.com/wekan/wekan/commit/3b44acd87c35340bf9fe5d210f4402f1b1a1dfdf).
|
||||
Thanks to xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- Try to fix Snap [1](https://github.com/wekan/wekan/commit/6fad68b9b9afd8de7074037d73eeac40f6a3f7c1), [2](https://github.com/wekan/wekan/commit/b737adfcdfc9b8084a7eb84420a89c014bbec1fb). Later reverted those like other ostrio-files changes too.
|
||||
Thanks to xet7.
|
||||
- [Add default attachments store path /var/snap/wekan/common/uploads where attachments will be
|
||||
stored](https://github.com/wekan/wekan/commit/c61a126c8bcb25a1eda0203b89c990ae31de7a70).
|
||||
Thanks to xet7.
|
||||
- [Make scrollParentContainer() more robust as it's used in a timeout callback. Example exception: Exception in setTimeout callback: TypeError: Cannot read property 'parentComponent' of null. Probably there is a better fix for this](https://github.com/wekan/wekan/commit/d5fbd50b760b1d3b84b5b4e8af3a8ed7608e2918).
|
||||
Thanks to marc1006.
|
||||
- [Fix error link not available. Fixes: Exception in template helper: TypeError: Cannot read property 'link' of
|
||||
undefined](https://github.com/wekan/wekan/commit/b7105d7b5712dcdbf9dadebfddaba7691810da5c).
|
||||
Thanks to marc1006.
|
||||
- [Fix minicard cover functionality. Otherwise, if `this.coverId` is undefined then `Attachments.findOne()` would return any
|
||||
attachment](https://github.com/wekan/wekan/commit/66d35a15280795b76a81c3e59cebbd2a29e4dff8).
|
||||
Thanks to marc1006.
|
||||
- [Some fixes suggested by deepcode.ai](https://github.com/wekan/wekan/pull/3112).
|
||||
Thanks to marc1006.
|
||||
- [Sorry marc1006, I had to revert deepcode.ai arrow function fixes because Python API docs generator does not work all when code has arrow functions](https://github.com/wekan/wekan/commit/f9018fc3a87080d8d97c371e29a8f3f0a20ca932).
|
||||
Thanks to xet7.
|
||||
- [Move In Progress ostrio-files changes to separate branch, and revert ostrio-files changes, so that:
|
||||
Export to CSV/TSV with custom fields works, Attachments are not exported to disk,
|
||||
It is possible to build arm64/s390x versions
|
||||
again](https://github.com/wekan/wekan/commit/d52affe65893f17bab59bb43aa9f5afbb54993d3).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.04 2020-05-24 Wekan release
|
||||
|
||||
Please use v4.05 or newer instead, that works better.
|
||||
|
||||
This release adds the following features:
|
||||
|
||||
- [Found Time Tracking GPLv3 software "Titra" with integration to Wekan](https://github.com/wekan/wekan/wiki/Time-Tracking).
|
||||
Thanks to willhseitz.
|
||||
- [Theme: Natural](https://github.com/wekan/wekan/pull/3098).
|
||||
You can select it from Board Settings / Change color / natural.
|
||||
Thanks to compumatter and helioguardabaxo.
|
||||
- [Theme: Modern](https://github.com/wekan/wekan/pull/3106).
|
||||
Thanks to jeroenstoker com and helioguardabaxo.
|
||||
- [Export board to HTML static page .zip archive](https://github.com/wekan/wekan/pull/3043).
|
||||
Thanks to Lewiscowles1986.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Change the swimlaneid of a card only if a new target swimlaneid is selected](https://github.com/wekan/wekan/pull/3108).
|
||||
Thanks to marc1006.
|
||||
- [Set '*' as default value for swimlane and list name in card move action](https://github.com/wekan/wekan/pull/3109).
|
||||
Thanks to hickorysb and marc1006.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.03 2020-05-16 Wekan release
|
||||
|
||||
This release adds the following features:
|
||||
|
||||
- [Theme: Clearblue](https://github.com/wekan/wekan/pull/3093).
|
||||
You can select it from Board Settings / Change color / clearblue.
|
||||
Thanks to CidKramer.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix Can't Scroll on All Boards on mobile phone. Added drag handles](https://github.com/wekan/wekan/issues/3096).
|
||||
Thanks to xet7.
|
||||
- [Try to fix Sandstorm Wekan Export menu](https://github.com/wekan/wekan/commit/1ac11d92ba8f38981c87db25e5b5e1fa2adb6968).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.02 2020-05-15 Wekan release
|
||||
|
||||
This release adds the following server platforms:
|
||||
|
||||
- [Android arm64/x64](https://github.com/wekan/wekan/wiki/Android).
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following features:
|
||||
|
||||
- [Install Wekan to mobile homescreen icon and use fullscreen
|
||||
PWA](https://github.com/wekan/wekan/commit/8d5adc04645e3e71423f16869f39b8d79969bccd).
|
||||
[Docs for iOS and Android at wiki PWA page](https://github.com/wekan/wekan/wiki/PWA).
|
||||
Thanks to xet7.
|
||||
- [Add options to rebuild-wekan.sh to run Meteor in development mode where after
|
||||
file change it rebuilds](https://github.com/wekan/wekan/commit/5f915ef966170ea7baca7ddeb11319bc08a26fef).
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Update dependencies](https://github.com/wekan/wekan/commit/75bdd33fda58ea0233f5b38c466bcb1a9b0406ab).
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following translations:
|
||||
|
||||
- [Add Spanish (Chile)](https://github.com/wekan/wekan/commit/96507e6777ed77a324eaec9799c5b46b0d25ad26).
|
||||
Thanks to isos, Transifex user.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix Deleting linked card makes board not load](https://github.com/wekan/wekan/issues/2785).
|
||||
Thanks to marc1006 and xet7.
|
||||
- [Fix getStartDayOfWeek once again](https://github.com/wekan/wekan/pull/3061).
|
||||
Thanks to marc1006.
|
||||
- [Fix shortcuts list and support card shortcuts when hovering
|
||||
a card](https://github.com/wekan/wekan/pull/3066).
|
||||
Thanks to marc1006.
|
||||
- [Add white-space:normal to copy-to-clipboard button in card
|
||||
details](https://github.com/wekan/wekan/pull/3075).
|
||||
Thanks to helioguardabaxo.
|
||||
- [Fix avatar-image class](https://github.com/wekan/wekan/pull/3083).
|
||||
Thanks to krupupakku.
|
||||
- [Fix Swimlanes ID missing in new boards](https://github.com/wekan/wekan/pull/3088).
|
||||
Thanks to krupupakku.
|
||||
- [Fix REST API so Create card does now allow an empty member
|
||||
list](https://github.com/wekan/wekan/pull/3084).
|
||||
Thanks to wackazong.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.01 2020-04-28 Wekan release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- [Upgrade to Node v12.16.3](https://github.com/wekan/wekan/commit/1d89e96dd101c11913f1acdd6d16b5650eaf18a7).
|
||||
Thanks to Node developers and xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix Docker builds](https://github.com/wekan/wekan/commit/280e66947e3afa878c41e876cf827ebcec81a2c6).
|
||||
Thanks to xet7.
|
||||
- [Fix Cards and Users API docs at https://wekan.github.io/api/ not generated because of
|
||||
syntax error and new Javascript syntax](https://github.com/wekan/wekan/commit/9ae20a3f51e63c29f536e2f5b3e66a2c7d88c691).
|
||||
Wekan uses wekan/releases/generate-docs*.sh Python code to generate OpenAPI docs,
|
||||
it did not show any errors while generating docs, only left out parts of API docs.
|
||||
This affected Wekan versions v3.94-v4.00.
|
||||
Thanks to pvcon13 and xet7.
|
||||
- [Fix list header height when cards count is shown](https://github.com/wekan/wekan/pull/3056).
|
||||
Thanks to marc1006.
|
||||
- [Smaller height for Add Board button](https://github.com/wekan/wekan/commit/6afc9259f084717a0cc3ce6d66979fd7c1471939).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v4.00 2020-04-27 Wekan release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Make sure that the board header buttons fit into one line even for devices with 360px width
|
||||
resolution](https://github.com/wekan/wekan/pull/3052).
|
||||
Thanks to marc1006.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.99 2020-04-27 Wekan release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Fix Boards are very hard to tap in mobile](https://github.com/wekan/wekan/pull/3051).
|
||||
Thanks to marc1006.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.98 2020-04-25 Wekan release
|
||||
|
||||
News:
|
||||
|
||||
- There is now many mobile and desktop webbrowser fixes. Please test does your
|
||||
favourite Javascript enabled webbrowser work, and add issues if something
|
||||
does not work, and there is no existing issue about that yet.
|
||||
- Desktop browser mode has setting for Show/Hide drag handles:
|
||||
top right click username / Change Settings / Show desktop drag handles.
|
||||
You can request desktop website also at mobile webbrowsers on Android.
|
||||
At iOS requesting desktop website did not seem to work yet.
|
||||
- At iOS Safari and Chrome, to see swimlane buttons you need to scroll to right.
|
||||
Fixes to this and other issues are welcome as pull request.
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Pre-fill the title of checklists (Trello-style)](https://github.com/wekan/wekan/pull/3030).
|
||||
Thanks to boeserwolf.
|
||||
- [Implement option to change the first day of the week in user settings](https://github.com/wekan/wekan/pull/3032).
|
||||
Thanks to marc1006.
|
||||
- [Add babel to build chain and linter. Enables fancy Javascript language
|
||||
features like optional chaining, for developer happiness](https://github.com/wekan/wekan/pull/3034).
|
||||
Thanks to boeserwolf.
|
||||
- [Use only one 'Apply' button for applying the user settings](https://github.com/wekan/wekan/pull/3039).
|
||||
Thanks to marc1006.
|
||||
- [Allow variable height for board list items. Allow words in title/description to be able to break
|
||||
and wrap onto the next line](https://github.com/wekan/wekan/pull/3046).
|
||||
Thanks to marc1006.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Upgrade to Meteor 1.10.2](https://github.com/wekan/wekan/commit/d1f98d0c472fb41e25fb29a9a6f6dae7db003f6f).
|
||||
Thanks to Meteor developers and xet7.
|
||||
- [Set Snap MongoDB compatibility to 4.2 according to Meteor ChangeLog](https://github.com/wekan/wekan/commit/7de18eccea3854db3be6197bf21afbfd3ddb65a6).
|
||||
Thanks to xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Multiple lint issue fixes](https://github.com/wekan/wekan/pull/3031).
|
||||
Thanks to marc1006.
|
||||
- [Fix lint errors in lint error fix](https://github.com/wekan/wekan/commit/9e95c06415e614e587d684ff9660cc53c5f8c8d3).
|
||||
Thanks to xet7.
|
||||
- [Fix getStartDayOfWeek function](https://github.com/wekan/wekan/pull/3038).
|
||||
Thanks to marc1006 and boeserwolf.
|
||||
- Improve mobile devices support [Part1](https://github.com/wekan/wekan/pull/3040) and [Part2](https://github.com/wekan/wekan/pull/3045).
|
||||
Thanks to marc1006.
|
||||
- [Fix Wekan not load at all in Firefox v.68 for Android](https://github.com/wekan/wekan/commit/1235363465b824d26129d4aa74a4445f362c1a73).
|
||||
Thanks to xet7.
|
||||
- [Fix comment typo in docker-compose.yml](https://github.com/wekan/wekan/pull/3044).
|
||||
Thanks to VictorioBerra.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.97 2020-04-19 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Sortable boards](https://github.com/wekan/wekan/pull/3027).
|
||||
Thanks to boeserwolf.
|
||||
- [Added dockerfiles for multi-arch builds and manifest](https://github.com/wekan/wekan/pull/3023).
|
||||
[In Progress](https://github.com/wekan/wekan/issues/2999).
|
||||
Thanks to brokencode64.
|
||||
- [Make linked card clickable](https://github.com/wekan/wekan/pull/3025).
|
||||
Thanks to boeserwolf.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix using checklists on mobile and iPad](https://github.com/wekan/wekan/pull/3019).
|
||||
Thanks to devinsm.
|
||||
- [Improve card layout on mobile devices](https://github.com/wekan/wekan/pull/3024).
|
||||
Thanks to marc1006.
|
||||
- [Make OCP OAuth work with Openshift 4.x](https://github.com/wekan/wekan/pull/3020).
|
||||
Thanks to ckavili.
|
||||
- [Remove old warning from Sandstorm import board data loss, because bug has been already
|
||||
fixed](https://github.com/wekan/wekan/commit/960fe5163b6a2f7c3dca03b5e31d69611b49f079).
|
||||
Thanks to aputsiaq and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.96 2020-04-15 Wekan release
|
||||
|
||||
This release adds the following Sandstorm updates:
|
||||
|
||||
- This is the first Sandstorm Wekan release that uses newest Meteor 1.10.1 and Node 12.x.
|
||||
Now all Wekan platforms use newest Meteor and Node 12.x LTS.
|
||||
Thanks to kentonv and xet7.
|
||||
- [Fix capnp workaround to work with newest Meteor and
|
||||
Node 12.x](https://github.com/wekan/wekan/commit/b2d546579c4957352c29b36c0c8a4a08b944dbb4).
|
||||
Thanks to kentonv.
|
||||
- [Update Sandstorm release script for newest Meteor and
|
||||
Node 12.x](https://github.com/wekan/wekan/commit/c5f782976b971fa3f2323e80a013bbf6a49c0596).
|
||||
Thanks to xet7.
|
||||
- [Remove Meteor 1.8.x files because Sandstorm Wekan now uses newest
|
||||
Meteor](https://github.com/wekan/wekan/commit/1a836969e10215bad47ac56a9b0d9de801b66fd2).
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following new features:
|
||||
|
||||
- [Hide password auth with environment variable PASSWORD_LOGIN_ENABLED=false](https://github.com/wekan/wekan/pull/3014).
|
||||
Snap example: `sudo snap set wekan password-login-enabled='false'` .
|
||||
Thanks to salleman33.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix Board admins can not clone or archive their boards at All Boards
|
||||
page](https://github.com/wekan/wekan/pull/3013).
|
||||
Thanks to salleman33.
|
||||
- [Fix `<p>` margin in card labels](https://github.com/wekan/wekan/pull/3015).
|
||||
Thanks to boeserwolf.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.95 2020-04-12 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Add gitpod config](https://github.com/wekan/wekan/pull/3009).
|
||||
This adds support for Gitpod.io, a free automated
|
||||
dev environment that makes contributing and generally working on GitHub
|
||||
projects much easier. It allows anyone to start a ready-to-code dev
|
||||
environment for any branch, issue and pull request with a single click.
|
||||
Thanks to juniormendonca.
|
||||
- [Public boards overview](https://github.com/wekan/wekan/pull/3008).
|
||||
Thanks to NicoP-S.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix styling issue in notifications drawer](https://github.com/wekan/wekan/pull/3012).
|
||||
Thanks to boeserwolf.
|
||||
- [Fix error in notifications cleanup cron](https://github.com/wekan/wekan/pull/3010).
|
||||
Thanks to jtbairdsr.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.94 2020-04-12 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Public vote](https://github.com/wekan/wekan/pull/3006).
|
||||
Thanks to NicoP-S.
|
||||
- [Add robots.txt disallow all](https://github.com/wekan/wekan/commit/3fae5355d40055757bf4a5f0c503581195609720).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.93 2020-04-10 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Trello vote import & hide export button if with_api is
|
||||
disabled](https://github.com/wekan/wekan/pull/3000).
|
||||
Thanks to NicoP-S.
|
||||
- [When adding a user to a board that has subtasks, also add user to the subtask
|
||||
board](https://github.com/wekan/wekan/pull/3004).
|
||||
Thanks to slvrpdr.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- Upgrade to Node v12.16.2 [Part1](https://github.com/wekan/wekan/commit/6db717b9b384fe1491063e507b80e67791a07e3a)
|
||||
and [Part2](https://github.com/wekan/wekan/commit/268d7fcb32186a902a84e7f6d80c50b1f3790bad).
|
||||
Thanks to Node developers and xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix bug that prevents editing or deleting
|
||||
comments](https://github.com/wekan/wekan/pull/3005).
|
||||
Thanks to jtbairdsr.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.92 2020-04-09 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
@@ -67,7 +884,7 @@ and fixes the following bugs:
|
||||
|
||||
- [Fix start-wekan.sh MongoDB port to 27017](https://github.com/wekan/wekan/commit/c60a092fc0ed9fe15c417bcb443b1e3e3aaedf7e).
|
||||
Thanks to Keelan and xet7.
|
||||
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.87 2020-04-01 Wekan release
|
||||
@@ -107,7 +924,7 @@ This release fixes the following bugs:
|
||||
@member mention not close card, and disabling clicking of
|
||||
@member mention](https://github.com/wekan/wekan/commit/b9099a8b7ea6f63c79bdcbb871cb993b2cb7e325).
|
||||
Thanks to xet7 !
|
||||
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.85 2020-03-23 Wekan release
|
||||
|
||||
+8
-4
@@ -4,10 +4,12 @@ LABEL maintainer="wekan"
|
||||
# Set the environment variables (defaults where required)
|
||||
# DOES NOT WORK: paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303
|
||||
# ENV BUILD_DEPS="paxctl"
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-essential git ca-certificates python3" \
|
||||
DEBUG=false \
|
||||
NODE_VERSION=v12.16.1 \
|
||||
METEOR_RELEASE=1.10-rc.2 \
|
||||
NODE_VERSION=v12.18.3 \
|
||||
METEOR_RELEASE=1.10.2 \
|
||||
USE_EDGE=false \
|
||||
METEOR_EDGE=1.5-beta.17 \
|
||||
NPM_VERSION=latest \
|
||||
@@ -39,6 +41,7 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
|
||||
TRUSTED_URL="" \
|
||||
WEBHOOKS_ATTRIBUTES="" \
|
||||
OAUTH2_ENABLED=false \
|
||||
OAUTH2_ADFS_ENABLED=false \
|
||||
OAUTH2_LOGIN_STYLE=redirect \
|
||||
OAUTH2_CLIENT_ID="" \
|
||||
OAUTH2_SECRET="" \
|
||||
@@ -112,8 +115,7 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
|
||||
CORS_ALLOW_HEADERS="" \
|
||||
CORS_EXPOSE_HEADERS="" \
|
||||
DEFAULT_AUTHENTICATION_METHOD="" \
|
||||
SCROLLINERTIA="0" \
|
||||
SCROLLAMOUNT="auto"
|
||||
PASSWORD_LOGIN_ENABLED=true
|
||||
|
||||
# Copy the app to the image
|
||||
COPY ${SRC_PATH} /home/wekan/app
|
||||
@@ -270,6 +272,8 @@ RUN \
|
||||
cd /home/wekan/app_build/bundle/programs/server/ && \
|
||||
gosu wekan:wekan npm install && \
|
||||
#gosu wekan:wekan npm install bcrypt && \
|
||||
# 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 && \
|
||||
\
|
||||
# Put back the original tar
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
FROM amd64/alpine:3.7 AS builder
|
||||
|
||||
# Set the environment variables for builder
|
||||
ENV QEMU_VERSION=v4.2.0-6 \
|
||||
QEMU_ARCHITECTURE=aarch64 \
|
||||
NODE_ARCHITECTURE=linux-arm64 \
|
||||
NODE_VERSION=v12.18.3 \
|
||||
WEKAN_VERSION=3.96 \
|
||||
WEKAN_ARCHITECTURE=arm64
|
||||
|
||||
# Install dependencies
|
||||
RUN apk update && apk add ca-certificates outils-sha1 && \
|
||||
\
|
||||
# Download qemu static for our architecture
|
||||
wget https://github.com/multiarch/qemu-user-static/releases/download/${QEMU_VERSION}/qemu-${QEMU_ARCHITECTURE}-static.tar.gz -O - | tar -xz && \
|
||||
\
|
||||
# Download wekan and shasum
|
||||
wget https://releases.wekan.team/raspi3/wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
|
||||
wget https://releases.wekan.team/raspi3/SHA256SUMS.txt && \
|
||||
# Verify wekan
|
||||
grep wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip SHA256SUMS.txt | sha256sum -c - && \
|
||||
\
|
||||
# Unzip wekan
|
||||
unzip wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
|
||||
\
|
||||
# Download node and shasums
|
||||
wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
|
||||
\
|
||||
# Verify nodejs authenticity
|
||||
grep node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz SHASUMS256.txt.asc | sha256sum -c - && \
|
||||
\
|
||||
# Extract node and remove tar.gz
|
||||
tar xvzf node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz
|
||||
|
||||
# Build wekan dockerfile
|
||||
FROM arm64v8/ubuntu:19.10
|
||||
LABEL maintainer="wekan"
|
||||
|
||||
# Set the environment variables (defaults where required)
|
||||
ENV QEMU_ARCHITECTURE=aarch64 \
|
||||
NODE_ARCHITECTURE=linux-arm64 \
|
||||
NODE_VERSION=v12.18.3 \
|
||||
NODE_ENV=production \
|
||||
NPM_VERSION=latest \
|
||||
WITH_API=true \
|
||||
PORT=8080 \
|
||||
ROOT_URL=http://localhost \
|
||||
MONGO_URL=mongodb://127.0.0.1:27017/wekan
|
||||
|
||||
# Copy qemu-static to image
|
||||
COPY --from=builder qemu-${QEMU_ARCHITECTURE}-static /usr/bin
|
||||
|
||||
# Copy the app to the image
|
||||
COPY --from=builder bundle /home/wekan/bundle
|
||||
|
||||
# Copy
|
||||
COPY --from=builder node-${NODE_VERSION}-${NODE_ARCHITECTURE} /opt/nodejs
|
||||
|
||||
RUN \
|
||||
set -o xtrace && \
|
||||
# Add non-root user wekan
|
||||
useradd --user-group --system --home-dir /home/wekan wekan && \
|
||||
\
|
||||
# Install Node
|
||||
ln -s /opt/nodejs/bin/node /usr/bin/node && \
|
||||
ln -s /opt/nodejs/bin/npm /usr/bin/npm && \
|
||||
mkdir -p /opt/nodejs/lib/node_modules/fibers/.node-gyp /root/.node-gyp/8.16.1 /home/wekan/.config && \
|
||||
chown wekan --recursive /home/wekan/.config && \
|
||||
\
|
||||
# Install Node dependencies
|
||||
npm install -g npm@${NPM_VERSION}
|
||||
|
||||
EXPOSE $PORT
|
||||
USER wekan
|
||||
|
||||
CMD ["node", "/home/wekan/bundle/main.js"]
|
||||
@@ -1,3 +1,5 @@
|
||||
[](https://gitpod.io/#https://github.com/wekan/wekan)
|
||||
|
||||
# Wekan - Open Source kanban
|
||||
|
||||
[](https://github.com/wekan/wekan/graphs/contributors)
|
||||
@@ -32,7 +34,7 @@ and PWA app that can be added as icon on Android and bookmark on iOS, used like
|
||||
|
||||
**NOTE**:
|
||||
- Please read the [FAQ](https://github.com/wekan/wekan/wiki/FAQ) first
|
||||
- Please don't feed the trolls and spammers that are mentioned in the FAQ :)
|
||||
- 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 :)
|
||||
|
||||
## About Wekan
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v3.92.0"
|
||||
appVersion: "v4.30.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// PWA
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function() {
|
||||
navigator.serviceWorker.register('/pwa-service-worker.js');
|
||||
});
|
||||
}
|
||||
@@ -34,38 +34,38 @@ template(name="activity")
|
||||
//- board activity ------------------------------------------------------
|
||||
if($eq mode 'board')
|
||||
if($eq activity.activityType 'createBoard')
|
||||
| {{_ 'activity-created' boardLabel}}.
|
||||
| {{{_ 'activity-created' boardLabelLink}}}.
|
||||
|
||||
if($eq activity.activityType 'importBoard')
|
||||
| {{{_ 'activity-imported-board' boardLabel sourceLink}}}.
|
||||
| {{{_ 'activity-imported-board' boardLabelLink sourceLink}}}.
|
||||
|
||||
if($eq activity.activityType 'addBoardMember')
|
||||
| {{{_ 'activity-added' memberLink boardLabel}}}.
|
||||
| {{{_ 'activity-added' memberLink boardLabelLink}}}.
|
||||
|
||||
if($eq activity.activityType 'removeBoardMember')
|
||||
| {{{_ 'activity-excluded' memberLink boardLabel}}}.
|
||||
| {{{_ 'activity-excluded' memberLink boardLabelLink}}}.
|
||||
|
||||
//- card activity -------------------------------------------------------
|
||||
if($eq activity.activityType 'createCard')
|
||||
if($eq mode 'card')
|
||||
| {{{_ 'activity-added' cardLabel activity.listName}}}.
|
||||
| {{{_ 'activity-added' cardLabelLink (sanitize activity.listName)}}}.
|
||||
else
|
||||
| {{{_ 'activity-added' cardLabel boardLabel}}}.
|
||||
| {{{_ 'activity-added' cardLabelLink boardLabelLink}}}.
|
||||
|
||||
if($eq activity.activityType 'importCard')
|
||||
| {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}.
|
||||
| {{{_ 'activity-imported' cardLink boardLabelLink sourceLink}}}.
|
||||
|
||||
if($eq activity.activityType 'moveCard')
|
||||
| {{{_ 'activity-moved' cardLabel activity.oldList.title activity.list.title}}}.
|
||||
| {{{_ 'activity-moved' cardLabelLink (sanitize activity.oldList.title) (sanitize activity.list.title)}}}.
|
||||
|
||||
if($eq activity.activityType 'moveCardBoard')
|
||||
| {{{_ 'activity-moved' cardLink activity.oldBoardName activity.boardName}}}.
|
||||
| {{{_ 'activity-moved' cardLink (sanitize activity.oldBoardName) (sanitize activity.boardName)}}}.
|
||||
|
||||
if($eq activity.activityType 'archivedCard')
|
||||
| {{{_ 'activity-archived' cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'restoredCard')
|
||||
| {{{_ 'activity-sent' cardLink boardLabel}}}.
|
||||
| {{{_ 'activity-sent' cardLink boardLabelLink}}}.
|
||||
|
||||
//- checklist activity --------------------------------------------------
|
||||
if($eq activity.activityType 'addChecklist')
|
||||
@@ -83,25 +83,25 @@ template(name="activity")
|
||||
| {{{_ 'activity-checklist-removed' cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'completeChecklist')
|
||||
| {{{_ 'activity-checklist-completed' activity.checklist.title cardLink}}}.
|
||||
| {{{_ 'activity-checklist-completed' (sanitize activity.checklist.title) cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'uncompleteChecklist')
|
||||
| {{{_ 'activity-checklist-uncompleted' activity.checklist.title cardLink}}}.
|
||||
| {{{_ 'activity-checklist-uncompleted' (sanitize activity.checklist.title) cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'checkedItem')
|
||||
| {{{_ 'activity-checked-item' checkItem activity.checklist.title cardLink}}}.
|
||||
| {{{_ 'activity-checked-item' (sanitize checkItem) (sanitize activity.checklist.title) cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'uncheckedItem')
|
||||
| {{{_ 'activity-unchecked-item' checkItem activity.checklist.title cardLink}}}.
|
||||
| {{{_ 'activity-unchecked-item' (sanitize checkItem) (sanitize activity.checklist.title) cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'addChecklistItem')
|
||||
| {{{_ 'activity-checklist-item-added' activity.checklist.title cardLink}}}.
|
||||
| {{{_ 'activity-checklist-item-added' (sanitize activity.checklist.title) cardLink}}}.
|
||||
.activity-checklist(href="{{ activity.card.absoluteUrl }}")
|
||||
+viewer
|
||||
= activity.checklistItem.title
|
||||
|
||||
if($eq activity.activityType 'removedChecklistItem')
|
||||
| {{{_ 'activity-checklist-item-removed' activity.checklist.title cardLink}}}.
|
||||
| {{{_ 'activity-checklist-item-removed' (sanitize activity.checklist.title) cardLink}}}.
|
||||
|
||||
//- comment activity ----------------------------------------------------
|
||||
if($eq mode 'card')
|
||||
@@ -143,31 +143,31 @@ template(name="activity")
|
||||
| {{_ 'activity-customfield-created' customField}}.
|
||||
|
||||
if($eq activity.activityType 'setCustomField')
|
||||
| {{{_ 'activity-set-customfield' lastCustomField lastCustomFieldValue cardLink}}}.
|
||||
| {{{_ 'activity-set-customfield' (sanitize lastCustomField) (sanitize lastCustomFieldValue) cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'unsetCustomField')
|
||||
| {{{_ 'activity-unset-customfield' lastCustomField cardLink}}}.
|
||||
| {{{_ 'activity-unset-customfield' (sanitize lastCustomField) cardLink}}}.
|
||||
|
||||
//- label activity ------------------------------------------------------
|
||||
if($eq activity.activityType 'addedLabel')
|
||||
| {{{_ 'activity-added-label' lastLabel cardLink}}}.
|
||||
| {{{_ 'activity-added-label' (sanitize lastLabel) cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'removedLabel')
|
||||
| {{{_ 'activity-removed-label' lastLabel cardLink}}}.
|
||||
| {{{_ 'activity-removed-label' (sanitize lastLabel) cardLink}}}.
|
||||
|
||||
//- list activity -------------------------------------------------------
|
||||
if($neq mode 'card')
|
||||
if($eq activity.activityType 'createList')
|
||||
| {{{_ 'activity-added' listLabel boardLabel}}}.
|
||||
| {{{_ 'activity-added' (sanitize listLabel) boardLabelLink}}}.
|
||||
|
||||
if($eq activity.activityType 'importList')
|
||||
| {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}.
|
||||
| {{{_ 'activity-imported' (sanitize listLabel) boardLabelLink sourceLink}}}.
|
||||
|
||||
if($eq activity.activityType 'removeList')
|
||||
| {{{_ 'activity-removed' activity.title boardLabel}}}.
|
||||
| {{{_ 'activity-removed' (sanitize activity.title) boardLabelLink}}}.
|
||||
|
||||
if($eq activity.activityType 'archivedList')
|
||||
| {{_ 'activity-archived' listLabel}}.
|
||||
| {{_ 'activity-archived' (sanitize listLabel)}}.
|
||||
|
||||
//- member activity ----------------------------------------------------
|
||||
if($eq activity.activityType 'joinMember')
|
||||
@@ -185,15 +185,15 @@ template(name="activity")
|
||||
//- swimlane activity --------------------------------------------------
|
||||
if($neq mode 'card')
|
||||
if($eq activity.activityType 'createSwimlane')
|
||||
| {{{_ 'activity-added' activity.swimlane.title boardLabel}}}.
|
||||
| {{_ 'activity-added' (sanitize activity.swimlane.title) boardLabelLink}}.
|
||||
|
||||
if($eq activity.activityType 'archivedSwimlane')
|
||||
| {{_ 'activity-archived' activity.swimlane.title}}.
|
||||
| {{_ 'activity-archived' (sanitize activity.swimlane.title)}}.
|
||||
|
||||
|
||||
//- I don't understand this part ----------------------------------------
|
||||
if(currentData.timeKey)
|
||||
| {{{_ activity.activityType }}}
|
||||
| {{_ activity.activityType }}
|
||||
= ' '
|
||||
i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
|
||||
if (currentData.timeOldValue)
|
||||
@@ -203,6 +203,6 @@ template(name="activity")
|
||||
i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
|
||||
= ' @'
|
||||
else if(currentData.timeValue)
|
||||
| {{{_ activity.activityType currentData.timeValue}}}
|
||||
| {{_ activity.activityType currentData.timeValue}}
|
||||
|
||||
span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
|
||||
@@ -1,3 +1,5 @@
|
||||
import sanitizeXss from 'xss';
|
||||
|
||||
const activitiesPerPage = 20;
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
@@ -5,8 +7,9 @@ BlazeComponent.extendComponent({
|
||||
// XXX Should we use ReactiveNumber?
|
||||
this.page = new ReactiveVar(1);
|
||||
this.loadNextPageLocked = false;
|
||||
const sidebar = this.parentComponent(); // XXX for some reason not working
|
||||
sidebar.callFirstWith(null, 'resetNextPeak');
|
||||
// TODO is sidebar always available? E.g. on small screens/mobile devices
|
||||
const sidebar = Sidebar;
|
||||
sidebar && sidebar.callFirstWith(null, 'resetNextPeak');
|
||||
this.autorun(() => {
|
||||
let mode = this.data().mode;
|
||||
const capitalizedMode = Utils.capitalize(mode);
|
||||
@@ -27,6 +30,8 @@ BlazeComponent.extendComponent({
|
||||
this.subscribe('activities', mode, searchId, limit, hideSystem, () => {
|
||||
this.loadNextPageLocked = false;
|
||||
|
||||
// TODO the guard can be removed as soon as the TODO above is resolved
|
||||
if (!sidebar) return;
|
||||
// If the sibear peak hasn't increased, that mean that there are no more
|
||||
// activities, and we can stop calling new subscriptions.
|
||||
// XXX This is hacky! We need to know excatly and reactively how many
|
||||
@@ -41,23 +46,22 @@ BlazeComponent.extendComponent({
|
||||
});
|
||||
});
|
||||
},
|
||||
}).register('activities');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
loadNextPage() {
|
||||
if (this.loadNextPageLocked === false) {
|
||||
this.page.set(this.page.get() + 1);
|
||||
this.loadNextPageLocked = true;
|
||||
}
|
||||
},
|
||||
}).register('activities');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
checkItem() {
|
||||
const checkItemId = this.currentData().activity.checklistItemId;
|
||||
const checkItem = ChecklistItems.findOne({ _id: checkItemId });
|
||||
return checkItem && checkItem.title;
|
||||
},
|
||||
|
||||
boardLabel() {
|
||||
boardLabelLink() {
|
||||
const data = this.currentData();
|
||||
if (data.mode !== 'board') {
|
||||
return createBoardLink(data.activity.board(), data.activity.listName);
|
||||
@@ -65,10 +69,10 @@ BlazeComponent.extendComponent({
|
||||
return TAPi18n.__('this-board');
|
||||
},
|
||||
|
||||
cardLabel() {
|
||||
cardLabelLink() {
|
||||
const data = this.currentData();
|
||||
if (data.mode !== 'card') {
|
||||
return createCardLink(this.currentData().activity.card());
|
||||
return createCardLink(data.activity.card());
|
||||
}
|
||||
return TAPi18n.__('this-card');
|
||||
},
|
||||
@@ -134,11 +138,11 @@ BlazeComponent.extendComponent({
|
||||
{
|
||||
href: source.url,
|
||||
},
|
||||
source.system,
|
||||
sanitizeXss(source.system),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return source.system;
|
||||
return sanitizeXss(source.system);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -162,10 +166,10 @@ BlazeComponent.extendComponent({
|
||||
href: attachment.url({ download: true }),
|
||||
target: '_blank',
|
||||
},
|
||||
attachment.name(),
|
||||
sanitizeXss(attachment.name()),
|
||||
),
|
||||
)) ||
|
||||
this.currentData().activity.attachmentName
|
||||
sanitizeXss(this.currentData().activity.attachmentName)
|
||||
);
|
||||
},
|
||||
|
||||
@@ -180,7 +184,7 @@ BlazeComponent.extendComponent({
|
||||
{
|
||||
// XXX We should use Popup.afterConfirmation here
|
||||
'click .js-delete-comment'() {
|
||||
const commentId = this.currentData().commentId;
|
||||
const commentId = this.currentData().activity.commentId;
|
||||
CardComments.remove(commentId);
|
||||
},
|
||||
'submit .js-edit-comment'(evt) {
|
||||
@@ -188,7 +192,7 @@ BlazeComponent.extendComponent({
|
||||
const commentText = this.currentComponent()
|
||||
.getValue()
|
||||
.trim();
|
||||
const commentId = Template.parentData().commentId;
|
||||
const commentId = Template.parentData().activity.commentId;
|
||||
if (commentText) {
|
||||
CardComments.update(commentId, {
|
||||
$set: {
|
||||
@@ -202,7 +206,14 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
}).register('activity');
|
||||
|
||||
Template.activity.helpers({
|
||||
sanitize(value) {
|
||||
return sanitizeXss(value);
|
||||
},
|
||||
});
|
||||
|
||||
function createCardLink(card) {
|
||||
if (!card) return '';
|
||||
return (
|
||||
card &&
|
||||
Blaze.toHTML(
|
||||
@@ -211,7 +222,7 @@ function createCardLink(card) {
|
||||
href: card.absoluteUrl(),
|
||||
class: 'action-card',
|
||||
},
|
||||
card.title,
|
||||
sanitizeXss(card.title),
|
||||
),
|
||||
)
|
||||
);
|
||||
@@ -228,7 +239,7 @@ function createBoardLink(board, list) {
|
||||
href: board.absoluteUrl(),
|
||||
class: 'action-board',
|
||||
},
|
||||
text,
|
||||
sanitizeXss(text),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ BlazeComponent.extendComponent({
|
||||
return Boards.find(
|
||||
{ archived: true },
|
||||
{
|
||||
sort: ['title'],
|
||||
sort: { sort: 1 /* boards default sorting */ },
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ template(name="board")
|
||||
template(name="boardBody")
|
||||
.board-wrapper(class=currentBoard.colorClass)
|
||||
+sidebar
|
||||
.board-canvas.js-swimlanes.js-perfect-scrollbar(
|
||||
.board-canvas.js-swimlanes(
|
||||
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}}")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Cookies } from 'meteor/ostrio:cookies';
|
||||
const cookies = new Cookies();
|
||||
const subManager = new SubsManager();
|
||||
const { calculateIndex, enableClickOnTouch } = Utils;
|
||||
const { calculateIndex } = Utils;
|
||||
const swimlaneWhileSortingHeight = 150;
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
@@ -191,9 +191,6 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
});
|
||||
|
||||
// ugly touch event hotfix
|
||||
enableClickOnTouch('.js-swimlane:not(.placeholder)');
|
||||
|
||||
this.autorun(() => {
|
||||
let showDesktopDragHandles = false;
|
||||
currentUser = Meteor.user();
|
||||
@@ -205,7 +202,7 @@ BlazeComponent.extendComponent({
|
||||
} else {
|
||||
showDesktopDragHandles = false;
|
||||
}
|
||||
if (!Utils.isMiniScreen() && showDesktopDragHandles) {
|
||||
if (Utils.isMiniScreen() || showDesktopDragHandles) {
|
||||
$swimlanesDom.sortable({
|
||||
handle: '.js-swimlane-header-handle',
|
||||
});
|
||||
@@ -215,9 +212,8 @@ BlazeComponent.extendComponent({
|
||||
});
|
||||
}
|
||||
|
||||
// Disable drag-dropping if the current user is not a board member or is miniscreen
|
||||
// Disable drag-dropping if the current user is not a board member
|
||||
$swimlanesDom.sortable('option', 'disabled', !userIsMember());
|
||||
$swimlanesDom.sortable('option', 'disabled', Utils.isMiniScreen());
|
||||
});
|
||||
|
||||
function userIsMember() {
|
||||
@@ -327,7 +323,7 @@ BlazeComponent.extendComponent({
|
||||
header: {
|
||||
left: 'title today prev,next',
|
||||
center:
|
||||
'agendaDay,listDay,timelineDay agendaWeek,listWeek,timelineWeek month,timelineMonth timelineYear',
|
||||
'agendaDay,listDay,timelineDay agendaWeek,listWeek,timelineWeek month,listMonth',
|
||||
right: '',
|
||||
},
|
||||
// height: 'parent', nope, doesn't work as the parent might be small
|
||||
|
||||
@@ -293,3 +293,449 @@ setBoardColor(color)
|
||||
|
||||
//.header-quick-access
|
||||
// backgroud-color: #568ba2
|
||||
|
||||
|
||||
/*
|
||||
Alternate "Clear" Styling
|
||||
*/
|
||||
setBoardClear(color1,color2)
|
||||
//color1: The quick access color
|
||||
//color2: The main bar color
|
||||
|
||||
&.sk-spinner div,
|
||||
.board-backgrounds-list &.background-box,
|
||||
.board-list & a
|
||||
background: linear-gradient(180deg, color1 0%, color2 100%)
|
||||
//background: linear-gradient(180deg, rgb(73, 155, 234) 0%, rgb(0, 174, 204) 100%)
|
||||
|
||||
.is-selected .minicard
|
||||
border-left: 3px solid color1
|
||||
|
||||
&.pop-over .pop-over-list li a:not(.disabled):hover,
|
||||
.sidebar .sidebar-content .sidebar-btn:hover,
|
||||
.sidebar-list li a:hover
|
||||
background-color: lighten(color1, 10%)
|
||||
|
||||
&#header ul li.current, &#header-quick-access ul li.current
|
||||
border-bottom: 4px solid lighten(color2, 10%)
|
||||
|
||||
&#header-quick-access
|
||||
background: darken(color1, 10%)
|
||||
//background: rgba(66,137,204,1)
|
||||
color: #FFF
|
||||
|
||||
&#header-quick-access #header-new-board-icon,
|
||||
&#header-quick-access #header-user-bar,
|
||||
&#header-quick-access ul li
|
||||
color: rgba(255,255,255,0.5)
|
||||
|
||||
// The background-color value here is not seen,
|
||||
// its covered by the background of #header-main-bar
|
||||
// it's just to aid transitions between boards
|
||||
&#header
|
||||
background-color: color2
|
||||
border-bottom: 1px solid darken(color2, 20%)
|
||||
border-top: 1px solid darken(color2, 40%)
|
||||
|
||||
// Since the theme uses a gradient for the header
|
||||
// and gradients break transitions, it has to be set here
|
||||
&#header #header-main-bar
|
||||
background: linear-gradient(180deg, color1 0%, color2 100%)
|
||||
|
||||
&#header #header-main-bar p
|
||||
margin-bottom: 6px
|
||||
|
||||
&#header #header-main-bar .board-header-btn.emphasis
|
||||
background: lighten(color2, 10%)
|
||||
|
||||
&:hover,
|
||||
.board-header-btn-close
|
||||
background: rgba(0,0,0,0.2)
|
||||
|
||||
&:hover .board-header-btn-close
|
||||
background: rgba(0,0,0,0.2)
|
||||
|
||||
.materialCheckBox.is-checked
|
||||
border-bottom: 2px solid color1
|
||||
border-right: 2px solid color1
|
||||
|
||||
.is-multiselection-active .multi-selection-checkbox
|
||||
&.is-checked + .minicard
|
||||
background: lighten(color2, 90%)
|
||||
|
||||
&:not(.is-checked) + .minicard:hover:not(.minicard-composer)
|
||||
background: lighten(color2, 97%)
|
||||
|
||||
.toggle-switch:checked ~ .toggle-label
|
||||
background-color: lighten(color1, 20%)
|
||||
|
||||
&:after
|
||||
background-color: darken(color1, 20%)
|
||||
|
||||
.board-canvas
|
||||
background: linear-gradient(135deg, color1 0%, color2 100%)
|
||||
|
||||
.swimlane
|
||||
background: none
|
||||
|
||||
.list:first-child
|
||||
margin-left: 15px
|
||||
|
||||
.list
|
||||
background: rgba(255,255,255,0.35)
|
||||
margin: 10px
|
||||
border: 0
|
||||
border-radius: 14px
|
||||
|
||||
.list.list-composer
|
||||
background: rgba(255,255,255,0.1)
|
||||
height: min-content
|
||||
flex: unset
|
||||
width: 270px
|
||||
padding-bottom: 16px
|
||||
|
||||
.list.list-composer .open-list-composer
|
||||
border-radius: 7px
|
||||
color: rgba(0,0,0,0.3)
|
||||
padding: 7px 10px
|
||||
display: block
|
||||
|
||||
.list.list-composer .open-list-composer:hover
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.2)
|
||||
background: rgba(255,255,255,0.7)
|
||||
color: rgba(0,0,0,0.6)
|
||||
|
||||
.list-header
|
||||
background-color: rgba(255,255,255,0.25)
|
||||
border-radius: 14px 14px 0 0
|
||||
|
||||
.list-header:not([class*="list-header-"])
|
||||
border-bottom: 6px solid rgba(255,255,255,0)
|
||||
|
||||
.list-header .list-header-name
|
||||
color: rgba(0,0,0,0.6)
|
||||
|
||||
.list-body
|
||||
padding: 11px
|
||||
|
||||
.minicard
|
||||
border-radius: 7px
|
||||
padding: 10px 10px 4px 10px
|
||||
box-shadow: 2px 2px 4px 0px rgba(0,0,0,0.15)
|
||||
color: #222
|
||||
|
||||
.card-details
|
||||
border-radius: 0 0 14px 14px
|
||||
box-shadow: 0 0 7px 0 rgba(0,0,0,0.5)
|
||||
margin-left: -10px
|
||||
|
||||
.list-body .open-minicard-composer
|
||||
border-radius: 7px
|
||||
color: rgba(0,0,0,.3)
|
||||
margin-bottom: 11px
|
||||
|
||||
.list-body .open-minicard-composer:hover
|
||||
background: rgba(255,255,255,0.7)
|
||||
color: rgba(0,0,0,0.6)
|
||||
|
||||
button[type=submit].primary, input[type=submit].primary
|
||||
box-shadow: none
|
||||
background-color: rgba(255,255,255,0.5)
|
||||
color: rgba(0,0,0,0.55)
|
||||
border-radius: 7px
|
||||
border: 0
|
||||
|
||||
button[type="submit"].primary:hover, input[type="submit"].primary:hover
|
||||
background-color: rgba(255,255,255,0.7)
|
||||
color: rgba(0,0,0,0.8)
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.2)
|
||||
|
||||
.quiet, .quiet a
|
||||
color: rgba(0,0,0,0.4)
|
||||
|
||||
.list-header .list-header-watch-icon
|
||||
color: rgba(0,0,0,0.5)
|
||||
position: absolute
|
||||
margin-top: -34px
|
||||
margin-let: -11px
|
||||
|
||||
a.fa, a i.fa
|
||||
color: rgba(0,0,0,0.3)
|
||||
|
||||
a:not(.disabled).is-active.fa, a:not(.disabled).is-active i.fa, a:not(.disabled):hover.fa, a:not(.disabled):hover i.fa
|
||||
color: rgba(0,0,0,0.6)
|
||||
|
||||
input[type="email"], input[type="password"], input[type="text"]
|
||||
border: 0
|
||||
border-radius: 7px
|
||||
|
||||
.sidebar-shadow
|
||||
box-shadow: none
|
||||
border-left: 9px solid color2
|
||||
|
||||
.is-open .sidebar-shadow
|
||||
box-shadow: -10px 0 8px rgba(0,0,0,0.3)
|
||||
|
||||
.list.ui-sortable-helper
|
||||
transform:rotate(0deg)
|
||||
|
||||
.minicard-wrapper.placeholder
|
||||
background: rgba(0,0,0,0.1)
|
||||
|
||||
.minicard-wrapper.ui-sortable-helper
|
||||
transform:rotate(0deg)
|
||||
opacity: 0.8
|
||||
|
||||
.list-body .open-minicard-composer
|
||||
color: rgba(0,0,0,.3)
|
||||
|
||||
.swinlane.ui-sortable-helper
|
||||
transform:rotate(0deg)
|
||||
|
||||
.swimlane .swimlane-header-wrap
|
||||
background: linear-gradient(0deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.25) 100%)
|
||||
|
||||
.swimlane-header-wrap .inlined-form
|
||||
width: 100%
|
||||
|
||||
.swimlane-header-wrap .list-composer
|
||||
text-align: center
|
||||
margin: 5px
|
||||
|
||||
.swimlane-header-wrap .list-name-input.full-line
|
||||
margin: 0
|
||||
display: inline-block
|
||||
width: 270px
|
||||
|
||||
.swimlane-header-wrap .edit-controls
|
||||
display: inline-block
|
||||
vertical-align: middle
|
||||
|
||||
.swimlane-header-wrap .primary.confirm
|
||||
margin-right: 0
|
||||
|
||||
.swimlane-header-wrap .fa.fa-times-thin
|
||||
margin-top: 2px
|
||||
|
||||
// This is a general fix so that the little grabby hand appears when dragging the list via the title
|
||||
.list.ui-sortable-helper,
|
||||
.list.ui-sortable-helper .list-header.ui-sortable-handle,
|
||||
.list.ui-sortable-helper .viewer
|
||||
cursor:-webkit-grabbing;
|
||||
cursor:grabbing
|
||||
|
||||
.board-color-clearblue
|
||||
setBoardClear(rgb(73, 155, 234),rgb(0, 174, 204))
|
||||
|
||||
/*
|
||||
Alternate "Natural" Styling
|
||||
*/
|
||||
.board-color-natural
|
||||
setBoardColor(#596557)
|
||||
|
||||
&#header-quick-access
|
||||
background-color: #2d392b
|
||||
|
||||
.ui-sortable
|
||||
background-color:#dedede
|
||||
|
||||
.list-header
|
||||
background-color: #c9cfc3
|
||||
border-bottom: 6px solid #c9cfc3
|
||||
|
||||
.swimlane .swimlane-header-wrap
|
||||
background-color: #c2c0ab
|
||||
|
||||
/*
|
||||
Alternate "Modern" Styling
|
||||
*/
|
||||
.board-color-modern
|
||||
setBoardColor(#2A80B8)
|
||||
|
||||
/* General */
|
||||
body
|
||||
background: #f5f5f5
|
||||
|
||||
&#header-quick-access
|
||||
padding: 10px
|
||||
font-size: 14px
|
||||
background: #333 !important
|
||||
|
||||
&#header-quick-access ul
|
||||
overflow: visible
|
||||
|
||||
&#header-quick-access ul li.current
|
||||
border: 0 !important
|
||||
font-weight: bold
|
||||
|
||||
&#header-quick-access ul li.separator
|
||||
display: none
|
||||
|
||||
&#header-quick-access ul li:nth-child(3)
|
||||
margin-right: 10px
|
||||
|
||||
&#header-quick-access ul li a
|
||||
padding: 5px 10px
|
||||
border-radius: 2px
|
||||
|
||||
&#header-quick-access ul li.current a
|
||||
border-radius: 2px
|
||||
background: rgba(255,255,255,.2)
|
||||
|
||||
&#header #header-main-bar h1
|
||||
font-family: Poppins
|
||||
font-weight: bold
|
||||
&#header-quick-access #header-user-bar
|
||||
position relative
|
||||
|
||||
&#header-quick-access #header-user-bar .header-user-bar-name
|
||||
margin: 5px 3px 0 0;
|
||||
|
||||
section#notifications-drawer
|
||||
top: 46px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,.1)
|
||||
max-width: 100%
|
||||
|
||||
section#notifications-drawer .header
|
||||
top: 46px;
|
||||
border-radius: 0 3px
|
||||
height: 21px
|
||||
background: #f7f7f7
|
||||
|
||||
/* Swimlane */
|
||||
.swimlane
|
||||
background: #f5f5f5
|
||||
|
||||
.swimlane .swimlane-header-wrap .swimlane-header
|
||||
font-family: Poppins
|
||||
|
||||
/* All board views */
|
||||
.board-list .board-list-item
|
||||
padding: 20px
|
||||
|
||||
.board-list-item-name
|
||||
font-family: Poppins
|
||||
|
||||
/* Board */
|
||||
.list
|
||||
background: transparent
|
||||
border-left: 0
|
||||
margin: 10px 0
|
||||
padding: 0px
|
||||
border-radius: 5px
|
||||
min-width: 300px
|
||||
|
||||
.list-body .open-minicard-composer:hover /*me*/
|
||||
background: none
|
||||
box-shadow: none
|
||||
|
||||
.list:first-child
|
||||
margin-left: 5px
|
||||
|
||||
.list.list-composer.js-list-composer
|
||||
transition: all .3s ease
|
||||
min-width: 80px
|
||||
|
||||
.open-list-composer.js-open-inlined-form:hover
|
||||
color: #222
|
||||
|
||||
.list-header
|
||||
background: none
|
||||
border-bottom-width: 0px
|
||||
|
||||
.list-header .list-header-name
|
||||
font-family: Poppins
|
||||
color: #000
|
||||
font-weight: 500
|
||||
|
||||
/* Card changes */
|
||||
.minicard
|
||||
background: #FFF
|
||||
padding: 15px 15px 10px
|
||||
box-shadow: 0 3px 8px rgba(0,0,0,.05)
|
||||
|
||||
.minicard-plum:hover:not(.minicard-composer), .is-selected .minicard-plum, .draggable-hover-card .minicard-plum
|
||||
background: none
|
||||
|
||||
.minicard-title
|
||||
line-height: 1.5em
|
||||
|
||||
.minicard .minicard-cover
|
||||
background-size: cover
|
||||
margin: -15px -15px 10px
|
||||
height: 100px
|
||||
|
||||
.card-label-orange
|
||||
color: #fff
|
||||
|
||||
.card-date
|
||||
font-size: 12px
|
||||
padding: 3px 5px
|
||||
|
||||
/* Pop over */
|
||||
.header-title
|
||||
font-family: Poppins
|
||||
font-size: 16px
|
||||
color: #333
|
||||
|
||||
.pop-over
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,.2)
|
||||
border: 0
|
||||
border-radius: 5px
|
||||
|
||||
.pop-over .header
|
||||
padding: 10px
|
||||
border-bottom: 0
|
||||
border-radius: 5px 5px 0 0
|
||||
background:#eee
|
||||
|
||||
.pop-over .header .header-title
|
||||
font-family: Poppins
|
||||
font-size:16px
|
||||
color:#333
|
||||
|
||||
.pop-over .header .close-btn
|
||||
font-size:20px
|
||||
top:6px
|
||||
right:8px
|
||||
|
||||
.pop-over .content-container .content
|
||||
padding: 5px 20px 20px
|
||||
width: 260px
|
||||
|
||||
.pop-over-list li > a
|
||||
border-radius: 5px
|
||||
|
||||
.pop-over-list li > a > i
|
||||
margin-right: 5px
|
||||
|
||||
.pop-over-list li>a .sub-name
|
||||
margin-bottom: 8px
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar .sidebar-shadow
|
||||
box-shadow: 0 0 60px rgba(0,0,0,.2)
|
||||
|
||||
.sidebar .sidebar-content
|
||||
padding: 30px
|
||||
|
||||
/* Notifications */
|
||||
.board-color-modern section#notifications-drawer
|
||||
border-radius:5px
|
||||
|
||||
.board-color-modern section#notifications-drawer .header
|
||||
padding: 18px 16px
|
||||
border-bottom: 0
|
||||
border-radius: 5px 5px 0 0
|
||||
background: #eee
|
||||
|
||||
.board-color-modern section#notifications-drawer .header h5
|
||||
font-family: Poppins
|
||||
font-weight: bold
|
||||
|
||||
.board-color-modern section#notifications-drawer .header .close
|
||||
font-size: 20px
|
||||
top: 14px
|
||||
|
||||
section#notifications-drawer .header .toggle-read
|
||||
top: 18px
|
||||
@@ -99,13 +99,13 @@ template(name="boardHeaderBar")
|
||||
a.board-header-btn.js-toggle-board-view(
|
||||
title="{{_ 'board-view'}}")
|
||||
i.fa.fa-caret-down
|
||||
if $eq boardView 'board-view-lists'
|
||||
i.fa.fa-trello
|
||||
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-lists'}}{{/if}}
|
||||
span {{#if boardView}}{{_ boardView}}{{else}}{{_ 'board-view-swimlanes'}}{{/if}}
|
||||
|
||||
if canModifyBoard
|
||||
a.board-header-btn.js-multiselection-activate(
|
||||
@@ -172,13 +172,6 @@ template(name="boardChangeWatchPopup")
|
||||
|
||||
template(name="boardChangeViewPopup")
|
||||
ul.pop-over-list
|
||||
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-swimlanes"
|
||||
a.js-open-swimlanes-view
|
||||
@@ -186,6 +179,13 @@ template(name="boardChangeViewPopup")
|
||||
| {{_ '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
|
||||
|
||||
@@ -33,22 +33,6 @@ Template.boardMenuPopup.events({
|
||||
'click .js-card-settings': Popup.open('boardCardSettings'),
|
||||
});
|
||||
|
||||
Template.boardMenuPopup.helpers({
|
||||
exportUrl() {
|
||||
const params = {
|
||||
boardId: Session.get('currentBoard'),
|
||||
};
|
||||
const queryParams = {
|
||||
authToken: Accounts._storedLoginToken(),
|
||||
};
|
||||
return FlowRouter.path('/api/boards/:boardId/export', params, queryParams);
|
||||
},
|
||||
exportFilename() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
return `wekan-export-board-${boardId}.json`;
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardChangeTitlePopup.events({
|
||||
submit(event, templateInstance) {
|
||||
const newTitle = templateInstance
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
template(name="boardList")
|
||||
.wrapper
|
||||
ul.board-list.clearfix
|
||||
ul.board-list.clearfix.js-boards
|
||||
li.js-add-board
|
||||
a.board-list-item.label {{_ 'add-board'}}
|
||||
each boards
|
||||
li(class="{{#if isStarred}}starred{{/if}}" class=colorClass)
|
||||
li(class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
|
||||
if isInvited
|
||||
.board-list-item
|
||||
span.details
|
||||
@@ -31,6 +31,10 @@ template(name="boardList")
|
||||
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 isMiniScreen
|
||||
i.fa.board-handle(
|
||||
class="fa-arrows"
|
||||
title="{{_ 'Drag board'}}")
|
||||
unless isMiniScreen
|
||||
if isSandstorm
|
||||
i.fa.js-clone-board(
|
||||
@@ -39,7 +43,7 @@ template(name="boardList")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
else if currentUser.isBoardAdmin
|
||||
else if isAdministrable
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
@@ -55,7 +59,7 @@ template(name="boardList")
|
||||
title="{{_ 'archive-board'}}")
|
||||
|
||||
template(name="boardListHeaderBar")
|
||||
h1 {{_ 'my-boards'}}
|
||||
h1 {{_ title }}
|
||||
.board-header-btns.right
|
||||
a.board-header-btn.js-open-archived-board
|
||||
i.fa.fa-archive
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const subManager = new SubsManager();
|
||||
const { calculateIndex, enableClickOnTouch } = Utils;
|
||||
|
||||
Template.boardListHeaderBar.events({
|
||||
'click .js-open-archived-board'() {
|
||||
@@ -7,6 +8,9 @@ Template.boardListHeaderBar.events({
|
||||
});
|
||||
|
||||
Template.boardListHeaderBar.helpers({
|
||||
title() {
|
||||
return FlowRouter.getRouteName() === 'home' ? 'my-boards' : 'public';
|
||||
},
|
||||
templatesBoardId() {
|
||||
return Meteor.user() && Meteor.user().getTemplatesBoardId();
|
||||
},
|
||||
@@ -20,20 +24,79 @@ BlazeComponent.extendComponent({
|
||||
Meteor.subscribe('setting');
|
||||
},
|
||||
|
||||
boards() {
|
||||
return Boards.find(
|
||||
{
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
type: 'board',
|
||||
onRendered() {
|
||||
const itemsSelector = '.js-board:not(.placeholder)';
|
||||
|
||||
const $boards = this.$('.js-boards');
|
||||
$boards.sortable({
|
||||
connectWith: '.js-boards',
|
||||
tolerance: 'pointer',
|
||||
appendTo: '.board-list',
|
||||
helper: 'clone',
|
||||
distance: 7,
|
||||
items: itemsSelector,
|
||||
placeholder: 'board-wrapper placeholder',
|
||||
start(evt, ui) {
|
||||
ui.helper.css('z-index', 1000);
|
||||
ui.placeholder.height(ui.helper.height());
|
||||
EscapeActions.executeUpTo('popup-close');
|
||||
},
|
||||
{ sort: ['title'] },
|
||||
);
|
||||
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 = calculateIndex(prevBoardDom, nextBoardBom, 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);
|
||||
},
|
||||
});
|
||||
|
||||
// ugly touch event hotfix
|
||||
enableClickOnTouch(itemsSelector);
|
||||
|
||||
// Disable drag-dropping if the current user is not a board member or is comment only
|
||||
this.autorun(() => {
|
||||
if (Utils.isMiniScreen()) {
|
||||
$boards.sortable({
|
||||
handle: '.board-handle',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
boards() {
|
||||
const query = {
|
||||
archived: false,
|
||||
type: 'board',
|
||||
};
|
||||
if (FlowRouter.getRouteName() === 'home')
|
||||
query['members.userId'] = Meteor.userId();
|
||||
else query.permission = 'public';
|
||||
|
||||
return Boards.find(query, {
|
||||
sort: { sort: 1 /* boards default sorting */ },
|
||||
});
|
||||
},
|
||||
isStarred() {
|
||||
const user = Meteor.user();
|
||||
return user && user.hasStarred(this.currentData()._id);
|
||||
},
|
||||
isAdministrable() {
|
||||
const user = Meteor.user();
|
||||
return user && user.isBoardAdmin(this.currentData()._id);
|
||||
},
|
||||
|
||||
hasOvertimeCards() {
|
||||
subManager.subscribe('board', this.currentData()._id, false);
|
||||
|
||||
@@ -11,6 +11,19 @@ $spaceBetweenTiles = 16px
|
||||
box-sizing: border-box
|
||||
position: relative
|
||||
|
||||
&.placeholder:after
|
||||
content: '';
|
||||
display: block;
|
||||
background: darken(white, 20%)
|
||||
border-radius: 3px;
|
||||
height: 106px;
|
||||
margin: 8px;
|
||||
|
||||
&.ui-sortable-helper
|
||||
cursor: grabbing
|
||||
transform: rotate(4deg)
|
||||
display: block !important
|
||||
|
||||
&.starred
|
||||
.fa-star,
|
||||
.fa-star-o
|
||||
@@ -20,7 +33,7 @@ $spaceBetweenTiles = 16px
|
||||
overflow: hidden;
|
||||
background-color: #999
|
||||
color: #f6f6f6
|
||||
height: 90px
|
||||
height: auto
|
||||
font-size: 16px
|
||||
line-height: 22px
|
||||
border-radius: 3px
|
||||
@@ -31,6 +44,7 @@ $spaceBetweenTiles = 16px
|
||||
margin: ($spaceBetweenTiles/2)
|
||||
position: relative
|
||||
text-decoration: none
|
||||
word-wrap: break-word
|
||||
|
||||
&.tile
|
||||
background-size: auto
|
||||
@@ -55,7 +69,7 @@ $spaceBetweenTiles = 16px
|
||||
|
||||
.label
|
||||
font-weight: normal
|
||||
line-height:90px
|
||||
line-height: 56px
|
||||
|
||||
:hover
|
||||
background-color:#939393
|
||||
@@ -183,7 +197,7 @@ $spaceBetweenTiles = 16px
|
||||
overflow: scroll
|
||||
|
||||
li
|
||||
width: 50%
|
||||
width: 50%
|
||||
|
||||
.board-list-item
|
||||
overflow: hidden
|
||||
@@ -194,6 +208,22 @@ $spaceBetweenTiles = 16px
|
||||
top: -100px
|
||||
left: -100px
|
||||
|
||||
.board-handle
|
||||
position: absolute
|
||||
padding: 7px
|
||||
top: 50%
|
||||
transform: translateY(-50%)
|
||||
right: 10px
|
||||
font-size: 24px
|
||||
|
||||
@media screen and (max-width: 360px)
|
||||
li
|
||||
width: 100%
|
||||
|
||||
.board-handle
|
||||
position: absolute
|
||||
padding: 7px
|
||||
top: 50%
|
||||
transform: translateY(-50%)
|
||||
right: 10px
|
||||
font-size: 24px
|
||||
@@ -4,8 +4,7 @@ template(name="cardCustomFieldsPopup")
|
||||
li.item(class="")
|
||||
a.name.js-select-field(href="#")
|
||||
span.full-name
|
||||
+viewer
|
||||
= name
|
||||
= name
|
||||
if hasCustomField
|
||||
i.fa.fa-check
|
||||
hr
|
||||
@@ -53,6 +52,23 @@ template(name="cardCustomField-number")
|
||||
if value
|
||||
= value
|
||||
|
||||
template(name="cardCustomField-currency")
|
||||
if canModifyCard
|
||||
+inlinedForm(classNames="js-card-customfield-currency")
|
||||
input(type="text" value=data.value)
|
||||
.edit-controls.clearfix
|
||||
button.primary(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
if value
|
||||
= formattedValue
|
||||
else
|
||||
| {{_ 'edit'}}
|
||||
else
|
||||
if value
|
||||
= formattedValue
|
||||
|
||||
template(name="cardCustomField-date")
|
||||
if canModifyCard
|
||||
a.js-edit-date(title="{{showTitle}}" class="{{classes}}")
|
||||
|
||||
@@ -80,6 +80,36 @@ CardCustomField.register('cardCustomField');
|
||||
}
|
||||
}.register('cardCustomField-number'));
|
||||
|
||||
// cardCustomField-currency
|
||||
(class extends CardCustomField {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
|
||||
this.currencyCode = this.data().definition.settings.currencyCode;
|
||||
}
|
||||
|
||||
formattedValue() {
|
||||
const locale = TAPi18n.getLanguage();
|
||||
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: this.currencyCode,
|
||||
}).format(this.data().value);
|
||||
}
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-card-customfield-currency'(event) {
|
||||
event.preventDefault();
|
||||
const value = Number(this.find('input').value, 10);
|
||||
this.card.setCustomField(this.customFieldId, value);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}.register('cardCustomField-currency'));
|
||||
|
||||
// cardCustomField-date
|
||||
(class extends CardCustomField {
|
||||
onCreated() {
|
||||
|
||||
@@ -386,3 +386,30 @@ CardEndDate.register('cardEndDate');
|
||||
return this.date.get().format('l');
|
||||
}
|
||||
}.register('minicardEndDate'));
|
||||
|
||||
class VoteEndDate extends CardDate {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getVoteEnd()));
|
||||
});
|
||||
}
|
||||
classes() {
|
||||
const classes = 'end-date' + ' ';
|
||||
return classes;
|
||||
}
|
||||
showDate() {
|
||||
return this.date.get().format('l LT');
|
||||
}
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
|
||||
}
|
||||
|
||||
events() {
|
||||
return super.events().concat({
|
||||
'click .js-edit-date': Popup.open('editVoteEndDate'),
|
||||
});
|
||||
}
|
||||
}
|
||||
VoteEndDate.register('voteEndDate');
|
||||
@@ -1,5 +1,5 @@
|
||||
template(name="cardDetails")
|
||||
section.card-details.js-card-details.js-perfect-scrollbar: .card-details-canvas
|
||||
section.card-details.js-card-details: .card-details-canvas
|
||||
.card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
|
||||
+inlinedForm(classNames="js-card-details-title")
|
||||
+editCardTitleForm
|
||||
@@ -32,7 +32,7 @@ template(name="cardDetails")
|
||||
// else
|
||||
{{_ 'top-level-card'}}
|
||||
if isLinkedCard
|
||||
h3.linked-card-location
|
||||
a.linked-card-location.js-go-to-linked-card
|
||||
+viewer
|
||||
| {{getBoardTitle}} > {{getTitle}}
|
||||
|
||||
@@ -42,12 +42,28 @@ template(name="cardDetails")
|
||||
else
|
||||
p.warning {{_ 'card-archived'}}
|
||||
|
||||
.card-details-items
|
||||
.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
|
||||
span.card-label(class="card-label-{{color}}" title=name)
|
||||
+viewer
|
||||
= name
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
if currentBoard.allowsReceivedDate
|
||||
hr
|
||||
.card-details-item.card-details-item-received
|
||||
h3
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sign-out
|
||||
card-details-item-title {{_ 'card-received'}}
|
||||
| {{_ 'card-received'}}
|
||||
if getReceived
|
||||
+cardReceivedDate
|
||||
else
|
||||
@@ -58,9 +74,9 @@ template(name="cardDetails")
|
||||
|
||||
if currentBoard.allowsStartDate
|
||||
.card-details-item.card-details-item-start
|
||||
h3
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-hourglass-start
|
||||
card-details-item-title {{_ 'card-start'}}
|
||||
| {{_ 'card-start'}}
|
||||
if getStart
|
||||
+cardStartDate
|
||||
else
|
||||
@@ -71,9 +87,9 @@ template(name="cardDetails")
|
||||
|
||||
if currentBoard.allowsDueDate
|
||||
.card-details-item.card-details-item-due
|
||||
h3
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sign-in
|
||||
card-details-item-title {{_ 'card-due'}}
|
||||
| {{_ 'card-due'}}
|
||||
if getDue
|
||||
+cardDueDate
|
||||
else
|
||||
@@ -84,9 +100,9 @@ template(name="cardDetails")
|
||||
|
||||
if currentBoard.allowsEndDate
|
||||
.card-details-item.card-details-item-end
|
||||
h3
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-hourglass-end
|
||||
card-details-item-title {{_ 'card-end'}}
|
||||
| {{_ 'card-end'}}
|
||||
if getEnd
|
||||
+cardEndDate
|
||||
else
|
||||
@@ -97,10 +113,11 @@ template(name="cardDetails")
|
||||
|
||||
//.card-details-items
|
||||
if currentBoard.allowsMembers
|
||||
hr
|
||||
.card-details-item.card-details-item-members
|
||||
h3
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-users
|
||||
card-details-item-title {{_ 'members'}}
|
||||
| {{_ 'members'}}
|
||||
each getMembers
|
||||
+userAvatar(userId=this cardId=../_id)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
@@ -112,9 +129,9 @@ template(name="cardDetails")
|
||||
//if assigneeSelected
|
||||
if currentBoard.allowsAssignee
|
||||
.card-details-item.card-details-item-assignees
|
||||
h3
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user
|
||||
card-details-item-title {{_ 'assignee'}}
|
||||
| {{_ 'assignee'}}
|
||||
each getAssignees
|
||||
+userAvatarAssignee(userId=this cardId=../_id)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
@@ -126,44 +143,23 @@ template(name="cardDetails")
|
||||
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
if currentBoard.allowsLabels
|
||||
.card-details-item.card-details-item-labels
|
||||
h3
|
||||
i.fa.fa-tags
|
||||
card-details-item-title {{_ 'labels'}}
|
||||
a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
|
||||
each labels
|
||||
span.card-label(class="card-label-{{color}}" title=name)
|
||||
+viewer
|
||||
= name
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
//.card-details-items
|
||||
each customFieldsWD
|
||||
.card-details-item.card-details-item-customfield
|
||||
h3.card-details-item-title
|
||||
+viewer
|
||||
= definition.name
|
||||
+cardCustomField
|
||||
|
||||
//.card-details-items
|
||||
if getSpentTime
|
||||
.card-details-item.card-details-item-spent
|
||||
if getIsOvertime
|
||||
h3.card-details-item-title {{_ 'overtime-hours'}}
|
||||
h3.card-details-item-title
|
||||
| {{_ 'overtime-hours'}}
|
||||
else
|
||||
h3.card-details-item-title {{_ 'spent-time-hours'}}
|
||||
h3.card-details-item-title
|
||||
| {{_ 'spent-time-hours'}}
|
||||
+cardSpentTime
|
||||
|
||||
//.card-details-items
|
||||
if currentBoard.allowsRequestedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-shopping-cart
|
||||
card-details-item-title {{_ 'requested-by'}}
|
||||
| {{_ 'requested-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
+inlinedForm(classNames="js-card-details-requester")
|
||||
@@ -181,9 +177,9 @@ template(name="cardDetails")
|
||||
|
||||
if currentBoard.allowsAssignedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user-plus
|
||||
card-details-item-title {{_ 'assigned-by'}}
|
||||
| {{_ 'assigned-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
+inlinedForm(classNames="js-card-details-assigner")
|
||||
@@ -199,32 +195,54 @@ template(name="cardDetails")
|
||||
+viewer
|
||||
= getAssignedBy
|
||||
|
||||
//.card-details-items
|
||||
if customFieldsWD
|
||||
hr
|
||||
each customFieldsWD
|
||||
.card-details-item.card-details-item-customfield
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-list-alt
|
||||
= definition.name
|
||||
+cardCustomField
|
||||
|
||||
if getVoteQuestion
|
||||
hr
|
||||
.vote-title
|
||||
h3
|
||||
i.fa.fa-thumbs-up
|
||||
card-details-item-title {{_ 'vote-question'}}
|
||||
div.flex
|
||||
h3
|
||||
i.fa.fa-thumbs-up
|
||||
| {{_ 'vote-question'}}
|
||||
if getVoteEnd
|
||||
+voteEndDate
|
||||
.vote-result
|
||||
.card-label.card-label-green
|
||||
+viewer
|
||||
= voteCountPositive
|
||||
.card-label.card-label-red
|
||||
+viewer
|
||||
= voteCountNegative
|
||||
if votePublic
|
||||
a.card-label.card-label-green.js-show-positive-votes {{ voteCountPositive }}
|
||||
a.card-label.card-label-red.js-show-negative-votes {{ voteCountNegative }}
|
||||
else
|
||||
.card-label.card-label-green {{ voteCountPositive }}
|
||||
.card-label.card-label-red {{ voteCountNegative }}
|
||||
unless ($and currentBoard.isPublic voteAllowNonBoardMembers )
|
||||
.card-label.card-label-gray {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
+viewer
|
||||
= getVoteQuestion
|
||||
button.card-details-green.js-vote.js-vote-positive(class="{{#if voteState}}voted{{/if}}") {{_ 'vote-for-it'}}
|
||||
button.card-details-red.js-vote.js-vote-negative(class="{{#if $eq voteState false}}voted{{/if}}") {{_ 'vote-against'}}
|
||||
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'}}
|
||||
|
||||
//- XXX We should use "editable" to avoid repetiting ourselves
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
if currentBoard.allowsDescriptionTitle
|
||||
hr
|
||||
h3
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-align-left
|
||||
card-details-item-title {{_ 'description'}}
|
||||
| {{_ 'description'}}
|
||||
if currentBoard.allowsDescriptionText
|
||||
+inlinedCardDescription(classNames="card-description js-card-description")
|
||||
+editor(autofocus=true)
|
||||
@@ -264,7 +282,7 @@ template(name="cardDetails")
|
||||
+subtasks(cardId = _id)
|
||||
if currentBoard.allowsAttachments
|
||||
hr
|
||||
h3
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-paperclip
|
||||
| {{_ 'attachments'}}
|
||||
.card-checklist-attachmentGalery.card-attachmentGalery
|
||||
@@ -273,7 +291,7 @@ template(name="cardDetails")
|
||||
hr
|
||||
unless currentUser.isNoComments
|
||||
.activity-title
|
||||
h3
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-history
|
||||
| {{ _ 'activity'}}
|
||||
if currentUser.isBoardMember
|
||||
@@ -333,16 +351,10 @@ template(name="cardDetailsActionsPopup")
|
||||
//li: a.js-members {{_ 'card-edit-members'}}
|
||||
//li: a.js-labels {{_ 'card-edit-labels'}}
|
||||
//li: a.js-attachments {{_ 'card-edit-attachments'}}
|
||||
if getVoteQuestion
|
||||
li
|
||||
a.js-cancel-voting
|
||||
i.fa.fa-thumbs-up
|
||||
| {{_ 'card-cancel-voting'}}
|
||||
else
|
||||
li
|
||||
a.js-start-voting
|
||||
i.fa.fa-thumbs-up
|
||||
| {{_ 'card-start-voting'}}
|
||||
li
|
||||
a.js-start-voting
|
||||
i.fa.fa-thumbs-up
|
||||
| {{_ 'card-edit-voting'}}
|
||||
li
|
||||
a.js-custom-fields
|
||||
i.fa.fa-list-alt
|
||||
@@ -465,14 +477,14 @@ template(name="cardAssigneesPopup")
|
||||
i.fa.fa-check
|
||||
if currentUser.isWorker
|
||||
ul.pop-over-list.js-card-assignee-list
|
||||
li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
|
||||
a.name.js-select-assignee(href="#")
|
||||
+userAvatar(userId=currentUser._id)
|
||||
span.full-name
|
||||
= currentUser.profile.fullname
|
||||
| (<span class="username">{{ currentUser.username }}</span>)
|
||||
if currentUser.isCardAssignee
|
||||
i.fa.fa-check
|
||||
li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
|
||||
a.name.js-select-assignee(href="#")
|
||||
+userAvatar(userId=currentUser._id)
|
||||
span.full-name
|
||||
= currentUser.profile.fullname
|
||||
| (<span class="username">{{ currentUser.username }}</span>)
|
||||
if currentUser.isCardAssignee
|
||||
i.fa.fa-check
|
||||
|
||||
template(name="userAvatarAssignee")
|
||||
a.assignee.js-assignee(title="{{userData.profile.fullname}} ({{userData.username}})")
|
||||
@@ -519,7 +531,7 @@ template(name="cardMorePopup")
|
||||
= ' '
|
||||
i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
input.inline-input(type="text" id="cardURL" readonly value="{{ absoluteUrl }}" autofocus="autofocus")
|
||||
button.js-copy-card-link-to-clipboard(class="btn") {{_ 'copy-card-link-to-clipboard'}}
|
||||
button.js-copy-card-link-to-clipboard(class="btn" id="clipboard") {{_ 'copy-card-link-to-clipboard'}}
|
||||
span.clearfix
|
||||
br
|
||||
h2 {{_ 'change-card-parent'}}
|
||||
@@ -564,14 +576,56 @@ template(name="setCardColorPopup")
|
||||
template(name="cardDeletePopup")
|
||||
p {{_ "card-delete-pop"}}
|
||||
unless archived
|
||||
p {{_ "card-delete-suggest-archive"}}
|
||||
p {{_ "card-delete-suggest-archive"}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="deleteVotePopup")
|
||||
p {{_ "vote-delete-pop"}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="cardStartVotingPopup")
|
||||
form.edit-vote-question
|
||||
.fields
|
||||
label(for="vote") {{_ 'vote-question'}}
|
||||
input.js-vote-field#vote(type="text" name="vote" value="{{card.getVoteQuestion}}" autofocus)
|
||||
input.js-vote-field#vote(type="text" name="vote" value="{{getVoteQuestion}}" autofocus disabled="{{#if getVoteQuestion}}disabled{{/if}}")
|
||||
.check-div
|
||||
a.flex(class="{{#if getVoteQuestion}}is-disabled{{else}}js-toggle-vote-allow-non-members{{/if}}")
|
||||
.materialCheckBox#vote-allow-non-members(name="vote-allow-non-members" class="{{#if voteAllowNonBoardMembers}}is-checked{{/if}}")
|
||||
span {{_ 'allowNonBoardMembers'}}
|
||||
.check-div
|
||||
a.flex(class="{{#if getVoteQuestion}}is-disabled{{else}}js-toggle-vote-public{{/if}}")
|
||||
.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
|
||||
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
//- button.js-remove-color.negate.wide.right {{_ 'delete'}}
|
||||
button.primary.js-submit {{_ 'save'}}
|
||||
if getVoteQuestion
|
||||
button.js-remove-vote.negate.wide.right {{_ 'delete'}}
|
||||
|
||||
template(name="positiveVoteMembersPopup")
|
||||
ul.pop-over-list.js-card-member-list
|
||||
each m in voteMemberPositive
|
||||
li.item
|
||||
a.name
|
||||
+userAvatar(userId=m._id)
|
||||
span.full-name
|
||||
= m.profile.fullname
|
||||
| (<span class="username">{{ m.username }}</span>)
|
||||
|
||||
template(name="negativeVoteMembersPopup")
|
||||
ul.pop-over-list.js-card-member-list
|
||||
each m in voteMemberNegative
|
||||
li.item
|
||||
a.name
|
||||
+userAvatar(userId=m._id)
|
||||
span.full-name
|
||||
= m.profile.fullname
|
||||
| (<span class="username">{{ m.username }}</span>)
|
||||
@@ -1,5 +1,5 @@
|
||||
const subManager = new SubsManager();
|
||||
const { calculateIndexData, enableClickOnTouch } = Utils;
|
||||
const { calculateIndexData } = Utils;
|
||||
|
||||
let cardColors;
|
||||
Meteor.startup(() => {
|
||||
@@ -8,7 +8,7 @@ Meteor.startup(() => {
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
mixins() {
|
||||
return [Mixins.InfiniteScrolling, Mixins.PerfectScrollbar];
|
||||
return [Mixins.InfiniteScrolling];
|
||||
},
|
||||
|
||||
calculateNextPeak() {
|
||||
@@ -38,32 +38,6 @@ BlazeComponent.extendComponent({
|
||||
Meteor.subscribe('unsaved-edits');
|
||||
},
|
||||
|
||||
voteState() {
|
||||
const card = this.currentData();
|
||||
const userId = Meteor.userId();
|
||||
let state;
|
||||
if (card.vote) {
|
||||
if (card.vote.positive) {
|
||||
state = _.contains(card.vote.positive, userId);
|
||||
if (state === true) return true;
|
||||
}
|
||||
if (card.vote.negative) {
|
||||
state = _.contains(card.vote.negative, userId);
|
||||
if (state === true) return false;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
voteCountPositive() {
|
||||
const card = this.currentData();
|
||||
if (card.vote && card.vote.positive) return card.vote.positive.length;
|
||||
return null;
|
||||
},
|
||||
voteCountNegative() {
|
||||
const card = this.currentData();
|
||||
if (card.vote && card.vote.negative) return card.vote.negative.length;
|
||||
return null;
|
||||
},
|
||||
isWatching() {
|
||||
const card = this.currentData();
|
||||
return card.findWatcher(Meteor.userId());
|
||||
@@ -84,11 +58,18 @@ BlazeComponent.extendComponent({
|
||||
|
||||
scrollParentContainer() {
|
||||
const cardPanelWidth = 510;
|
||||
const bodyBoardComponent = this.parentComponent().parentComponent();
|
||||
const parentComponent = this.parentComponent();
|
||||
// TODO sometimes parentComponent is not available, maybe because it's not
|
||||
// yet created?!
|
||||
if (!parentComponent) return;
|
||||
const bodyBoardComponent = parentComponent.parentComponent();
|
||||
//On Mobile View Parent is Board, Not Board Body. I cant see how this funciton should work then.
|
||||
if (bodyBoardComponent === null) return;
|
||||
const $cardView = this.$(this.firstNode());
|
||||
const $cardContainer = bodyBoardComponent.$('.js-swimlanes');
|
||||
// TODO sometimes cardContainer is not available, maybe because it's not yet
|
||||
// created?!
|
||||
if (!$cardContainer) return;
|
||||
const cardContainerScroll = $cardContainer.scrollLeft();
|
||||
const cardContainerWidth = $cardContainer.width();
|
||||
|
||||
@@ -143,6 +124,15 @@ BlazeComponent.extendComponent({
|
||||
return result;
|
||||
},
|
||||
|
||||
showVotingButtons() {
|
||||
const card = this.currentData();
|
||||
return (
|
||||
(currentUser.isBoardMember() ||
|
||||
(currentUser && card.voteAllowNonBoardMembers())) &&
|
||||
!card.expiredVote()
|
||||
);
|
||||
},
|
||||
|
||||
onRendered() {
|
||||
if (Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED) {
|
||||
// Send Webhook but not create Activities records ---
|
||||
@@ -181,13 +171,6 @@ BlazeComponent.extendComponent({
|
||||
|
||||
if (!Utils.isMiniScreen()) {
|
||||
Meteor.setTimeout(() => {
|
||||
$('.card-details').mCustomScrollbar({
|
||||
theme: 'minimal-dark',
|
||||
setWidth: false,
|
||||
setLeft: 0,
|
||||
scrollbarPosition: 'outside',
|
||||
mouseWheel: true,
|
||||
});
|
||||
this.scrollParentContainer();
|
||||
}, 500);
|
||||
}
|
||||
@@ -226,9 +209,6 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
});
|
||||
|
||||
// ugly touch event hotfix
|
||||
enableClickOnTouch('.card-checklist-items .js-checklist');
|
||||
|
||||
const $subtasksDom = this.$('.card-subtasks-items');
|
||||
|
||||
$subtasksDom.sortable({
|
||||
@@ -264,26 +244,21 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
});
|
||||
|
||||
// ugly touch event hotfix
|
||||
enableClickOnTouch('.card-subtasks-items .js-subtasks');
|
||||
|
||||
function userIsMember() {
|
||||
return Meteor.user() && Meteor.user().isBoardMember();
|
||||
}
|
||||
|
||||
// Disable sorting if the current user is not a board member
|
||||
this.autorun(() => {
|
||||
if ($checklistsDom.data('sortable')) {
|
||||
$checklistsDom.sortable('option', 'disabled', !userIsMember());
|
||||
const disabled = !userIsMember() || Utils.isMiniScreen();
|
||||
if (
|
||||
$checklistsDom.data('uiSortable') ||
|
||||
$checklistsDom.data('sortable')
|
||||
) {
|
||||
$checklistsDom.sortable('option', 'disabled', disabled);
|
||||
}
|
||||
if ($subtasksDom.data('sortable')) {
|
||||
$subtasksDom.sortable('option', 'disabled', !userIsMember());
|
||||
}
|
||||
if ($checklistsDom.data('sortable')) {
|
||||
$checklistsDom.sortable('option', 'disabled', Utils.isMiniScreen());
|
||||
}
|
||||
if ($subtasksDom.data('sortable')) {
|
||||
$subtasksDom.sortable('option', 'disabled', Utils.isMiniScreen());
|
||||
if ($subtasksDom.data('uiSortable') || $subtasksDom.data('sortable')) {
|
||||
$subtasksDom.sortable('option', 'disabled', disabled);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -373,6 +348,9 @@ BlazeComponent.extendComponent({
|
||||
this.data().setRequestedBy('');
|
||||
}
|
||||
},
|
||||
'click .js-go-to-linked-card'() {
|
||||
Utils.goCardId(this.data().linkedId);
|
||||
},
|
||||
'click .js-member': Popup.open('cardMember'),
|
||||
'click .js-add-members': Popup.open('cardMembers'),
|
||||
'click .js-assignee': Popup.open('cardAssignee'),
|
||||
@@ -382,6 +360,8 @@ BlazeComponent.extendComponent({
|
||||
'click .js-start-date': Popup.open('editCardStartDate'),
|
||||
'click .js-due-date': Popup.open('editCardDueDate'),
|
||||
'click .js-end-date': Popup.open('editCardEndDate'),
|
||||
'click .js-show-positive-votes': Popup.open('positiveVoteMembers'),
|
||||
'click .js-show-negative-votes': Popup.open('negativeVoteMembers'),
|
||||
'mouseenter .js-card-details'() {
|
||||
const parentComponent = this.parentComponent().parentComponent();
|
||||
//on mobile view parent is Board, not BoardBody.
|
||||
@@ -409,9 +389,9 @@ BlazeComponent.extendComponent({
|
||||
const forIt = $(e.target).hasClass('js-vote-positive');
|
||||
let newState = null;
|
||||
if (
|
||||
this.voteState() == null ||
|
||||
(this.voteState() == false && forIt) ||
|
||||
(this.voteState() == true && !forIt)
|
||||
this.data().voteState() === null ||
|
||||
(this.data().voteState() === false && forIt) ||
|
||||
(this.data().voteState() === true && !forIt)
|
||||
) {
|
||||
newState = forIt;
|
||||
}
|
||||
@@ -609,11 +589,6 @@ Template.cardDetailsActionsPopup.events({
|
||||
'click .js-copy-card': Popup.open('copyCard'),
|
||||
'click .js-copy-checklist-cards': Popup.open('copyChecklistToManyCards'),
|
||||
'click .js-set-card-color': Popup.open('setCardColor'),
|
||||
'click .js-cancel-voting'(event) {
|
||||
event.preventDefault();
|
||||
this.unsetVote();
|
||||
Popup.close();
|
||||
},
|
||||
'click .js-move-card-to-top'(event) {
|
||||
event.preventDefault();
|
||||
const minOrder = _.min(
|
||||
@@ -716,7 +691,7 @@ BlazeComponent.extendComponent({
|
||||
_id: { $ne: Meteor.user().getTemplatesBoardId() },
|
||||
},
|
||||
{
|
||||
sort: ['title'],
|
||||
sort: { sort: 1 /* boards default sorting */ },
|
||||
},
|
||||
);
|
||||
return boards;
|
||||
@@ -892,7 +867,7 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
},
|
||||
{
|
||||
sort: ['title'],
|
||||
sort: { sort: 1 /* boards default sorting */ },
|
||||
},
|
||||
);
|
||||
return boards;
|
||||
@@ -965,7 +940,23 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
'click .js-delete': Popup.afterConfirm('cardDelete', function() {
|
||||
Popup.close();
|
||||
Cards.remove(this._id);
|
||||
// verify that there are no linked cards
|
||||
if (Cards.find({ linkedId: this._id }).count() === 0) {
|
||||
Cards.remove(this._id);
|
||||
} else {
|
||||
// TODO: Maybe later we can list where the linked cards are.
|
||||
// Now here is popup with a hint that the card cannot be deleted
|
||||
// as there are linked cards.
|
||||
// Related:
|
||||
// client/components/lists/listHeader.js about line 248
|
||||
// https://github.com/wekan/wekan/issues/2785
|
||||
const message = `${TAPi18n.__(
|
||||
'delete-linked-card-before-this-card',
|
||||
)} linkedId: ${
|
||||
this._id
|
||||
} at client/components/cards/cardDetails.js and https://github.com/wekan/wekan/issues/2785`;
|
||||
alert(message);
|
||||
}
|
||||
Utils.goBoardId(this.boardId);
|
||||
}),
|
||||
'change .js-field-parent-board'(event) {
|
||||
@@ -998,17 +989,93 @@ BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-end-date': Popup.open('editVoteEndDate'),
|
||||
'submit .edit-vote-question'(evt) {
|
||||
evt.preventDefault();
|
||||
const voteQuestion = evt.target.vote.value;
|
||||
this.currentCard.setVoteQuestion(voteQuestion);
|
||||
const publicVote = $('#vote-public').hasClass('is-checked');
|
||||
const allowNonBoardMembers = $('#vote-allow-non-members').hasClass(
|
||||
'is-checked',
|
||||
);
|
||||
const endString = this.currentCard.getVoteEnd();
|
||||
|
||||
this.currentCard.setVoteQuestion(
|
||||
voteQuestion,
|
||||
publicVote,
|
||||
allowNonBoardMembers,
|
||||
);
|
||||
if (endString) {
|
||||
this.currentCard.setVoteEnd(endString);
|
||||
}
|
||||
Popup.close();
|
||||
},
|
||||
'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => {
|
||||
event.preventDefault();
|
||||
this.currentCard.unsetVote();
|
||||
Popup.close();
|
||||
}),
|
||||
'click a.js-toggle-vote-public'(event) {
|
||||
event.preventDefault();
|
||||
$('#vote-public').toggleClass('is-checked');
|
||||
},
|
||||
'click a.js-toggle-vote-allow-non-members'(event) {
|
||||
event.preventDefault();
|
||||
$('#vote-allow-non-members').toggleClass('is-checked');
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('cardStartVotingPopup');
|
||||
|
||||
// editVoteEndDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
this.data().getVoteEnd() && this.date.set(moment(this.data().getVoteEnd()));
|
||||
}
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .edit-date'(evt) {
|
||||
evt.preventDefault();
|
||||
|
||||
// if no time was given, init with 12:00
|
||||
const time =
|
||||
evt.target.time.value ||
|
||||
moment(new Date().setHours(12, 0, 0)).format('LT');
|
||||
|
||||
const dateString = `${evt.target.date.value} ${time}`;
|
||||
const newDate = moment(dateString, 'L LT', true);
|
||||
if (newDate.isValid()) {
|
||||
// if active vote - store it
|
||||
if (this.currentData().getVoteQuestion()) {
|
||||
this._storeDate(newDate.toDate());
|
||||
Popup.close();
|
||||
} else {
|
||||
this.currentData().vote = { end: newDate.toDate() }; // set vote end temp
|
||||
Popup.back();
|
||||
}
|
||||
} else {
|
||||
this.error.set('invalid-date');
|
||||
evt.target.date.focus();
|
||||
}
|
||||
},
|
||||
'click .js-delete-date'(evt) {
|
||||
evt.preventDefault();
|
||||
this._deleteDate();
|
||||
Popup.close();
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
_storeDate(newDate) {
|
||||
this.card.setVoteEnd(newDate);
|
||||
}
|
||||
_deleteDate() {
|
||||
this.card.unsetVoteEnd();
|
||||
}
|
||||
}.register('editVoteEndDatePopup'));
|
||||
|
||||
// Close the card details pane by pressing escape
|
||||
EscapeActions.register(
|
||||
'detailsPane',
|
||||
|
||||
@@ -10,6 +10,9 @@ avatar-radius = 50%
|
||||
left: -2000px
|
||||
top: 0px
|
||||
|
||||
#clipboard
|
||||
white-space: normal
|
||||
|
||||
.assignee
|
||||
border-radius: 3px
|
||||
display: block
|
||||
@@ -37,6 +40,8 @@ avatar-radius = 50%
|
||||
position: absolute
|
||||
|
||||
&.avatar-image
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
height: 100%
|
||||
width: @height
|
||||
|
||||
@@ -84,7 +89,7 @@ avatar-radius = 50%
|
||||
.card-details
|
||||
padding: 0
|
||||
flex-shrink: 0
|
||||
flex-basis: 510px
|
||||
flex-basis: 600px
|
||||
will-change: flex-basis
|
||||
overflow-y: scroll
|
||||
overflow-x: hidden
|
||||
@@ -94,21 +99,18 @@ avatar-radius = 50%
|
||||
animation: flexGrowIn 0.1s
|
||||
box-shadow: 0 0 7px 0 darken(white, 30%)
|
||||
transition: flex-basis 0.1s
|
||||
box-sizing: border-box
|
||||
|
||||
.mCustomScrollBox
|
||||
padding-left: 0
|
||||
|
||||
.ps-scrollbar-y-rail
|
||||
pointer-event: all
|
||||
position: absolute
|
||||
|
||||
.card-details-canvas
|
||||
width: 470px
|
||||
padding-left: 20px
|
||||
width: auto
|
||||
padding: 0 20px
|
||||
|
||||
.card-details-header
|
||||
margin: 0 -20px 5px
|
||||
padding 7px 16px
|
||||
padding 7px 20px
|
||||
background: darken(white, 7%)
|
||||
border-bottom: 1px solid darken(white, 14%)
|
||||
|
||||
@@ -196,23 +198,32 @@ avatar-radius = 50%
|
||||
margin-right: 0.5em
|
||||
&:last-child
|
||||
margin-right: 0
|
||||
&.card-details-item-labels,
|
||||
&.card-details-item-labels
|
||||
display: block
|
||||
word-wrap: break-word
|
||||
max-width: 95%
|
||||
flex-grow: 1
|
||||
&.card-details-item-members,
|
||||
&.card-details-item-assignees,
|
||||
&.card-details-item-received,
|
||||
&.card-details-item-start,
|
||||
&.card-details-item-due,
|
||||
&.card-details-item-end,
|
||||
&.card-details-item-customfield,
|
||||
&.card-details-item-name
|
||||
display: block
|
||||
word-wrap: break-word
|
||||
max-width: 48%
|
||||
max-width: 36%
|
||||
flex-grow: 1
|
||||
&.card-details-item-received,
|
||||
&.card-details-item-start,
|
||||
&.card-details-item-due,
|
||||
&.card-details-item-end
|
||||
display: block
|
||||
word-wrap: break-word
|
||||
max-width: 28%
|
||||
flex-grow: 1
|
||||
|
||||
.card-details-item-title
|
||||
font-size: 16px
|
||||
color: #000
|
||||
font-weight: bold
|
||||
color: #4d4d4d
|
||||
|
||||
.card-label
|
||||
padding-top: 5px
|
||||
@@ -336,5 +347,12 @@ card-details-color(background, color...)
|
||||
.vote-title
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
|
||||
.js-edit-date
|
||||
align-self: baseline
|
||||
margin-left: 5px
|
||||
|
||||
.vote-result
|
||||
display: flex
|
||||
.js-show-positive-votes
|
||||
cursor: pointer
|
||||
@@ -1,7 +1,17 @@
|
||||
template(name="checklists")
|
||||
h3
|
||||
i.fa.fa-check
|
||||
| {{_ 'checklists'}}
|
||||
.checklists-title
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-check
|
||||
| {{_ 'checklists'}}
|
||||
if currentUser.isBoardMember
|
||||
.material-toggle-switch
|
||||
span.toggle-switch-title {{_ 'hide-checked-items'}}
|
||||
if hideCheckedItems
|
||||
input.toggle-switch(type="checkbox" id="toggleHideCheckedItemsButton" checked="checked")
|
||||
else
|
||||
input.toggle-switch(type="checkbox" id="toggleHideCheckedItemsButton")
|
||||
label.toggle-label(for="toggleHideCheckedItemsButton")
|
||||
|
||||
if toggleDeleteDialog.get
|
||||
.board-overlay#card-details-overlay
|
||||
+checklistDeleteDialog(checklist = checklistToDelete)
|
||||
@@ -86,9 +96,10 @@ template(name="checklistItems")
|
||||
| {{_ 'add-checklist-item'}}...
|
||||
|
||||
template(name='checklistItemDetail')
|
||||
.js-checklist-item.checklist-item
|
||||
.js-checklist-item.checklist-item(class="{{#if item.isFinished }}is-checked{{#if hideCheckedItems}} invisible{{/if}}{{/if}}")
|
||||
if canModifyCard
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
.check-box-container
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { calculateIndexData, enableClickOnTouch } = Utils;
|
||||
const { calculateIndexData, capitalize } = Utils;
|
||||
|
||||
function initSorting(items) {
|
||||
items.sortable({
|
||||
@@ -36,9 +36,6 @@ function initSorting(items) {
|
||||
checklistItem.move(checklistId, sortIndex.base);
|
||||
},
|
||||
});
|
||||
|
||||
// ugly touch event hotfix
|
||||
enableClickOnTouch('.js-checklist-item:not(.placeholder)');
|
||||
}
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
@@ -54,14 +51,15 @@ BlazeComponent.extendComponent({
|
||||
return Meteor.user() && Meteor.user().isBoardMember();
|
||||
}
|
||||
|
||||
// Disable sorting if the current user is not a board member
|
||||
// Disable sorting if the current user is not a board member or is a miniscreen
|
||||
self.autorun(() => {
|
||||
const $itemsDom = $(self.itemsDom);
|
||||
if ($itemsDom.data('sortable')) {
|
||||
$(self.itemsDom).sortable('option', 'disabled', !userIsMember());
|
||||
}
|
||||
if ($itemsDom.data('sortable')) {
|
||||
$(self.itemsDom).sortable('option', 'disabled', Utils.isMiniScreen());
|
||||
if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
|
||||
$(self.itemsDom).sortable(
|
||||
'option',
|
||||
'disabled',
|
||||
!userIsMember() || Utils.isMiniScreen(),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -177,6 +175,16 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
},
|
||||
|
||||
focusChecklistItem(event) {
|
||||
// If a new checklist is created, pre-fill the title and select it.
|
||||
const checklist = this.currentData().checklist;
|
||||
if (!checklist) {
|
||||
const textarea = event.target;
|
||||
textarea.value = capitalize(TAPi18n.__('r-checklist'));
|
||||
textarea.select();
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
const events = {
|
||||
'click .toggle-delete-checklist-dialog'(event) {
|
||||
@@ -185,6 +193,9 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
this.toggleDeleteDialog.set(!this.toggleDeleteDialog.get());
|
||||
},
|
||||
'click #toggleHideCheckedItemsButton'() {
|
||||
Meteor.call('toggleHideCheckedItems');
|
||||
},
|
||||
};
|
||||
|
||||
return [
|
||||
@@ -196,12 +207,21 @@ BlazeComponent.extendComponent({
|
||||
'submit .js-edit-checklist-item': this.editChecklistItem,
|
||||
'click .js-delete-checklist-item': this.deleteItem,
|
||||
'click .confirm-checklist-delete': this.deleteChecklist,
|
||||
'focus .js-add-checklist-item': this.focusChecklistItem,
|
||||
keydown: this.pressKey,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('checklists');
|
||||
|
||||
Template.checklists.helpers({
|
||||
hideCheckedItems() {
|
||||
const currentUser = Meteor.user();
|
||||
if (currentUser) return currentUser.hasHideCheckedItems();
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
Template.checklistDeleteDialog.onCreated(() => {
|
||||
const $cardDetails = this.$('.card-details');
|
||||
this.scrollState = {
|
||||
@@ -237,6 +257,11 @@ Template.checklistItemDetail.helpers({
|
||||
!Meteor.user().isWorker()
|
||||
);
|
||||
},
|
||||
hideCheckedItems() {
|
||||
const user = Meteor.user();
|
||||
if (user) return user.hasHideCheckedItems();
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
@@ -250,7 +275,7 @@ BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-checklist-item .check-box': this.toggleItem,
|
||||
'click .js-checklist-item .check-box-container': this.toggleItem,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
@@ -16,6 +16,10 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
|
||||
&:hover
|
||||
color: inherit
|
||||
|
||||
.checklists-title
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
|
||||
.checklist-title
|
||||
.checkbox
|
||||
float: left
|
||||
@@ -99,6 +103,17 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
|
||||
margin-top: 3px
|
||||
display: flex
|
||||
background: darken(white, 3%)
|
||||
opacity: 1
|
||||
transition: height 0ms 400ms, opacity 400ms 0ms
|
||||
height: auto
|
||||
overflow: hidden
|
||||
|
||||
&.is-checked.invisible
|
||||
opacity: 0
|
||||
height: 0
|
||||
transition: height 0ms 0ms, opacity 600ms 0ms
|
||||
margin-top: 0
|
||||
margin-bottom: 0
|
||||
|
||||
&.placeholder
|
||||
background: darken(white, 20%)
|
||||
@@ -113,6 +128,9 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
|
||||
&:hover
|
||||
background-color: darken(white, 8%)
|
||||
|
||||
.check-box-container
|
||||
padding-right: 1px;
|
||||
|
||||
.check-box
|
||||
margin: 0.1em 0 0 0;
|
||||
&.is-checked
|
||||
@@ -121,10 +139,11 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
|
||||
|
||||
.item-title
|
||||
flex: 1
|
||||
padding-left: 10px;
|
||||
margin-left: 10px;
|
||||
&.is-checked
|
||||
color: #8c8c8c
|
||||
font-style: italic
|
||||
text-decoration: line-through
|
||||
& .viewer
|
||||
p
|
||||
margin-bottom: 2px
|
||||
|
||||
@@ -158,6 +158,8 @@
|
||||
|
||||
.edit-labels-pop-over
|
||||
margin-bottom: 8px
|
||||
.card-label .viewer p
|
||||
margin: 0
|
||||
|
||||
.edit-labels-pop-over .shortcut
|
||||
display: inline-block
|
||||
|
||||
@@ -4,8 +4,8 @@ template(name="minicard")
|
||||
class="{{#if isLinkedBoard}}linked-board{{/if}}"
|
||||
class="minicard-{{colorClass}}")
|
||||
if isMiniScreen
|
||||
//.handle
|
||||
// .fa.fa-arrows
|
||||
.handle
|
||||
.fa.fa-arrows
|
||||
unless isMiniScreen
|
||||
if showDesktopDragHandles
|
||||
.handle
|
||||
@@ -74,8 +74,12 @@ template(name="minicard")
|
||||
+viewer
|
||||
= definition.name
|
||||
.minicard-custom-field-item
|
||||
+viewer
|
||||
= trueValue
|
||||
if $eq definition.type "currency"
|
||||
+viewer
|
||||
= formattedCurrencyCustomFieldValue(definition)
|
||||
else
|
||||
+viewer
|
||||
= trueValue
|
||||
|
||||
if getAssignees
|
||||
.minicard-assignees.js-minicard-assignees
|
||||
@@ -102,8 +106,10 @@ template(name="minicard")
|
||||
span.badge-icon.fa.fa-align-left
|
||||
if getVoteQuestion
|
||||
.badge.badge-state-image-only(title=getVoteQuestion)
|
||||
span.badge-icon.fa.fa-thumbs-up
|
||||
span.badge-icon.fa.fa-thumbs-down
|
||||
span.badge-icon.fa.fa-thumbs-up(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-text {{ voteCountNegative }}
|
||||
if attachments.count
|
||||
.badge
|
||||
span.badge-icon.fa.fa-paperclip
|
||||
|
||||
@@ -9,6 +9,20 @@ BlazeComponent.extendComponent({
|
||||
return 'minicard';
|
||||
},
|
||||
|
||||
formattedCurrencyCustomFieldValue(definition) {
|
||||
const customField = this.data()
|
||||
.customFieldsWD()
|
||||
.find(f => f._id === definition._id);
|
||||
const customFieldTrueValue =
|
||||
customField && customField.trueValue ? customField.trueValue : '';
|
||||
|
||||
const locale = TAPi18n.getLanguage();
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: definition.settings.currencyCode,
|
||||
}).format(customFieldTrueValue);
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -87,7 +87,9 @@
|
||||
width: 11px
|
||||
height: @width
|
||||
border-radius: 2px
|
||||
margin-left: 3px
|
||||
margin-right: 3px
|
||||
margin-bottom: 3px
|
||||
|
||||
.minicard-custom-fields
|
||||
display:block;
|
||||
.minicard-custom-field
|
||||
@@ -299,3 +301,8 @@ minicard-color(background, color...)
|
||||
|
||||
.minicard-indigo
|
||||
minicard-color(#4b0082, #ffffff) //White text for better visibility
|
||||
|
||||
.text-red
|
||||
color:red
|
||||
.text-green
|
||||
color:green
|
||||
@@ -1,5 +1,5 @@
|
||||
template(name="subtasks")
|
||||
h3
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sitemap
|
||||
| {{_ 'subtasks'}}
|
||||
if toggleDeleteDialog.get
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
export function getMembersToMap(data) {
|
||||
// we will work on the list itself (an ordered array of objects) when a
|
||||
// mapping is done, we add a 'wekan' field to the object representing the
|
||||
// imported member
|
||||
|
||||
const membersToMap = [];
|
||||
const importedMembers = [];
|
||||
let membersIndex;
|
||||
|
||||
for (let i = 0; i < data[0].length; i++) {
|
||||
if (data[0][i].toLowerCase() === 'members') {
|
||||
membersIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
if (data[i][membersIndex]) {
|
||||
for (const importedMember of data[i][membersIndex].split(' ')) {
|
||||
if (importedMember && importedMembers.indexOf(importedMember) === -1) {
|
||||
importedMembers.push(importedMember);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let importedMember of importedMembers) {
|
||||
importedMember = {
|
||||
username: importedMember,
|
||||
id: importedMember,
|
||||
};
|
||||
const wekanUser = Users.findOne({ username: importedMember.username });
|
||||
if (wekanUser) importedMember.wekanId = wekanUser._id;
|
||||
membersToMap.push(importedMember);
|
||||
}
|
||||
|
||||
return membersToMap;
|
||||
}
|
||||
@@ -13,11 +13,8 @@ template(name="import")
|
||||
template(name="importTextarea")
|
||||
form
|
||||
p: label(for='import-textarea') {{_ instruction}} {{_ 'import-board-instruction-about-errors'}}
|
||||
textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
|
||||
textarea.js-import-json(placeholder="{{_ importPlaceHolder}}" autofocus)
|
||||
| {{jsonText}}
|
||||
if isSandstorm
|
||||
h1.warning {{_ 'import-sandstorm-backup-warning'}}
|
||||
p.warning {{_ 'import-sandstorm-warning'}}
|
||||
input.primary.wide(type="submit" value="{{_ 'import'}}")
|
||||
|
||||
template(name="importMapMembers")
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import trelloMembersMapper from './trelloMembersMapper';
|
||||
import wekanMembersMapper from './wekanMembersMapper';
|
||||
import csvMembersMapper from './csvMembersMapper';
|
||||
|
||||
const Papa = require('papaparse');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
title() {
|
||||
@@ -30,20 +33,30 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
},
|
||||
|
||||
importData(evt) {
|
||||
importData(evt, dataSource) {
|
||||
evt.preventDefault();
|
||||
const dataJson = this.find('.js-import-json').value;
|
||||
try {
|
||||
const dataObject = JSON.parse(dataJson);
|
||||
this.setError('');
|
||||
this.importedData.set(dataObject);
|
||||
const membersToMap = this._prepareAdditionalData(dataObject);
|
||||
// store members data and mapping in Session
|
||||
// (we go deep and 2-way, so storing in data context is not a viable option)
|
||||
const input = this.find('.js-import-json').value;
|
||||
if (dataSource === 'csv') {
|
||||
const csv = input.indexOf('\t') > 0 ? input.replace(/(\t)/g, ',') : input;
|
||||
const ret = Papa.parse(csv);
|
||||
if (ret && ret.data && ret.data.length) this.importedData.set(ret.data);
|
||||
else throw new Meteor.Error('error-csv-schema');
|
||||
const membersToMap = this._prepareAdditionalData(ret.data);
|
||||
this.membersToMap.set(membersToMap);
|
||||
this.nextStep();
|
||||
} catch (e) {
|
||||
this.setError('error-json-malformed');
|
||||
} else {
|
||||
try {
|
||||
const dataObject = JSON.parse(input);
|
||||
this.setError('');
|
||||
this.importedData.set(dataObject);
|
||||
const membersToMap = this._prepareAdditionalData(dataObject);
|
||||
// store members data and mapping in Session
|
||||
// (we go deep and 2-way, so storing in data context is not a viable option)
|
||||
this.membersToMap.set(membersToMap);
|
||||
this.nextStep();
|
||||
} catch (e) {
|
||||
this.setError('error-json-malformed');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -91,6 +104,9 @@ BlazeComponent.extendComponent({
|
||||
case 'wekan':
|
||||
membersToMap = wekanMembersMapper.getMembersToMap(dataObject);
|
||||
break;
|
||||
case 'csv':
|
||||
membersToMap = csvMembersMapper.getMembersToMap(dataObject);
|
||||
break;
|
||||
}
|
||||
return membersToMap;
|
||||
},
|
||||
@@ -109,11 +125,23 @@ BlazeComponent.extendComponent({
|
||||
return `import-board-instruction-${Session.get('importSource')}`;
|
||||
},
|
||||
|
||||
importPlaceHolder() {
|
||||
const importSource = Session.get('importSource');
|
||||
if (importSource === 'csv') {
|
||||
return 'import-csv-placeholder';
|
||||
} else {
|
||||
return 'import-json-placeholder';
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
submit(evt) {
|
||||
return this.parentComponent().importData(evt);
|
||||
return this.parentComponent().importData(
|
||||
evt,
|
||||
Session.get('importSource'),
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Cookies } from 'meteor/ostrio:cookies';
|
||||
const cookies = new Cookies();
|
||||
const { calculateIndex, enableClickOnTouch } = Utils;
|
||||
const { calculateIndex } = Utils;
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
// Proxy
|
||||
@@ -74,18 +74,16 @@ BlazeComponent.extendComponent({
|
||||
const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
|
||||
const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
|
||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
let swimlaneId = '';
|
||||
const defaultSwimlaneId = currentBoard.getDefaultSwimline()._id;
|
||||
let targetSwimlaneId = null;
|
||||
|
||||
// only set a new swimelane ID if the swimlanes view is active
|
||||
if (
|
||||
Utils.boardView() === 'board-view-swimlanes' ||
|
||||
currentBoard.isTemplatesBoard()
|
||||
)
|
||||
swimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))._id;
|
||||
else if (
|
||||
Utils.boardView() === 'board-view-lists' ||
|
||||
Utils.boardView() === 'board-view-cal' ||
|
||||
!Utils.boardView
|
||||
)
|
||||
swimlaneId = currentBoard.getDefaultSwimline()._id;
|
||||
targetSwimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))
|
||||
._id;
|
||||
|
||||
// Normally the jquery-ui sortable library moves the dragged DOM element
|
||||
// to its new position, which disrupts Blaze reactive updates mechanism
|
||||
@@ -98,9 +96,12 @@ BlazeComponent.extendComponent({
|
||||
|
||||
if (MultiSelection.isActive()) {
|
||||
Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => {
|
||||
const newSwimlaneId = targetSwimlaneId
|
||||
? targetSwimlaneId
|
||||
: card.swimlaneId || defaultSwimlaneId;
|
||||
card.move(
|
||||
currentBoard._id,
|
||||
swimlaneId,
|
||||
newSwimlaneId,
|
||||
listId,
|
||||
sortIndex.base + i * sortIndex.increment,
|
||||
);
|
||||
@@ -108,15 +109,15 @@ BlazeComponent.extendComponent({
|
||||
} else {
|
||||
const cardDomElement = ui.item.get(0);
|
||||
const card = Blaze.getData(cardDomElement);
|
||||
card.move(currentBoard._id, swimlaneId, listId, sortIndex.base);
|
||||
const newSwimlaneId = targetSwimlaneId
|
||||
? targetSwimlaneId
|
||||
: card.swimlaneId || defaultSwimlaneId;
|
||||
card.move(currentBoard._id, newSwimlaneId, listId, sortIndex.base);
|
||||
}
|
||||
boardComponent.setIsDragging(false);
|
||||
},
|
||||
});
|
||||
|
||||
// ugly touch event hotfix
|
||||
enableClickOnTouch(itemsSelector);
|
||||
|
||||
this.autorun(() => {
|
||||
let showDesktopDragHandles = false;
|
||||
currentUser = Meteor.user();
|
||||
@@ -129,7 +130,7 @@ BlazeComponent.extendComponent({
|
||||
showDesktopDragHandles = false;
|
||||
}
|
||||
|
||||
if (!Utils.isMiniScreen() && showDesktopDragHandles) {
|
||||
if (Utils.isMiniScreen() || showDesktopDragHandles) {
|
||||
$cards.sortable({
|
||||
handle: '.handle',
|
||||
});
|
||||
@@ -139,27 +140,16 @@ BlazeComponent.extendComponent({
|
||||
});
|
||||
}
|
||||
|
||||
if ($cards.data('sortable')) {
|
||||
if ($cards.data('uiSortable') || $cards.data('sortable')) {
|
||||
$cards.sortable(
|
||||
'option',
|
||||
'disabled',
|
||||
// Disable drag-dropping when user is not member/is miniscreen
|
||||
// Disable drag-dropping when user is not member
|
||||
!userIsMember(),
|
||||
// Not disable drag-dropping while in multi-selection mode
|
||||
// MultiSelection.isActive() || !userIsMember(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($cards.data('sortable')) {
|
||||
$cards.sortable(
|
||||
'option',
|
||||
'disabled',
|
||||
// Disable drag-dropping when user is not member/is miniscreen
|
||||
Utils.isMiniScreen(),
|
||||
// Not disable drag-dropping while in multi-selection mode
|
||||
// MultiSelection.isActive() || !userIsMember(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// We want to re-run this function any time a card is added.
|
||||
|
||||
@@ -43,9 +43,6 @@
|
||||
background: white
|
||||
margin: -3px 0 8px
|
||||
|
||||
.list-header-card-count
|
||||
height: 35px
|
||||
|
||||
.list-header-add
|
||||
flex: 0 0 auto
|
||||
padding: 20px 12px 4px
|
||||
@@ -60,6 +57,9 @@
|
||||
background-color: #e4e4e4;
|
||||
border-bottom: 6px solid #e4e4e4;
|
||||
|
||||
&.list-header-card-count
|
||||
min-height: 35px
|
||||
height: auto
|
||||
|
||||
&.ui-sortable-handle
|
||||
cursor: grab
|
||||
@@ -120,9 +120,6 @@
|
||||
form
|
||||
margin-bottom: 9px
|
||||
|
||||
.ps-scrollbar-y-rail
|
||||
transform: translateX(2px)
|
||||
|
||||
.open-minicard-composer
|
||||
border-radius: 2px
|
||||
color: #8c8c8c
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
template(name="listBody")
|
||||
.list-body.js-perfect-scrollbar
|
||||
.list-body
|
||||
.minicards.clearfix.js-minicards(class="{{#if reachedWipLimit}}js-list-full{{/if}}")
|
||||
if cards.count
|
||||
+inlinedForm(autoclose=false position="top")
|
||||
@@ -105,8 +105,10 @@ template(name="searchElementPopup")
|
||||
each boards
|
||||
option(value="{{_id}}") {{title}}
|
||||
form.js-search-term-form
|
||||
label
|
||||
| {{_ 'template'}}
|
||||
input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus dir="auto")
|
||||
.list-body.js-perfect-scrollbar.search-card-results
|
||||
.list-body.search-card-results
|
||||
.minicards.clearfix.js-minicards
|
||||
if isBoardTemplateSearch
|
||||
each results
|
||||
|
||||
@@ -8,7 +8,7 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
mixins() {
|
||||
return [Mixins.PerfectScrollbar];
|
||||
return [];
|
||||
},
|
||||
|
||||
openForm(options) {
|
||||
@@ -77,7 +77,7 @@ BlazeComponent.extendComponent({
|
||||
else if (
|
||||
Utils.boardView() === 'board-view-lists' ||
|
||||
Utils.boardView() === 'board-view-cal' ||
|
||||
!Utils.boardView
|
||||
!Utils.boardView()
|
||||
)
|
||||
swimlaneId = board.getDefaultSwimline()._id;
|
||||
|
||||
@@ -411,7 +411,7 @@ BlazeComponent.extendComponent({
|
||||
type: 'board',
|
||||
},
|
||||
{
|
||||
sort: ['title'],
|
||||
sort: { sort: 1 /* boards default sorting */ },
|
||||
},
|
||||
);
|
||||
return boards;
|
||||
@@ -523,7 +523,7 @@ BlazeComponent.extendComponent({
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
mixins() {
|
||||
return [Mixins.PerfectScrollbar];
|
||||
return [];
|
||||
},
|
||||
|
||||
onCreated() {
|
||||
@@ -597,7 +597,7 @@ BlazeComponent.extendComponent({
|
||||
type: 'board',
|
||||
},
|
||||
{
|
||||
sort: ['title'],
|
||||
sort: { sort: 1 /* boards default sorting */ },
|
||||
},
|
||||
);
|
||||
return boards;
|
||||
@@ -658,10 +658,7 @@ BlazeComponent.extendComponent({
|
||||
_id = element.copy(this.boardId, this.swimlaneId, this.listId);
|
||||
// 1.B Linked card
|
||||
} else {
|
||||
delete element._id;
|
||||
element.type = 'cardType-linkedCard';
|
||||
element.linkedId = element.linkedId || element._id;
|
||||
_id = Cards.insert(element);
|
||||
_id = element.link(this.boardId, this.swimlaneId, this.listId);
|
||||
}
|
||||
Filter.addException(_id);
|
||||
// List insertion
|
||||
@@ -675,7 +672,7 @@ BlazeComponent.extendComponent({
|
||||
element.sort = Boards.findOne(this.boardId)
|
||||
.swimlanes()
|
||||
.count();
|
||||
element.type = 'swimlalne';
|
||||
element.type = 'swimlane';
|
||||
_id = element.copy(this.boardId);
|
||||
} else if (this.isBoardTemplateSearch) {
|
||||
board = Boards.findOne(element.linkedId);
|
||||
@@ -724,7 +721,7 @@ BlazeComponent.extendComponent({
|
||||
|
||||
onRendered() {
|
||||
this.spinner = this.find('.sk-spinner-list');
|
||||
this.container = this.$(this.spinner).parents('.js-perfect-scrollbar')[0];
|
||||
this.container = this.$(this.spinner).parents('.list-body')[0];
|
||||
|
||||
$(this.container).on(
|
||||
`scroll.spinner_${this.swimlaneId}_${this.listId}`,
|
||||
|
||||
@@ -30,10 +30,9 @@ template(name="listHeader")
|
||||
if canSeeAddCard
|
||||
a.js-add-card.fa.fa-plus.list-header-plus-icon
|
||||
a.fa.fa-navicon.js-open-list-menu
|
||||
//a.list-header-handle.handle.fa.fa-arrows.js-list-handle
|
||||
else
|
||||
a.list-header-menu-icon.fa.fa-angle-right.js-select-list
|
||||
//a.list-header-handle.handle.fa.fa-arrows.js-list-handle
|
||||
a.list-header-handle.handle.fa.fa-arrows.js-list-handle
|
||||
else if currentUser.isBoardMember
|
||||
if isWatching
|
||||
i.list-header-watch-icon.fa.fa-eye
|
||||
|
||||
@@ -223,8 +223,35 @@ BlazeComponent.extendComponent({
|
||||
Template.listMorePopup.events({
|
||||
'click .js-delete': Popup.afterConfirm('listDelete', function() {
|
||||
Popup.close();
|
||||
this.allCards().map(card => Cards.remove(card._id));
|
||||
Lists.remove(this._id);
|
||||
// TODO how can we avoid the fetch call?
|
||||
const allCards = this.allCards().fetch();
|
||||
const allCardIds = _.pluck(allCards, '_id');
|
||||
// it's okay if the linked cards are on the same list
|
||||
if (
|
||||
Cards.find({
|
||||
$and: [
|
||||
{ listId: { $ne: this._id } },
|
||||
{ linkedId: { $in: allCardIds } },
|
||||
],
|
||||
}).count() === 0
|
||||
) {
|
||||
allCardIds.map(_id => Cards.remove(_id));
|
||||
Lists.remove(this._id);
|
||||
} else {
|
||||
// TODO: Figure out more informative message.
|
||||
// Popup with a hint that the list cannot be deleted as there are
|
||||
// linked cards. We can adapt the query above so we can list the linked
|
||||
// cards.
|
||||
// Related:
|
||||
// client/components/cards/cardDetails.js about line 969
|
||||
// https://github.com/wekan/wekan/issues/2785
|
||||
const message = `${TAPi18n.__(
|
||||
'delete-linked-cards-before-this-list',
|
||||
)} linkedId: ${
|
||||
this._id
|
||||
} at client/components/lists/listHeader.js and https://github.com/wekan/wekan/issues/2785`;
|
||||
alert(message);
|
||||
}
|
||||
Utils.goBoardId(this.boardId);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -330,7 +330,7 @@ Template.viewer.events({
|
||||
// the corresponding text). Clicking a link shouldn't fire these actions, stop
|
||||
// we stop these event at the viewer component level.
|
||||
'click a'(event, templateInstance) {
|
||||
let prevent = true;
|
||||
const prevent = true;
|
||||
const userId = event.currentTarget.dataset.userid;
|
||||
if (userId) {
|
||||
Popup.open('member').call({ userId }, event, templateInstance);
|
||||
|
||||
@@ -15,3 +15,27 @@
|
||||
local('Roboto-Bold'),
|
||||
url('/fonts/roboto-bold.woff2') format('woff2'),
|
||||
url('/fonts/roboto-bold.woff') format('woff')
|
||||
|
||||
@font-face
|
||||
font-family: 'Poppins'
|
||||
font-style: normal
|
||||
font-weight: 400
|
||||
src: local('Poppins'),
|
||||
local('Poppins-Regular'),
|
||||
url('/fonts/poppins-regular.woff') format('woff')
|
||||
|
||||
@font-face
|
||||
font-family: 'Poppins'
|
||||
font-style: normal
|
||||
font-weight: 500
|
||||
src: local('Poppins Medium'),
|
||||
local('Poppins-Medium'),
|
||||
url('/fonts/poppins-medium.woff') format('woff')
|
||||
|
||||
@font-face
|
||||
font-family: 'Poppins'
|
||||
font-style: normal
|
||||
font-weight: 700
|
||||
src: local('Poppins Bold'),
|
||||
local('Poppins-Bold'),
|
||||
url('/fonts/poppins-bold.woff') format('woff')
|
||||
@@ -24,6 +24,11 @@ template(name="header")
|
||||
a(href="{{pathFor 'home'}}")
|
||||
span.fa.fa-home
|
||||
| {{_ 'all-boards'}}
|
||||
li.separator -
|
||||
li
|
||||
a(href="{{pathFor 'public'}}")
|
||||
span.fa.fa-globe
|
||||
| {{_ 'public'}}
|
||||
each currentUser.starredBoards
|
||||
li.separator -
|
||||
li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
&.current
|
||||
color: darken(white, 5%)
|
||||
|
||||
&:first-child .fa-home
|
||||
&:first-child .fa-home,&:nth-child(3) .fa-globe
|
||||
margin-right: 5px
|
||||
|
||||
a.js-create-board
|
||||
@@ -175,7 +175,7 @@
|
||||
.board-header-btn
|
||||
height: 32px
|
||||
line-height: @height
|
||||
font-size: 16px
|
||||
font-size: 15px
|
||||
|
||||
i.fa
|
||||
line-height: 32px
|
||||
|
||||
@@ -6,10 +6,16 @@ head
|
||||
where the application is deployed with a path prefix, but it seems to be
|
||||
difficult to do that cleanly with Blaze -- at least without adding extra
|
||||
packages.
|
||||
link(rel="shortcut icon" href="/wekan-favicon.png")
|
||||
link(rel="apple-touch-icon" href="/wekan-favicon.png")
|
||||
link(rel="mask-icon" href="/wekan-logo-150.svg")
|
||||
link(rel="manifest" href="/wekan-manifest.json")
|
||||
link(rel="shortcut icon" type="image/x-icon" href="/favicon.ico")
|
||||
link(rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png")
|
||||
link(rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png")
|
||||
link(rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png")
|
||||
link(rel="manifest" href="/site.webmanifest")
|
||||
link(rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5")
|
||||
meta(name="apple-mobile-web-app-title" content="Wekan")
|
||||
meta(name="application-name" content="Wekan")
|
||||
meta(name="msapplication-TileColor" content="#00aba9")
|
||||
meta(name="theme-color" content="#ffffff")
|
||||
|
||||
template(name="userFormsLayout")
|
||||
section.auth-layout
|
||||
|
||||
@@ -31,6 +31,11 @@ Template.userFormsLayout.onCreated(function() {
|
||||
return this.stop();
|
||||
},
|
||||
});
|
||||
Meteor.call('isPasswordLoginDisabled', (_, result) => {
|
||||
if (result) {
|
||||
$('.at-pwd-form').hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.userFormsLayout.onRendered(() => {
|
||||
@@ -69,10 +74,34 @@ Template.userFormsLayout.helpers({
|
||||
let name = lang.name;
|
||||
if (lang.name === 'br') {
|
||||
name = 'Brezhoneg';
|
||||
} else if (lang.name === 'Español') {
|
||||
name = 'español';
|
||||
} else if (lang.name === 'Español de Argentina') {
|
||||
name = 'español de Argentina';
|
||||
} else if (lang.name === 'Español de Chile') {
|
||||
name = 'español de Chile';
|
||||
} else if (lang.name === 'Español de Colombia') {
|
||||
name = 'español de Colombia';
|
||||
} else if (lang.name === 'Español de México') {
|
||||
name = 'español de México';
|
||||
} else if (lang.name === 'Español de Puerto Rico') {
|
||||
name = 'español de Puerto Rico';
|
||||
} else if (lang.name === 'fr-BE') {
|
||||
name = 'Français (Belgique)';
|
||||
} else if (lang.name === 'fr-CA') {
|
||||
name = 'Français (Canada)';
|
||||
} else if (lang.name === 'fr-CA') {
|
||||
name = 'Français (Canada)';
|
||||
} else if (lang.name === 'ig') {
|
||||
name = 'Igbo';
|
||||
} else if (lang.name === 'lv') {
|
||||
name = 'Latviešu';
|
||||
} else if (lang.name === 'latviešu valoda') {
|
||||
name = 'Latviešu';
|
||||
} else if (lang.name === 'oc') {
|
||||
name = 'Occitan';
|
||||
} else if (lang.name === 'st') {
|
||||
name = 'Sãotomense';
|
||||
} else if (lang.name === '繁体中文(台湾)') {
|
||||
name = '繁體中文(台灣)';
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ a:hover,a:focus
|
||||
border-radius: unset
|
||||
|
||||
html, body, input, select, textarea, button
|
||||
font: 14px Roboto, "Helvetica Neue", Arial, Helvetica, sans-serif
|
||||
font: 14px Roboto, Poppins, "Helvetica Neue", Arial, Helvetica, sans-serif
|
||||
line-height: 18px
|
||||
color: #4d4d4d
|
||||
|
||||
|
||||
@@ -107,11 +107,6 @@ $popupWidth = 300px
|
||||
padding: 8px 4px 8px 10px
|
||||
margin-right: 8px
|
||||
|
||||
&::-webkit-scrollbar-button
|
||||
display: block
|
||||
height: 4px
|
||||
width: 4px
|
||||
|
||||
.at-form
|
||||
.at-error, .at-result
|
||||
padding: 8px 12px
|
||||
@@ -135,6 +130,10 @@ $popupWidth = 300px
|
||||
margin-bottom: 8px
|
||||
|
||||
.pop-over-list
|
||||
li
|
||||
display: block
|
||||
clear: both
|
||||
|
||||
li > a
|
||||
clear: both
|
||||
cursor: pointer
|
||||
@@ -316,6 +315,7 @@ $popupWidth = 300px
|
||||
input[type="file"]
|
||||
margin: 4px 0 12px
|
||||
width: 100%
|
||||
box-sizing: border-box
|
||||
|
||||
.pop-over-list
|
||||
li > a
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
const { isTouchDevice } = Utils;
|
||||
|
||||
Mixins.PerfectScrollbar = BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
if (!isTouchDevice()) {
|
||||
const component = this.mixinParent();
|
||||
const domElement = component.find('.js-perfect-scrollbar');
|
||||
Ps.initialize(domElement);
|
||||
|
||||
// XXX We should create an event map to be consistent with other components
|
||||
// but since BlazeComponent doesn't merge Mixins events transparently I
|
||||
// prefered to use a jQuery event (which is what an event map ends up doing)
|
||||
component.$(domElement).on('mouseenter', () => Ps.update(domElement));
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
.ps-container
|
||||
position: relative
|
||||
@@ -10,8 +10,7 @@ section#notifications-drawer
|
||||
border-radius: 2px
|
||||
max-height: calc(100vh - 28px - 36px)
|
||||
color: black
|
||||
padding-top 36px;
|
||||
overflow: scroll
|
||||
padding-top 36px
|
||||
|
||||
a:hover
|
||||
color: belize !important
|
||||
@@ -66,3 +65,5 @@ section#notifications-drawer
|
||||
display: block
|
||||
padding: 0px 16px
|
||||
margin: 0
|
||||
height: calc(100vh - 102px)
|
||||
overflow-y: scroll
|
||||
@@ -11,7 +11,7 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
},
|
||||
{
|
||||
sort: ['title'],
|
||||
sort: { sort: 1 /* boards default sorting */ },
|
||||
},
|
||||
);
|
||||
return boards;
|
||||
@@ -68,8 +68,8 @@ BlazeComponent.extendComponent({
|
||||
const ruleName = this.data().ruleName.get();
|
||||
const trigger = this.data().triggerVar.get();
|
||||
const actionSelected = this.find('#move-spec-action').value;
|
||||
const swimlaneName = this.find('#swimlaneName').value;
|
||||
const listName = this.find('#listName').value;
|
||||
const swimlaneName = this.find('#swimlaneName').value || '*';
|
||||
const listName = this.find('#listName').value || '*';
|
||||
const boardId = Session.get('currentBoard');
|
||||
const destBoardId = this.find('#board-id').value;
|
||||
const desc = Utils.getTriggerActionDesc(event, this);
|
||||
|
||||
@@ -75,7 +75,7 @@ template(name="cardActions")
|
||||
button.trigger-button.trigger-button-color.js-show-color-palette(
|
||||
id="color-action"
|
||||
class="card-details-{{cardColorButton}}")
|
||||
| {{{_ cardColorButtonText }}}
|
||||
| {{_ cardColorButtonText }}
|
||||
div.trigger-button.js-set-color-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
|
||||
|
||||
@@ -164,6 +164,7 @@ BlazeComponent.extendComponent({
|
||||
const boardId = Session.get('currentBoard');
|
||||
const actionId = Actions.insert({
|
||||
actionType: 'removeMember',
|
||||
// deepcode ignore NoHardcodedCredentials: it's no credential
|
||||
username: '*',
|
||||
boardId,
|
||||
desc,
|
||||
|
||||
@@ -6,10 +6,14 @@ template(name="ruleDetails")
|
||||
.triggers-content
|
||||
.triggers-body
|
||||
.triggers-main-body
|
||||
h4
|
||||
| {{_ 'r-trigger'}}
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
= trigger
|
||||
= trigger
|
||||
h4
|
||||
| {{_ 'r-action'}}
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
|
||||
@@ -48,7 +48,7 @@ BlazeComponent.extendComponent({
|
||||
'members.isAdmin': true,
|
||||
},
|
||||
{
|
||||
sort: ['title'],
|
||||
sort: { sort: 1 /* boards default sorting */ },
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ template(name="sidebar")
|
||||
a.board-header-btn.js-shortcuts
|
||||
i.fa.fa-keyboard-o
|
||||
span {{_ 'keyboard-shortcuts' }}
|
||||
.sidebar-content.js-board-sidebar-content.js-perfect-scrollbar
|
||||
.sidebar-content.js-board-sidebar-content
|
||||
//a.hide-btn.js-hide-sidebar
|
||||
// i.fa.fa-navicon
|
||||
unless isDefaultView
|
||||
@@ -230,6 +230,8 @@ template(name="chooseBoardSource")
|
||||
a(href="{{pathFor '/import/trello'}}") {{_ 'from-trello'}}
|
||||
li
|
||||
a(href="{{pathFor '/import/wekan'}}") {{_ 'from-wekan'}}
|
||||
li
|
||||
a(href="{{pathFor '/import/csv'}}") {{_ 'from-csv'}}
|
||||
|
||||
template(name="archiveBoardPopup")
|
||||
p {{_ 'close-board-pop'}}
|
||||
@@ -298,10 +300,11 @@ template(name="boardMenuPopup")
|
||||
if currentUser.isBoardAdmin
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a(href="{{exportUrl}}", download="{{exportFilename}}")
|
||||
i.fa.fa-share-alt
|
||||
| {{_ 'export-board'}}
|
||||
if withApi
|
||||
li
|
||||
a.js-export-board
|
||||
i.fa.fa-share-alt
|
||||
| {{_ 'export-board'}}
|
||||
li
|
||||
a.js-outgoing-webhooks
|
||||
i.fa.fa-globe
|
||||
@@ -327,9 +330,8 @@ template(name="boardMenuPopup")
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a(href="{{exportUrl}}", download="{{exportFilename}}")
|
||||
a.js-export-board
|
||||
i.fa.fa-share-alt
|
||||
i.fa.fa-sign-out
|
||||
| {{_ 'export-board'}}
|
||||
li
|
||||
a.js-import-board
|
||||
@@ -358,6 +360,25 @@ template(name="boardMenuPopup")
|
||||
i.fa.fa-sitemap
|
||||
| {{_ 'subtask-settings'}}
|
||||
|
||||
template(name="exportBoard")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.download-json-link(href="{{exportUrl}}", download="{{exportJsonFilename}}")
|
||||
i.fa.fa-share-alt
|
||||
| {{_ 'export-board-json'}}
|
||||
//li
|
||||
// a(href="{{exportCsvUrl}}", download="{{exportCsvFilename}}")
|
||||
// i.fa.fa-share-alt
|
||||
// | {{_ 'export-board-csv'}}
|
||||
//li
|
||||
// a(href="{{exportTsvUrl}}", download="{{exportTsvFilename}}")
|
||||
// i.fa.fa-share-alt
|
||||
// | {{_ 'export-board-tsv'}}
|
||||
li
|
||||
a.html-export-board
|
||||
i.fa.fa-archive
|
||||
| {{_ 'export-board-html'}}
|
||||
|
||||
template(name="labelsWidget")
|
||||
.board-widget.board-widget-labels
|
||||
h3
|
||||
|
||||
@@ -16,7 +16,7 @@ const viewTitles = {
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
mixins() {
|
||||
return [Mixins.InfiniteScrolling, Mixins.PerfectScrollbar];
|
||||
return [Mixins.InfiniteScrolling];
|
||||
},
|
||||
|
||||
onCreated() {
|
||||
@@ -213,9 +213,20 @@ Template.boardMenuPopup.events({
|
||||
'click .js-import-board': Popup.open('chooseBoardSource'),
|
||||
'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
|
||||
'click .js-card-settings': Popup.open('boardCardSettings'),
|
||||
'click .js-export-board': Popup.open('exportBoard'),
|
||||
});
|
||||
|
||||
Template.boardMenuPopup.onCreated(function() {
|
||||
this.apiEnabled = new ReactiveVar(false);
|
||||
Meteor.call('_isApiEnabled', (e, result) => {
|
||||
this.apiEnabled.set(result);
|
||||
});
|
||||
});
|
||||
|
||||
Template.boardMenuPopup.helpers({
|
||||
withApi() {
|
||||
return Template.instance().apiEnabled.get();
|
||||
},
|
||||
exportUrl() {
|
||||
const params = {
|
||||
boardId: Session.get('currentBoard'),
|
||||
@@ -395,6 +406,70 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
}).register('chooseBoardSourcePopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'exportBoard';
|
||||
},
|
||||
withApi() {
|
||||
return Template.instance().apiEnabled.get();
|
||||
},
|
||||
exportUrl() {
|
||||
const params = {
|
||||
boardId: Session.get('currentBoard'),
|
||||
};
|
||||
const queryParams = {
|
||||
authToken: Accounts._storedLoginToken(),
|
||||
};
|
||||
return FlowRouter.path('/api/boards/:boardId/export', params, queryParams);
|
||||
},
|
||||
exportCsvUrl() {
|
||||
const params = {
|
||||
boardId: Session.get('currentBoard'),
|
||||
};
|
||||
const queryParams = {
|
||||
authToken: Accounts._storedLoginToken(),
|
||||
};
|
||||
return FlowRouter.path(
|
||||
'/api/boards/:boardId/export/csv',
|
||||
params,
|
||||
queryParams,
|
||||
);
|
||||
},
|
||||
exportTsvUrl() {
|
||||
const params = {
|
||||
boardId: Session.get('currentBoard'),
|
||||
};
|
||||
const queryParams = {
|
||||
authToken: Accounts._storedLoginToken(),
|
||||
delimiter: '\t',
|
||||
};
|
||||
return FlowRouter.path(
|
||||
'/api/boards/:boardId/export/csv',
|
||||
params,
|
||||
queryParams,
|
||||
);
|
||||
},
|
||||
exportJsonFilename() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
return `wekan-export-board-${boardId}.json`;
|
||||
},
|
||||
exportCsvFilename() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
return `wekan-export-board-${boardId}.csv`;
|
||||
},
|
||||
exportTsvFilename() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
return `wekan-export-board-${boardId}.tsv`;
|
||||
},
|
||||
}).register('exportBoardPopup');
|
||||
|
||||
Template.exportBoard.events({
|
||||
'click .html-export-board': async event => {
|
||||
event.preventDefault();
|
||||
await ExportHtml(Popup)();
|
||||
},
|
||||
});
|
||||
|
||||
Template.labelsWidget.events({
|
||||
'click .js-label': Popup.open('editLabel'),
|
||||
'click .js-add-label': Popup.open('createLabel'),
|
||||
@@ -499,7 +574,7 @@ BlazeComponent.extendComponent({
|
||||
'members.userId': Meteor.userId(),
|
||||
},
|
||||
{
|
||||
sort: ['title'],
|
||||
sort: { sort: 1 /* boards default sorting */ },
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -677,7 +752,7 @@ BlazeComponent.extendComponent({
|
||||
'members.userId': Meteor.userId(),
|
||||
},
|
||||
{
|
||||
sort: ['title'],
|
||||
sort: { sort: 1 /* boards default sorting */ },
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -33,6 +33,17 @@ template(name="createCustomFieldPopup")
|
||||
option(value=value selected="selected") {{name}}
|
||||
else
|
||||
option(value=value) {{name}}
|
||||
|
||||
div.js-field-settings.js-field-settings-currency(class="{{#if isTypeNotSelected 'currency'}}hide{{/if}}")
|
||||
label
|
||||
| {{_ 'custom-field-currency-option'}}
|
||||
select.js-field-currency
|
||||
each getCurrencyCodes
|
||||
if selected
|
||||
option(value=value selected="selected") {{name}}
|
||||
else
|
||||
option(value=value) {{name}}
|
||||
|
||||
div.js-field-settings.js-field-settings-dropdown(class="{{#if isTypeNotSelected 'dropdown'}}hide{{/if}}")
|
||||
label
|
||||
| {{_ 'custom-field-dropdown-options'}}
|
||||
|
||||
@@ -16,12 +16,62 @@ BlazeComponent.extendComponent({
|
||||
}).register('customFieldsSidebar');
|
||||
|
||||
const CreateCustomFieldPopup = BlazeComponent.extendComponent({
|
||||
_types: ['text', 'number', 'date', 'dropdown'],
|
||||
_types: ['text', 'number', 'date', 'dropdown', 'currency'],
|
||||
|
||||
_currencyList: [
|
||||
{
|
||||
name: 'US Dollar',
|
||||
code: 'USD',
|
||||
},
|
||||
{
|
||||
name: 'Euro',
|
||||
code: 'EUR',
|
||||
},
|
||||
{
|
||||
name: 'Yen',
|
||||
code: 'JPY',
|
||||
},
|
||||
{
|
||||
name: 'Pound Sterling',
|
||||
code: 'GBP',
|
||||
},
|
||||
{
|
||||
name: 'Australian Dollar',
|
||||
code: 'AUD',
|
||||
},
|
||||
{
|
||||
name: 'Canadian Dollar',
|
||||
code: 'CAD',
|
||||
},
|
||||
{
|
||||
name: 'Swiss Franc',
|
||||
code: 'CHF',
|
||||
},
|
||||
{
|
||||
name: 'Yuan Renminbi',
|
||||
code: 'CNY',
|
||||
},
|
||||
{
|
||||
name: 'Hong Kong Dollar',
|
||||
code: 'HKD',
|
||||
},
|
||||
{
|
||||
name: 'New Zealand Dollar',
|
||||
code: 'NZD',
|
||||
},
|
||||
],
|
||||
|
||||
onCreated() {
|
||||
this.type = new ReactiveVar(
|
||||
this.data().type ? this.data().type : this._types[0],
|
||||
);
|
||||
|
||||
this.currencyCode = new ReactiveVar(
|
||||
this.data().settings && this.data().settings.currencyCode
|
||||
? this.data().settings.currencyCode
|
||||
: this._currencyList[0].code,
|
||||
);
|
||||
|
||||
this.dropdownItems = new ReactiveVar(
|
||||
this.data().settings && this.data().settings.dropdownItems
|
||||
? this.data().settings.dropdownItems
|
||||
@@ -44,6 +94,18 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({
|
||||
return this.type.get() !== type;
|
||||
},
|
||||
|
||||
getCurrencyCodes() {
|
||||
const currentCode = this.currencyCode.get();
|
||||
|
||||
return this._currencyList.map(({ name, code }) => {
|
||||
return {
|
||||
name: `${code} - ${name}`,
|
||||
value: code,
|
||||
selected: code === currentCode,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
getDropdownItems() {
|
||||
const items = this.dropdownItems.get();
|
||||
Array.from(this.findAll('.js-field-settings-dropdown input')).forEach(
|
||||
@@ -62,6 +124,11 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({
|
||||
getSettings() {
|
||||
const settings = {};
|
||||
switch (this.type.get()) {
|
||||
case 'currency': {
|
||||
const currencyCode = this.currencyCode.get();
|
||||
settings.currencyCode = currencyCode;
|
||||
break;
|
||||
}
|
||||
case 'dropdown': {
|
||||
const dropdownItems = this.getDropdownItems().filter(
|
||||
item => !!item.name.trim(),
|
||||
@@ -80,6 +147,10 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({
|
||||
const value = evt.target.value;
|
||||
this.type.set(value);
|
||||
},
|
||||
'change .js-field-currency'(evt) {
|
||||
const value = evt.target.value;
|
||||
this.currencyCode.set(value);
|
||||
},
|
||||
'keydown .js-dropdown-item.last'(evt) {
|
||||
if (evt.target.value.trim() && evt.keyCode === 13) {
|
||||
const items = this.getDropdownItems();
|
||||
|
||||
@@ -157,8 +157,9 @@ Template.disambiguateMultiMemberPopup.events({
|
||||
});
|
||||
|
||||
Template.moveSelectionPopup.events({
|
||||
'click .js-select-list'() {
|
||||
mutateSelectedCards('move', this._id);
|
||||
'click .js-select-list'(event) {
|
||||
// Move the minicard to the end of the target list
|
||||
mutateSelectedCards('moveToEndOfList', { listId: this._id });
|
||||
EscapeActions.executeUpTo('multiselection');
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
template(name="searchSidebar")
|
||||
form.js-search-term-form
|
||||
input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus dir="auto")
|
||||
.list-body.js-perfect-scrollbar
|
||||
.list-body
|
||||
.minilists.clearfix.js-minilists
|
||||
each (lists)
|
||||
a.minilist-wrapper.js-minilist(href=absoluteUrl)
|
||||
|
||||
@@ -11,7 +11,8 @@ template(name="swimlaneHeader")
|
||||
template(name="swimlaneFixedHeader")
|
||||
.swimlane-header(
|
||||
class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}")
|
||||
= title
|
||||
+viewer
|
||||
= title
|
||||
.swimlane-header-menu
|
||||
unless currentUser.isCommentOnly
|
||||
a.fa.fa-plus.js-open-add-swimlane-menu.swimlane-header-plus-icon
|
||||
|
||||
@@ -2,7 +2,7 @@ template(name="swimlane")
|
||||
.swimlane
|
||||
+swimlaneHeader
|
||||
unless collapseSwimlane
|
||||
.swimlane.js-lists.js-swimlane
|
||||
.swimlane.js-lists.js-swimlane(id="swimlane-{{_id}}")
|
||||
if isMiniScreen
|
||||
if currentListIsInThisSwimlane _id
|
||||
+list(currentList)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Cookies } from 'meteor/ostrio:cookies';
|
||||
const cookies = new Cookies();
|
||||
const { calculateIndex, enableClickOnTouch } = Utils;
|
||||
const { calculateIndex } = Utils;
|
||||
|
||||
function currentListIsInThisSwimlane(swimlaneId) {
|
||||
const currentList = Lists.findOne(Session.get('currentList'));
|
||||
@@ -23,8 +23,8 @@ function currentCardIsInThisList(listId, swimlaneId) {
|
||||
currentCard.listId === listId &&
|
||||
currentCard.swimlaneId === swimlaneId
|
||||
);
|
||||
// Default view: board-view-lists
|
||||
else return currentCard && currentCard.listId === listId;
|
||||
|
||||
// https://github.com/wekan/wekan/issues/1623
|
||||
// https://github.com/ChronikEwok/wekan/commit/cad9b20451bb6149bfb527a99b5001873b06c3de
|
||||
// TODO: In public board, if you would like to switch between List/Swimlane view, you could
|
||||
@@ -87,9 +87,6 @@ function initSortable(boardComponent, $listsDom) {
|
||||
},
|
||||
});
|
||||
|
||||
// ugly touch event hotfix
|
||||
enableClickOnTouch('.js-list:not(.js-list-composer)');
|
||||
|
||||
function userIsMember() {
|
||||
return (
|
||||
Meteor.user() &&
|
||||
@@ -111,7 +108,7 @@ function initSortable(boardComponent, $listsDom) {
|
||||
showDesktopDragHandles = false;
|
||||
}
|
||||
|
||||
if (!Utils.isMiniScreen() && showDesktopDragHandles) {
|
||||
if (Utils.isMiniScreen() || showDesktopDragHandles) {
|
||||
$listsDom.sortable({
|
||||
handle: '.js-list-handle',
|
||||
});
|
||||
@@ -122,34 +119,12 @@ function initSortable(boardComponent, $listsDom) {
|
||||
}
|
||||
|
||||
const $listDom = $listsDom;
|
||||
if ($listDom.data('sortable')) {
|
||||
if ($listDom.data('uiSortable') || $listDom.data('sortable')) {
|
||||
$listsDom.sortable(
|
||||
'option',
|
||||
'disabled',
|
||||
// Disable drag-dropping when user is not member/is worker/is miniscreen
|
||||
!userIsMember(),
|
||||
// Not disable drag-dropping while in multi-selection mode
|
||||
// MultiSelection.isActive() || !userIsMember(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($listDom.data('sortable')) {
|
||||
$listsDom.sortable(
|
||||
'option',
|
||||
'disabled',
|
||||
// Disable drag-dropping when user is not member/is worker/is miniscreen
|
||||
Meteor.user().isWorker(),
|
||||
// Not disable drag-dropping while in multi-selection mode
|
||||
// MultiSelection.isActive() || !userIsMember(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($listDom.data('sortable')) {
|
||||
$listsDom.sortable(
|
||||
'option',
|
||||
'disabled',
|
||||
// Disable drag-dropping when user is not member/is worker/is miniscreen
|
||||
Utils.isMiniScreen(),
|
||||
// Disable drag-dropping when user is not member/is worker
|
||||
!userIsMember() || Meteor.user().isWorker(),
|
||||
// Not disable drag-dropping while in multi-selection mode
|
||||
// MultiSelection.isActive() || !userIsMember(),
|
||||
);
|
||||
@@ -210,8 +185,7 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
|
||||
const noDragInside = ['a', 'input', 'textarea', 'p'].concat(
|
||||
Utils.isMiniScreen() ||
|
||||
(!Utils.isMiniScreen() && showDesktopDragHandles)
|
||||
Utils.isMiniScreen() || showDesktopDragHandles
|
||||
? ['.js-list-handle', '.js-swimlane-header-handle']
|
||||
: ['.js-list-header'],
|
||||
);
|
||||
|
||||
@@ -29,6 +29,8 @@ avatar-radius = 50%
|
||||
position: absolute
|
||||
|
||||
&.avatar-image
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
height: 100%
|
||||
width: @height
|
||||
|
||||
|
||||
@@ -112,11 +112,20 @@ template(name="changeSettingsPopup")
|
||||
i.fa.fa-check
|
||||
unless currentUser.isWorker
|
||||
li
|
||||
label.bold
|
||||
label.bold.clear
|
||||
i.fa.fa-sort-numeric-asc
|
||||
| {{_ 'show-cards-minimum-count'}}
|
||||
input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="0" max="99" onkeydown="return false")
|
||||
input.js-apply-show-cards-at.left(type="submit" value="{{_ 'apply'}}")
|
||||
label.bold.clear
|
||||
i.fa.fa-calendar
|
||||
| {{_ 'start-day-of-week'}}
|
||||
select#start-day-of-week.inline-input.left
|
||||
each day in weekDays startDayOfWeek
|
||||
if day.isSelected
|
||||
option(selected="true", value="#{day.value}") #{day.name}
|
||||
else
|
||||
option(value="#{day.value}") #{day.name}
|
||||
input.js-apply-user-settings.left(type="submit" value="{{_ 'apply'}}")
|
||||
|
||||
template(name="userDeletePopup")
|
||||
unless currentUser.isWorker
|
||||
|
||||
@@ -224,6 +224,27 @@ Template.changeSettingsPopup.helpers({
|
||||
return cookies.get('limitToShowCardsCount');
|
||||
}
|
||||
},
|
||||
weekDays(startDay) {
|
||||
return [
|
||||
TAPi18n.__('sunday'),
|
||||
TAPi18n.__('monday'),
|
||||
TAPi18n.__('tuesday'),
|
||||
TAPi18n.__('wednesday'),
|
||||
TAPi18n.__('thursday'),
|
||||
TAPi18n.__('friday'),
|
||||
TAPi18n.__('saturday'),
|
||||
].map(function(day, index) {
|
||||
return { name: day, value: index, isSelected: index === startDay };
|
||||
});
|
||||
},
|
||||
startDayOfWeek() {
|
||||
currentUser = Meteor.user();
|
||||
if (currentUser) {
|
||||
return currentUser.getStartDayOfWeek();
|
||||
} else {
|
||||
return cookies.get('startDayOfWeek');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.changeSettingsPopup.events({
|
||||
@@ -247,20 +268,31 @@ Template.changeSettingsPopup.events({
|
||||
cookies.set('hasHiddenSystemMessages', 'true');
|
||||
}
|
||||
},
|
||||
'click .js-apply-show-cards-at'(event, templateInstance) {
|
||||
'click .js-apply-user-settings'(event, templateInstance) {
|
||||
event.preventDefault();
|
||||
const minLimit = parseInt(
|
||||
templateInstance.$('#show-cards-count-at').val(),
|
||||
10,
|
||||
);
|
||||
const startDay = parseInt(
|
||||
templateInstance.$('#start-day-of-week').val(),
|
||||
10,
|
||||
);
|
||||
const currentUser = Meteor.user();
|
||||
if (!isNaN(minLimit)) {
|
||||
currentUser = Meteor.user();
|
||||
if (currentUser) {
|
||||
Meteor.call('changeLimitToShowCardsCount', minLimit);
|
||||
} else {
|
||||
cookies.set('limitToShowCardsCount', minLimit);
|
||||
}
|
||||
Popup.back();
|
||||
}
|
||||
if (!isNaN(startDay)) {
|
||||
if (currentUser) {
|
||||
Meteor.call('changeStartDayOfWeek', startDay);
|
||||
} else {
|
||||
cookies.set('startDayOfWeek', startDay);
|
||||
}
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
@@ -10,12 +10,22 @@ DatePicker = BlazeComponent.extendComponent({
|
||||
this.defaultTime = defaultTime;
|
||||
},
|
||||
|
||||
startDayOfWeek() {
|
||||
const currentUser = Meteor.user();
|
||||
if (currentUser) {
|
||||
return currentUser.getStartDayOfWeek();
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
|
||||
onRendered() {
|
||||
const $picker = this.$('.js-datepicker')
|
||||
.datepicker({
|
||||
todayHighlight: true,
|
||||
todayBtn: 'linked',
|
||||
language: TAPi18n.getLanguage(),
|
||||
weekStart: this.startDayOfWeek(),
|
||||
})
|
||||
.on(
|
||||
'changeDate',
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
};
|
||||
return this.each(function() {
|
||||
const element = this;
|
||||
$(element).bind('dragenter dragover dragleave', stopFn);
|
||||
return $(element).bind('drop', function(event) {
|
||||
$(element).on('dragenter dragover dragleave', stopFn);
|
||||
return $(element).on('drop', function(event) {
|
||||
stopFn(event);
|
||||
const files = event.dataTransfer.files;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
const JSZip = require('jszip');
|
||||
|
||||
window.ExportHtml = Popup => {
|
||||
const saveAs = function(blob, filename) {
|
||||
const dl = document.createElement('a');
|
||||
dl.href = window.URL.createObjectURL(blob);
|
||||
dl.onclick = event => document.body.removeChild(event.target);
|
||||
dl.style.display = 'none';
|
||||
dl.target = '_blank';
|
||||
dl.download = filename;
|
||||
document.body.appendChild(dl);
|
||||
dl.click();
|
||||
};
|
||||
|
||||
const asyncForEach = async function(array, callback) {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
};
|
||||
|
||||
const getPageHtmlString = () => {
|
||||
return `<!doctype html>${window.document.querySelector('html').outerHTML}`;
|
||||
};
|
||||
|
||||
const removeAnchors = htmlString => {
|
||||
const replaceOpenAnchor = htmlString.replace(
|
||||
new RegExp('<a ', 'gim'),
|
||||
'<span ',
|
||||
);
|
||||
return replaceOpenAnchor.replace(new RegExp('</a', 'gim'), '</span');
|
||||
};
|
||||
|
||||
const ensureSidebarRemoved = () => {
|
||||
document.querySelector('.board-sidebar.sidebar').remove();
|
||||
};
|
||||
|
||||
const addJsonExportToZip = async (zip, boardSlug) => {
|
||||
const downloadJSONLink = document.querySelector('.download-json-link');
|
||||
const downloadJSONURL = downloadJSONLink.href;
|
||||
const response = await fetch(downloadJSONURL);
|
||||
const responseBody = await response.text();
|
||||
zip.file(`data/${boardSlug}.json`, responseBody);
|
||||
};
|
||||
|
||||
const closeSidebar = () => {
|
||||
document.querySelector('.board-header-btn.js-toggle-sidebar').click();
|
||||
};
|
||||
|
||||
const cleanBoardHtml = () => {
|
||||
Array.from(document.querySelectorAll('script')).forEach(elem =>
|
||||
elem.remove(),
|
||||
);
|
||||
Array.from(
|
||||
document.querySelectorAll('link:not([rel="stylesheet"])'),
|
||||
).forEach(elem => elem.remove());
|
||||
document.querySelector('#header-quick-access').remove();
|
||||
Array.from(
|
||||
document.querySelectorAll('#header-main-bar .board-header-btns'),
|
||||
).forEach(elem => elem.remove());
|
||||
Array.from(document.querySelectorAll('.list-composer')).forEach(elem =>
|
||||
elem.remove(),
|
||||
);
|
||||
Array.from(
|
||||
document.querySelectorAll(
|
||||
'.list-composer,.js-card-composer, .js-add-card',
|
||||
),
|
||||
).forEach(elem => elem.remove());
|
||||
Array.from(document.querySelectorAll('[href]:not(link)')).forEach(elem =>
|
||||
elem.attributes.removeNamedItem('href'),
|
||||
);
|
||||
Array.from(document.querySelectorAll('[href]')).forEach(elem => {
|
||||
// eslint-disable-next-line no-self-assign
|
||||
elem.href = elem.href;
|
||||
// eslint-disable-next-line no-self-assign
|
||||
elem.src = elem.src;
|
||||
});
|
||||
Array.from(document.querySelectorAll('.is-editable')).forEach(elem => {
|
||||
elem.classList.remove('is-editable');
|
||||
});
|
||||
};
|
||||
|
||||
const getBoardSlug = () => {
|
||||
return window.location.href.split('/').pop();
|
||||
};
|
||||
|
||||
const getStylesheetList = () => {
|
||||
return Array.from(
|
||||
document.querySelectorAll('link[href][rel="stylesheet"]'),
|
||||
);
|
||||
};
|
||||
|
||||
const downloadStylesheets = async (stylesheets, zip) => {
|
||||
await asyncForEach(stylesheets, async elem => {
|
||||
const response = await fetch(elem.href);
|
||||
const responseBody = await response.text();
|
||||
|
||||
const finalResponse = responseBody.replace(
|
||||
new RegExp('packages/[^/]+/upstream/', 'gim'),
|
||||
'../',
|
||||
);
|
||||
|
||||
const filename = elem.href
|
||||
.split('/')
|
||||
.pop()
|
||||
.split('?')
|
||||
.shift();
|
||||
const fileFullPath = `style/${filename}`;
|
||||
zip.file(fileFullPath, finalResponse);
|
||||
elem.href = `../${fileFullPath}`;
|
||||
});
|
||||
};
|
||||
|
||||
const getSrcAttached = () => {
|
||||
return Array.from(document.querySelectorAll('[src]'));
|
||||
};
|
||||
|
||||
const downloadSrcAttached = async (elements, zip, boardSlug) => {
|
||||
await asyncForEach(elements, async elem => {
|
||||
const response = await fetch(elem.src);
|
||||
const responseBody = await response.blob();
|
||||
const filename = elem.src
|
||||
.split('/')
|
||||
.pop()
|
||||
.split('?')
|
||||
.shift();
|
||||
const fileFullPath = `${boardSlug}/${elem.tagName.toLowerCase()}/${filename}`;
|
||||
zip.file(fileFullPath, responseBody);
|
||||
elem.src = `./${elem.tagName.toLowerCase()}/${filename}`;
|
||||
});
|
||||
};
|
||||
|
||||
const removeCssUrlSurround = url => {
|
||||
const working = url || '';
|
||||
return working
|
||||
.split('url(')
|
||||
.join('')
|
||||
.split('")')
|
||||
.join('')
|
||||
.split('"')
|
||||
.join('')
|
||||
.split("')")
|
||||
.join('')
|
||||
.split("'")
|
||||
.join('')
|
||||
.split(')')
|
||||
.join('');
|
||||
};
|
||||
|
||||
const getCardCovers = () => {
|
||||
return Array.from(document.querySelectorAll('.minicard-cover')).filter(
|
||||
elem => elem.style['background-image'],
|
||||
);
|
||||
};
|
||||
|
||||
const downloadCardCovers = async (elements, zip, boardSlug) => {
|
||||
await asyncForEach(elements, async elem => {
|
||||
const response = await fetch(
|
||||
removeCssUrlSurround(elem.style['background-image']),
|
||||
);
|
||||
const responseBody = await response.blob();
|
||||
const filename = removeCssUrlSurround(elem.style['background-image'])
|
||||
.split('/')
|
||||
.pop()
|
||||
.split('?')
|
||||
.shift()
|
||||
.split('#')
|
||||
.shift();
|
||||
const fileFullPath = `${boardSlug}/covers/${filename}`;
|
||||
zip.file(fileFullPath, responseBody);
|
||||
elem.style = "background-image: url('" + `covers/${filename}` + "')";
|
||||
});
|
||||
};
|
||||
|
||||
const addBoardHTMLToZip = (boardSlug, zip) => {
|
||||
ensureSidebarRemoved();
|
||||
const htmlOutputPath = `${boardSlug}/index.html`;
|
||||
zip.file(
|
||||
htmlOutputPath,
|
||||
new Blob([removeAnchors(getPageHtmlString())], {
|
||||
type: 'application/html',
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return async () => {
|
||||
const zip = new JSZip();
|
||||
const boardSlug = getBoardSlug();
|
||||
|
||||
await addJsonExportToZip(zip, boardSlug);
|
||||
Popup.close();
|
||||
closeSidebar();
|
||||
cleanBoardHtml();
|
||||
|
||||
await downloadStylesheets(getStylesheetList(), zip);
|
||||
await downloadSrcAttached(getSrcAttached(), zip, boardSlug);
|
||||
await downloadCardCovers(getCardCovers(), zip, boardSlug);
|
||||
|
||||
addBoardHTMLToZip(boardSlug, zip);
|
||||
|
||||
const content = await zip.generateAsync({ type: 'blob' });
|
||||
saveAs(content, `${boardSlug}.zip`);
|
||||
window.location.reload();
|
||||
};
|
||||
};
|
||||
Loaded 100 of 280 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user